Files
languageservices/workflow-parser/src/model/convert.ts
T

148 lines
4.9 KiB
TypeScript
Raw Normal View History

2023-01-09 19:02:19 -05:00
import {TemplateContext} from "../templates/template-context";
import {TemplateToken, TemplateTokenError} from "../templates/tokens/template-token";
2023-02-06 15:10:34 -05:00
import {FileProvider} from "../workflows/file-provider";
import {parseFileReference} from "../workflows/file-reference";
2023-02-06 15:10:34 -05:00
import {parseWorkflow} from "../workflows/workflow-parser";
2023-01-09 19:02:19 -05:00
import {convertConcurrency} from "./converter/concurrency";
import {convertOn} from "./converter/events";
import {handleTemplateTokenErrors} from "./converter/handle-errors";
import {convertJobs} from "./converter/jobs";
2023-02-06 15:10:34 -05:00
import {convertReferencedWorkflow} from "./converter/referencedWorkflow";
import {isReusableWorkflowJob} from "./type-guards";
2023-01-09 19:02:19 -05:00
import {WorkflowTemplate} from "./workflow-template";
export enum ErrorPolicy {
ReturnErrorsOnly,
2023-01-09 19:02:19 -05:00
TryConversion
}
2023-02-06 15:10:34 -05:00
export type WorkflowTemplateConverterOptions = {
/**
* The maximum depth of reusable workflows allowed in the workflow.
* If this depth is exceeded, an error will be reported.
* If {@link fetchReusableWorkflowDepth} is less than this value, the maximum depth
* won't be enforced.
* Default: 4
*/
maxReusableWorkflowDepth?: number;
/**
* The depth to fetch reusable workflows, up to {@link maxReusableWorkflowDepth}.
* Currently only a fetch depth of 0 or 1 is supported.
* Default: 0
*/
fetchReusableWorkflowDepth?: number;
/**
* The error policy to use when converting the workflow.
* By default, conversion will be skipped if there are errors in the {@link TemplateContext}.
*/
errorPolicy?: ErrorPolicy;
2023-02-06 15:10:34 -05:00
};
const defaultOptions: Required<WorkflowTemplateConverterOptions> = {
maxReusableWorkflowDepth: 4,
fetchReusableWorkflowDepth: 0,
errorPolicy: ErrorPolicy.ReturnErrorsOnly
2023-02-06 15:10:34 -05:00
};
export async function convertWorkflowTemplate(
context: TemplateContext,
root: TemplateToken,
2023-02-06 15:10:34 -05:00
fileProvider?: FileProvider,
options: WorkflowTemplateConverterOptions = defaultOptions
): Promise<WorkflowTemplate> {
2023-01-09 19:02:19 -05:00
const result = {} as WorkflowTemplate;
const opts = getOptionsWithDefaults(options);
if (context.errors.getErrors().length > 0 && opts.errorPolicy === ErrorPolicy.ReturnErrorsOnly) {
2023-01-09 19:02:19 -05:00
result.errors = context.errors.getErrors().map(x => ({
Message: x.message
}));
return result;
}
2023-02-06 15:10:34 -05:00
if (fileProvider === undefined && opts.fetchReusableWorkflowDepth > 0) {
context.error(root, new Error("A file provider is required to fetch reusable workflows"));
}
try {
2023-01-09 19:02:19 -05:00
const rootMapping = root.assertMapping("root");
for (const item of rootMapping) {
2023-01-09 19:02:19 -05:00
const key = item.key.assertString("root key");
switch (key.value) {
case "on":
2023-01-09 19:02:19 -05:00
result.events = handleTemplateTokenErrors(root, context, {}, () => convertOn(context, item.value));
break;
case "jobs":
2023-01-09 19:02:19 -05:00
result.jobs = handleTemplateTokenErrors(root, context, [], () => convertJobs(context, item.value));
break;
case "concurrency":
2023-01-09 19:02:19 -05:00
handleTemplateTokenErrors(root, context, {}, () => convertConcurrency(context, item.value));
result.concurrency = item.value;
break;
case "env":
2023-01-09 19:02:19 -05:00
result.env = item.value;
break;
}
}
2023-02-06 15:10:34 -05:00
// Load referenced workflows
for (const job of result.jobs || []) {
if (isReusableWorkflowJob(job)) {
if (opts.maxReusableWorkflowDepth === 0) {
context.error(job.ref, new Error("Reusable workflows are not allowed"));
continue;
}
if (opts.fetchReusableWorkflowDepth === 0 || fileProvider === undefined) {
continue;
}
try {
const file = await fileProvider.getFileContent(parseFileReference(job.ref.value));
2023-02-06 15:10:34 -05:00
const workflow = parseWorkflow(file, context);
if (!workflow.value) {
continue;
}
convertReferencedWorkflow(context, workflow.value, job);
} catch {
context.error(job.ref, new Error("Unable to find reusable workflow"));
}
}
}
} catch (err) {
if (err instanceof TemplateTokenError) {
2023-01-09 19:02:19 -05:00
context.error(err.token, err);
} else {
// Report error for the root node
2023-01-09 19:02:19 -05:00
context.error(root, err);
}
} finally {
if (context.errors.getErrors().length > 0) {
2023-01-09 19:02:19 -05:00
result.errors = context.errors.getErrors().map(x => ({
Message: x.message
}));
}
}
2023-01-09 19:02:19 -05:00
return result;
}
2023-02-06 15:10:34 -05:00
function getOptionsWithDefaults(options: WorkflowTemplateConverterOptions): Required<WorkflowTemplateConverterOptions> {
return {
maxReusableWorkflowDepth:
options.maxReusableWorkflowDepth !== undefined
? options.maxReusableWorkflowDepth
: defaultOptions.maxReusableWorkflowDepth,
fetchReusableWorkflowDepth:
options.fetchReusableWorkflowDepth !== undefined
? options.fetchReusableWorkflowDepth
: defaultOptions.fetchReusableWorkflowDepth,
errorPolicy: options.errorPolicy !== undefined ? options.errorPolicy : defaultOptions.errorPolicy
2023-02-06 15:10:34 -05:00
};
}