Parse reusable workflows up to a depth of 1
This commit is contained in:
@@ -8,12 +8,16 @@ const nullTrace: TraceWriter = {
|
||||
error: x => {}
|
||||
};
|
||||
|
||||
export function createWorkflowContext(workflow: string, job?: string, stepIndex?: number): WorkflowContext {
|
||||
export async function createWorkflowContext(
|
||||
workflow: string,
|
||||
job?: string,
|
||||
stepIndex?: number
|
||||
): Promise<WorkflowContext> {
|
||||
const parsed = parseWorkflow({name: "test.yaml", content: workflow}, nullTrace);
|
||||
if (!parsed.value) {
|
||||
throw new Error("Failed to parse workflow");
|
||||
}
|
||||
const template = convertWorkflowTemplate(parsed.context, parsed.value);
|
||||
const template = await convertWorkflowTemplate(parsed.context, parsed.value);
|
||||
const context: WorkflowContext = {uri: "test.yaml", template};
|
||||
|
||||
if (job) {
|
||||
|
||||
@@ -71,7 +71,7 @@ export async function complete(
|
||||
}
|
||||
|
||||
const {token, keyToken, parent, path} = findToken(newPos, result.value);
|
||||
const template = convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
|
||||
const workflowContext = getWorkflowContext(textDocument.uri, template, path);
|
||||
|
||||
// If we are inside an expression, take a different code-path. The workflow parser does not correctly create
|
||||
|
||||
@@ -5,7 +5,7 @@ import {getPositionFromCursor} from "../test-utils/cursor-position";
|
||||
import {findToken} from "../utils/find-token";
|
||||
import {getWorkflowContext, WorkflowContext} from "./workflow-context";
|
||||
|
||||
function testGetWorkflowContext(input: string): WorkflowContext {
|
||||
async function testGetWorkflowContext(input: string): Promise<WorkflowContext> {
|
||||
const [textDocument, pos] = getPositionFromCursor(input);
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
@@ -18,7 +18,7 @@ function testGetWorkflowContext(input: string): WorkflowContext {
|
||||
let template: WorkflowTemplate | undefined;
|
||||
|
||||
if (result.value) {
|
||||
template = convertWorkflowTemplate(result.context, result.value);
|
||||
template = await convertWorkflowTemplate(result.context, result.value);
|
||||
}
|
||||
|
||||
const {path} = findToken(pos, result.value);
|
||||
@@ -27,8 +27,8 @@ function testGetWorkflowContext(input: string): WorkflowContext {
|
||||
}
|
||||
|
||||
describe("getWorkflowContext", () => {
|
||||
it("context for workflow", () => {
|
||||
const context = testGetWorkflowContext(`on: push
|
||||
it("context for workflow", async () => {
|
||||
const context = await testGetWorkflowContext(`on: push
|
||||
name: te|st
|
||||
jobs:
|
||||
build:
|
||||
@@ -41,8 +41,8 @@ jobs:
|
||||
expect(context.step).toBeUndefined();
|
||||
});
|
||||
|
||||
it("context for workflow job", () => {
|
||||
const context = testGetWorkflowContext(`on: push
|
||||
it("context for workflow job", async () => {
|
||||
const context = await testGetWorkflowContext(`on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-lat|est
|
||||
@@ -54,8 +54,8 @@ jobs:
|
||||
expect(context.step).toBeUndefined();
|
||||
});
|
||||
|
||||
it("context for workflow run step", () => {
|
||||
const context = testGetWorkflowContext(`on: push
|
||||
it("context for workflow run step", async () => {
|
||||
const context = await testGetWorkflowContext(`on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -71,8 +71,8 @@ jobs:
|
||||
expect(step.run.toDisplayString()).toBe("echo Hello");
|
||||
});
|
||||
|
||||
it("context for workflow uses step", () => {
|
||||
const context = testGetWorkflowContext(`on: push
|
||||
it("context for workflow uses step", async () => {
|
||||
const context = await testGetWorkflowContext(`on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function documentLinks(document: TextDocument): Promise<DocumentLin
|
||||
return [];
|
||||
}
|
||||
|
||||
const template = convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
|
||||
// Add links to referenced actions
|
||||
const actionLinks: DocumentLink[] = [];
|
||||
|
||||
@@ -110,7 +110,7 @@ async function hoverExpression(input: string) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const template = convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
|
||||
const workflowContext = getWorkflowContext(td.uri, template, []);
|
||||
const context = await getContext(allowedContext, contextProviderConfig, workflowContext, Mode.Completion);
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ export async function hover(document: TextDocument, position: Position, config?:
|
||||
const allowedContext = tokenDefinitionInfo.allowedContext || [];
|
||||
const {namedContexts, functions} = splitAllowedContext(allowedContext);
|
||||
|
||||
const template = convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
|
||||
const workflowContext = getWorkflowContext(document.uri, template, tokenResult.path);
|
||||
const context = await getContext(namedContexts, config?.contextProviderConfig, workflowContext, Mode.Completion);
|
||||
|
||||
@@ -107,7 +107,7 @@ async function getDescription(
|
||||
return defaultDescription;
|
||||
}
|
||||
|
||||
const template = convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
|
||||
const workflowContext = getWorkflowContext(document.uri, template, path);
|
||||
const description = await config.descriptionProvider.getDescription(workflowContext, token, path);
|
||||
return description || defaultDescription;
|
||||
|
||||
@@ -59,7 +59,7 @@ export async function validate(
|
||||
const result: ParseWorkflowResult = parseWorkflow(file, nullTrace);
|
||||
if (result.value) {
|
||||
// Errors will be updated in the context. Attempt to do the conversion anyway in order to give the user more information
|
||||
const template = convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
|
||||
|
||||
// Validate expressions and value providers
|
||||
await additionalValidations(diagnostics, textDocument.uri, template, result.value, config);
|
||||
|
||||
@@ -7,7 +7,7 @@ function serializeTemplate(template: unknown): unknown {
|
||||
}
|
||||
|
||||
describe("convertWorkflowTemplate", () => {
|
||||
it("converts workflow with one job", () => {
|
||||
it("converts workflow with one job", async () => {
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
name: "wf.yaml",
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
nullTrace
|
||||
);
|
||||
|
||||
const template = convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
|
||||
expect(serializeTemplate(template)).toEqual({
|
||||
events: {
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
});
|
||||
});
|
||||
|
||||
it("converts workflow if expressions", () => {
|
||||
it("converts workflow if expressions", async () => {
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
name: "wf.yaml",
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
nullTrace
|
||||
);
|
||||
|
||||
const template = convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
|
||||
expect(serializeTemplate(template)).toEqual({
|
||||
events: {
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
});
|
||||
});
|
||||
|
||||
it("converts workflow with empty needs", () => {
|
||||
it("converts workflow with empty needs", async () => {
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
name: "wf.yaml",
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
nullTrace
|
||||
);
|
||||
|
||||
const template = convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
|
||||
expect(serializeTemplate(template)).toEqual({
|
||||
errors: [
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
});
|
||||
});
|
||||
|
||||
it("converts workflow with needs errors", () => {
|
||||
it("converts workflow with needs errors", async () => {
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
name: "wf.yaml",
|
||||
@@ -152,7 +152,7 @@ jobs:
|
||||
nullTrace
|
||||
);
|
||||
|
||||
const template = convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
|
||||
expect(serializeTemplate(template)).toEqual({
|
||||
errors: [
|
||||
@@ -211,7 +211,7 @@ jobs:
|
||||
});
|
||||
});
|
||||
|
||||
it("converts workflow with invalid on", () => {
|
||||
it("converts workflow with invalid on", async () => {
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
name: "wf.yaml",
|
||||
@@ -229,7 +229,7 @@ jobs:
|
||||
nullTrace
|
||||
);
|
||||
|
||||
const template = convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
|
||||
expect(template.jobs).not.toBeUndefined();
|
||||
expect(template.jobs).toHaveLength(1);
|
||||
@@ -271,7 +271,7 @@ jobs:
|
||||
});
|
||||
});
|
||||
|
||||
it("converts workflow with invalid jobs", () => {
|
||||
it("converts workflow with invalid jobs", async () => {
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
name: "wf.yaml",
|
||||
@@ -282,7 +282,7 @@ jobs:
|
||||
nullTrace
|
||||
);
|
||||
|
||||
const template = convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
|
||||
expect(template.jobs).not.toBeUndefined();
|
||||
expect(template.jobs).toHaveLength(0);
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import {TemplateContext} from "../templates/template-context";
|
||||
import {TemplateToken, TemplateTokenError} from "../templates/tokens/template-token";
|
||||
import {FileProvider} from "../workflows/file-provider";
|
||||
import {parseWorkflow} from "../workflows/workflow-parser";
|
||||
import {convertConcurrency} from "./converter/concurrency";
|
||||
import {convertOn} from "./converter/events";
|
||||
import {handleTemplateTokenErrors} from "./converter/handle-errors";
|
||||
import {convertJobs} from "./converter/jobs";
|
||||
import {convertReferencedWorkflow} from "./converter/referencedWorkflow";
|
||||
import {isReusableWorkflowJob} from "./type-guards";
|
||||
import {WorkflowTemplate} from "./workflow-template";
|
||||
|
||||
export enum ErrorPolicy {
|
||||
@@ -11,11 +15,35 @@ export enum ErrorPolicy {
|
||||
TryConversion
|
||||
}
|
||||
|
||||
export function convertWorkflowTemplate(
|
||||
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;
|
||||
};
|
||||
|
||||
const defaultOptions: Required<WorkflowTemplateConverterOptions> = {
|
||||
maxReusableWorkflowDepth: 4,
|
||||
fetchReusableWorkflowDepth: 0
|
||||
};
|
||||
|
||||
export async function convertWorkflowTemplate(
|
||||
context: TemplateContext,
|
||||
root: TemplateToken,
|
||||
errorPolicy: ErrorPolicy = ErrorPolicy.ReturnErrorsOnly
|
||||
): WorkflowTemplate {
|
||||
errorPolicy: ErrorPolicy = ErrorPolicy.ReturnErrorsOnly,
|
||||
fileProvider?: FileProvider,
|
||||
options: WorkflowTemplateConverterOptions = defaultOptions
|
||||
): Promise<WorkflowTemplate> {
|
||||
const result = {} as WorkflowTemplate;
|
||||
|
||||
if (context.errors.getErrors().length > 0 && errorPolicy === ErrorPolicy.ReturnErrorsOnly) {
|
||||
@@ -25,6 +53,12 @@ export function convertWorkflowTemplate(
|
||||
return result;
|
||||
}
|
||||
|
||||
const opts = getOptionsWithDefaults(options);
|
||||
|
||||
if (fileProvider === undefined && opts.fetchReusableWorkflowDepth > 0) {
|
||||
context.error(root, new Error("A file provider is required to fetch reusable workflows"));
|
||||
}
|
||||
|
||||
try {
|
||||
const rootMapping = root.assertMapping("root");
|
||||
|
||||
@@ -49,6 +83,31 @@ export function convertWorkflowTemplate(
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 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(job.ref.value);
|
||||
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) {
|
||||
context.error(err.token, err);
|
||||
@@ -66,3 +125,16 @@ export function convertWorkflowTemplate(
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getOptionsWithDefaults(options: WorkflowTemplateConverterOptions): Required<WorkflowTemplateConverterOptions> {
|
||||
return {
|
||||
maxReusableWorkflowDepth:
|
||||
options.maxReusableWorkflowDepth !== undefined
|
||||
? options.maxReusableWorkflowDepth
|
||||
: defaultOptions.maxReusableWorkflowDepth,
|
||||
fetchReusableWorkflowDepth:
|
||||
options.fetchReusableWorkflowDepth !== undefined
|
||||
? options.fetchReusableWorkflowDepth
|
||||
: defaultOptions.fetchReusableWorkflowDepth
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import {TemplateContext} from "../../templates/template-context";
|
||||
import {StringToken, MappingToken, BasicExpressionToken} from "../../templates/tokens";
|
||||
import {isSequence, isString} from "../../templates/tokens/type-guards";
|
||||
import {BaseJob} from "../workflow-template";
|
||||
import {convertConcurrency} from "./concurrency";
|
||||
import {handleTemplateTokenErrors} from "./handle-errors";
|
||||
import {IdBuilder} from "./id-builder";
|
||||
|
||||
export function convertBaseJob(
|
||||
context: TemplateContext,
|
||||
jobKey: StringToken,
|
||||
token: MappingToken
|
||||
): Omit<BaseJob, "type"> {
|
||||
const error = new IdBuilder().tryAddKnownId(jobKey.value);
|
||||
if (error) {
|
||||
context.error(jobKey, error);
|
||||
}
|
||||
|
||||
const result: Omit<BaseJob, "type"> = {
|
||||
id: jobKey,
|
||||
name: undefined,
|
||||
needs: undefined,
|
||||
if: new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined),
|
||||
concurrency: undefined,
|
||||
strategy: undefined
|
||||
};
|
||||
|
||||
for (const item of token) {
|
||||
const propertyName = item.key.assertString("job property name");
|
||||
switch (propertyName.value) {
|
||||
case "concurrency":
|
||||
handleTemplateTokenErrors(item.value, context, undefined, () => convertConcurrency(context, item.value));
|
||||
result.concurrency = item.value;
|
||||
break;
|
||||
|
||||
case "name":
|
||||
result.name = item.value.assertScalar("job name");
|
||||
break;
|
||||
|
||||
case "needs":
|
||||
result.needs = [];
|
||||
if (isString(item.value)) {
|
||||
const jobNeeds = item.value.assertString("job needs id");
|
||||
result.needs.push(jobNeeds);
|
||||
}
|
||||
|
||||
if (isSequence(item.value)) {
|
||||
for (const seqItem of item.value) {
|
||||
const jobNeeds = seqItem.assertString("job needs id");
|
||||
result.needs.push(jobNeeds);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "strategy":
|
||||
result.strategy = item.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.name) {
|
||||
result.name = result.id;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,70 +1,137 @@
|
||||
import {TemplateContext} from "../../templates/template-context";
|
||||
import {StringToken, MappingToken} from "../../templates/tokens";
|
||||
import {Job} from "../workflow-template";
|
||||
import {convertBaseJob} from "./base-job";
|
||||
import {StringToken, MappingToken, BasicExpressionToken, TemplateToken, ScalarToken} from "../../templates/tokens";
|
||||
import {isSequence, isString} from "../../templates/tokens/type-guards";
|
||||
import {WorkflowJob, Step} from "../workflow-template";
|
||||
import {convertConcurrency} from "./concurrency";
|
||||
import {convertToJobContainer, convertToJobServices} from "./container";
|
||||
import {handleTemplateTokenErrors} from "./handle-errors";
|
||||
import {IdBuilder} from "./id-builder";
|
||||
import {convertToActionsEnvironmentRef} from "./job/environment";
|
||||
import {convertRunsOn} from "./job/runs-on";
|
||||
import {convertSteps} from "./steps";
|
||||
|
||||
export function convertJob(context: TemplateContext, jobKey: StringToken, token: MappingToken): Job {
|
||||
const base = convertBaseJob(context, jobKey, token);
|
||||
const result: Job = {
|
||||
type: "job",
|
||||
id: base.id,
|
||||
name: base.name,
|
||||
needs: base.needs,
|
||||
if: base.if,
|
||||
env: undefined,
|
||||
concurrency: base.concurrency,
|
||||
environment: undefined,
|
||||
strategy: base.strategy,
|
||||
"runs-on": undefined,
|
||||
container: undefined,
|
||||
services: undefined,
|
||||
outputs: undefined,
|
||||
steps: []
|
||||
};
|
||||
export function convertJob(context: TemplateContext, jobKey: StringToken, token: MappingToken): WorkflowJob {
|
||||
const error = new IdBuilder().tryAddKnownId(jobKey.value);
|
||||
if (error) {
|
||||
context.error(jobKey, error);
|
||||
}
|
||||
|
||||
let concurrency, container, env, environment, name, outputs, runsOn, services, strategy: TemplateToken | undefined;
|
||||
let needs: StringToken[] | undefined = undefined;
|
||||
let steps: Step[] = [];
|
||||
let workflowJobRef: StringToken | undefined;
|
||||
|
||||
for (const item of token) {
|
||||
const propertyName = item.key.assertString("job property name");
|
||||
switch (propertyName.value) {
|
||||
case "concurrency":
|
||||
handleTemplateTokenErrors(item.value, context, undefined, () => convertConcurrency(context, item.value));
|
||||
concurrency = item.value;
|
||||
break;
|
||||
|
||||
case "container":
|
||||
convertToJobContainer(context, item.value);
|
||||
result.container = item.value;
|
||||
container = item.value;
|
||||
break;
|
||||
|
||||
case "env":
|
||||
result.env = item.value.assertMapping("job env");
|
||||
env = item.value.assertMapping("job env");
|
||||
break;
|
||||
|
||||
case "environment":
|
||||
handleTemplateTokenErrors(item.value, context, undefined, () =>
|
||||
convertToActionsEnvironmentRef(context, item.value)
|
||||
);
|
||||
result.environment = item.value;
|
||||
environment = item.value;
|
||||
break;
|
||||
|
||||
case "name":
|
||||
name = item.value.assertScalar("job name");
|
||||
break;
|
||||
|
||||
case "needs": {
|
||||
needs = [];
|
||||
if (isString(item.value)) {
|
||||
const jobNeeds = item.value.assertString("job needs id");
|
||||
needs.push(jobNeeds);
|
||||
}
|
||||
|
||||
if (isSequence(item.value)) {
|
||||
for (const seqItem of item.value) {
|
||||
const jobNeeds = seqItem.assertString("job needs id");
|
||||
needs.push(jobNeeds);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "outputs":
|
||||
result.outputs = item.value.assertMapping("job outputs");
|
||||
outputs = item.value.assertMapping("job outputs");
|
||||
break;
|
||||
|
||||
case "runs-on":
|
||||
handleTemplateTokenErrors(item.value, context, undefined, () => convertRunsOn(context, item.value));
|
||||
result["runs-on"] = item.value;
|
||||
runsOn = item.value;
|
||||
break;
|
||||
|
||||
case "services":
|
||||
convertToJobServices(context, item.value);
|
||||
result.services = item.value;
|
||||
services = item.value;
|
||||
break;
|
||||
|
||||
case "steps":
|
||||
result.steps = convertSteps(context, item.value);
|
||||
steps = convertSteps(context, item.value);
|
||||
break;
|
||||
|
||||
case "strategy":
|
||||
strategy = item.value;
|
||||
break;
|
||||
|
||||
case "uses":
|
||||
workflowJobRef = item.value.assertString("job item uses");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
if (workflowJobRef !== undefined) {
|
||||
return {
|
||||
type: "reusableWorkflowJob",
|
||||
id: jobKey,
|
||||
name: jobName(name, jobKey),
|
||||
needs: needs ?? [],
|
||||
if: new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined),
|
||||
ref: workflowJobRef,
|
||||
concurrency,
|
||||
strategy
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: "job",
|
||||
id: jobKey,
|
||||
name: jobName(name, jobKey),
|
||||
needs,
|
||||
if: new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined),
|
||||
env,
|
||||
concurrency,
|
||||
environment,
|
||||
strategy,
|
||||
"runs-on": runsOn,
|
||||
container,
|
||||
services,
|
||||
outputs,
|
||||
steps
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function jobName(name: ScalarToken | undefined, jobKey: StringToken): ScalarToken {
|
||||
if (name === undefined) {
|
||||
return jobKey;
|
||||
}
|
||||
|
||||
if (isString(name) && name.value === "") {
|
||||
return jobKey;
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import {TemplateContext} from "../../templates/template-context";
|
||||
import {StringToken} from "../../templates/tokens";
|
||||
import {TemplateToken} from "../../templates/tokens/template-token";
|
||||
import {isMapping} from "../../templates/tokens/type-guards";
|
||||
import {Job} from "../workflow-template";
|
||||
import {WorkflowJob} from "../workflow-template";
|
||||
import {handleTemplateTokenErrors} from "./handle-errors";
|
||||
import {convertJob} from "./job";
|
||||
|
||||
@@ -11,9 +11,9 @@ type nodeInfo = {
|
||||
needs: StringToken[];
|
||||
};
|
||||
|
||||
export function convertJobs(context: TemplateContext, token: TemplateToken): Job[] {
|
||||
export function convertJobs(context: TemplateContext, token: TemplateToken): WorkflowJob[] {
|
||||
if (isMapping(token)) {
|
||||
const result: Job[] = [];
|
||||
const result: WorkflowJob[] = [];
|
||||
const jobsWithSatisfiedNeeds: nodeInfo[] = [];
|
||||
const alljobsWithUnsatisfiedNeeds: nodeInfo[] = [];
|
||||
|
||||
@@ -49,7 +49,7 @@ export function convertJobs(context: TemplateContext, token: TemplateToken): Job
|
||||
function validateNeeds(
|
||||
token: TemplateToken,
|
||||
context: TemplateContext,
|
||||
result: Job[],
|
||||
result: WorkflowJob[],
|
||||
jobsWithSatisfiedNeeds: nodeInfo[],
|
||||
alljobsWithUnsatisfiedNeeds: nodeInfo[]
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import {TemplateContext} from "../../templates/template-context";
|
||||
import {TemplateToken} from "../../templates/tokens";
|
||||
import {TokenType} from "../../templates/tokens/types";
|
||||
import {ReusableWorkflowJob} from "../workflow-template";
|
||||
import {convertOn} from "./events";
|
||||
import {handleTemplateTokenErrors} from "./handle-errors";
|
||||
import {convertJobs} from "./jobs";
|
||||
|
||||
export function convertReferencedWorkflow(
|
||||
context: TemplateContext,
|
||||
referencedWorkflow: TemplateToken,
|
||||
job: ReusableWorkflowJob
|
||||
) {
|
||||
const mapping = referencedWorkflow.assertMapping("root");
|
||||
|
||||
let onToken: TemplateToken | undefined;
|
||||
for (const pair of mapping) {
|
||||
const key = pair.key.assertString("root key");
|
||||
switch (key.value) {
|
||||
case "on": {
|
||||
handleTemplateTokenErrors(mapping, context, undefined, () =>
|
||||
convertReferencedWorkflowOn(context, pair.value, job)
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "jobs": {
|
||||
job.jobs = handleTemplateTokenErrors(mapping, context, [], () => convertJobs(context, pair.value));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!onToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
const events = convertOn(context, onToken);
|
||||
if (events.workflow_call === undefined) {
|
||||
context.error(onToken, "workflow_call key is not defined in the referenced workflow.");
|
||||
}
|
||||
}
|
||||
|
||||
function convertReferencedWorkflowOn(context: TemplateContext, on: TemplateToken, job: ReusableWorkflowJob) {
|
||||
switch (on.templateTokenType) {
|
||||
case TokenType.String: {
|
||||
const event = on.assertString("Reference workflow on value").value;
|
||||
if (event === "workflow_call") {
|
||||
// TODO: Validate workflow job trigger
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case TokenType.Sequence: {
|
||||
const events = on.assertSequence("Reference workflow on value");
|
||||
for (const eventToken of events) {
|
||||
const event = eventToken.assertString(`Reference workflow on value ${eventToken}`).value;
|
||||
if (event === "workflow_call") {
|
||||
// TODO: Validate workflow job trigger
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case TokenType.Mapping: {
|
||||
const eventMapping = on.assertMapping("Reference workflow on value");
|
||||
|
||||
for (const pair of eventMapping) {
|
||||
const event = pair.key.assertString(`Reference workflow on value ${pair.key}`).value;
|
||||
if (event !== "workflow_call") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pair.value.templateTokenType === TokenType.Null) {
|
||||
// TODO: Validate workflow job trigger
|
||||
return;
|
||||
}
|
||||
|
||||
const definitions = pair.value.assertMapping(`Reference workflow on value ${pair.key}`);
|
||||
for (const definition of definitions) {
|
||||
const definitionKey = definition.key.assertString(`on-workflow_call-${definition.key}`).value;
|
||||
switch (definitionKey) {
|
||||
case "inputs":
|
||||
job["input-definitions"] = definition.value.assertMapping(`on-workflow_call-${definition.key}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Validate workflow job trigger
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
context.error(on, "workflow_call key is not defined in the referenced workflow.");
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import {TemplateContext} from "../../templates/template-context";
|
||||
import {MappingToken, StringToken} from "../../templates/tokens";
|
||||
import {ReusableWorkflowJob} from "../workflow-template";
|
||||
import {convertBaseJob} from "./base-job";
|
||||
|
||||
export function convertResuableWorkflowJob(
|
||||
context: TemplateContext,
|
||||
jobKey: StringToken,
|
||||
token: MappingToken
|
||||
): ReusableWorkflowJob {
|
||||
const base = convertBaseJob(context, jobKey, token);
|
||||
|
||||
const result: ReusableWorkflowJob = {
|
||||
type: "reusableWorkflowJob",
|
||||
id: base.id,
|
||||
name: base.name,
|
||||
needs: base.needs,
|
||||
if: base.if,
|
||||
concurrency: base.concurrency,
|
||||
strategy: base.strategy,
|
||||
uses: new StringToken(undefined, undefined, "undefined", undefined, undefined)
|
||||
};
|
||||
|
||||
for (const item of token) {
|
||||
const propertyName = item.key.assertString("job property name");
|
||||
switch (propertyName.value) {
|
||||
case "uses":
|
||||
result.uses = item.value.assertString("job item uses");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -57,7 +57,10 @@ export type Job = BaseJob & {
|
||||
// `workflow-job` in the schema
|
||||
export type ReusableWorkflowJob = BaseJob & {
|
||||
type: "reusableWorkflowJob";
|
||||
uses: StringToken;
|
||||
ref: StringToken;
|
||||
"input-definitions"?: MappingToken;
|
||||
"input-values"?: MappingToken;
|
||||
jobs?: WorkflowJob[];
|
||||
};
|
||||
|
||||
export type Container = {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import {File} from "./file";
|
||||
|
||||
export interface FileProvider {
|
||||
getFileContent(path: string): Promise<File>;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import * as templateReader from "../templates/template-reader";
|
||||
import {TemplateToken} from "../templates/tokens/template-token";
|
||||
import {TraceWriter} from "../templates/trace-writer";
|
||||
import {File} from "./file";
|
||||
import {FileProvider} from "./file-provider";
|
||||
import {WORKFLOW_ROOT} from "./workflow-constants";
|
||||
import {getWorkflowSchema} from "./workflow-schema";
|
||||
import {YamlObjectReader} from "./yaml-object-reader";
|
||||
@@ -12,8 +11,13 @@ export interface ParseWorkflowResult {
|
||||
value: TemplateToken | undefined;
|
||||
}
|
||||
|
||||
export function parseWorkflow(entryFile: File, trace: TraceWriter, fileProvider?: FileProvider): ParseWorkflowResult {
|
||||
const context = new TemplateContext(new TemplateValidationErrors(), getWorkflowSchema(), trace);
|
||||
export function parseWorkflow(entryFile: File, trace: TraceWriter): ParseWorkflowResult;
|
||||
export function parseWorkflow(entryFile: File, context: TemplateContext): ParseWorkflowResult;
|
||||
export function parseWorkflow(entryFile: File, contextOrTrace: TraceWriter | TemplateContext): ParseWorkflowResult {
|
||||
const context =
|
||||
contextOrTrace instanceof TemplateContext
|
||||
? contextOrTrace
|
||||
: new TemplateContext(new TemplateValidationErrors(), getWorkflowSchema(), contextOrTrace);
|
||||
|
||||
const fileId = context.getFileId(entryFile.name);
|
||||
const reader = new YamlObjectReader(context, fileId, entryFile.content);
|
||||
|
||||
@@ -3,6 +3,8 @@ import * as path from "path";
|
||||
import * as YAML from "yaml";
|
||||
import {convertWorkflowTemplate} from "./model/convert";
|
||||
import {TraceWriter} from "./templates/trace-writer";
|
||||
import {File} from "./workflows/file";
|
||||
import {FileProvider} from "./workflows/file-provider";
|
||||
import {parseWorkflow} from "./workflows/workflow-parser";
|
||||
|
||||
interface TestOptions {
|
||||
@@ -41,17 +43,34 @@ describe("x-lang tests", () => {
|
||||
const testOptions: TestOptions = YAML.parse(testDocs[0]);
|
||||
const unsupportedTest = contains(testOptions.skip, "TypeScript");
|
||||
|
||||
const test = () => {
|
||||
let testFileName = ".github/workflows" + fileName.substring(fileName.lastIndexOf("/"));
|
||||
let testInput = testDocs[1];
|
||||
let expectedTemplate = testDocs[2].trim();
|
||||
// TODO: when reusable workflows are implemented, implement correctly
|
||||
const test = async () => {
|
||||
const testFileName = ".github/workflows" + fileName.substring(fileName.lastIndexOf("/"));
|
||||
const testInput = testDocs[1];
|
||||
const expectedTemplate = testDocs[testDocs.length - 1].trim();
|
||||
|
||||
// For reusable workflow tests, additional workflows are passed in as pairs of
|
||||
// file names and file contents
|
||||
const reusableWorkflows: Record<string, File> = {};
|
||||
if (fileName.indexOf("reusable") !== -1) {
|
||||
testFileName = testDocs[1];
|
||||
testInput = testDocs[2];
|
||||
expectedTemplate = testDocs[3].trim();
|
||||
for (let i = 2; i < testDocs.length - 1; i = i + 2) {
|
||||
reusableWorkflows[testDocs[i]] = {
|
||||
name: testDocs[i],
|
||||
content: testDocs[i + 1]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const testFileProvider: FileProvider = {
|
||||
getFileContent: async (fileName: string) => {
|
||||
const file = reusableWorkflows[fileName];
|
||||
if (file) {
|
||||
return file;
|
||||
}
|
||||
|
||||
throw new Error("File not found: " + fileName);
|
||||
}
|
||||
};
|
||||
|
||||
const parseResult = parseWorkflow(
|
||||
{
|
||||
name: testFileName,
|
||||
@@ -62,7 +81,15 @@ describe("x-lang tests", () => {
|
||||
|
||||
expect(parseResult.value).not.toBeUndefined();
|
||||
|
||||
const workflowTemplate = convertWorkflowTemplate(parseResult.context, parseResult.value!);
|
||||
const workflowTemplate = await convertWorkflowTemplate(
|
||||
parseResult.context,
|
||||
parseResult.value!,
|
||||
undefined,
|
||||
testFileProvider,
|
||||
{
|
||||
fetchReusableWorkflowDepth: 1
|
||||
}
|
||||
);
|
||||
|
||||
// Unless this tests is only used by TypeScript, remove the events for now.
|
||||
// TODO: Remove this once we parse events everywhere
|
||||
|
||||
-1
@@ -1,7 +1,6 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- Go
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- Go
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- Go
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- Go
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
|
||||
Reference in New Issue
Block a user