Parse reusable workflows up to a depth of 1

This commit is contained in:
Josh Gross
2023-02-06 15:10:34 -05:00
parent b9a01ed5aa
commit 13aaa381c8
22 changed files with 356 additions and 183 deletions
@@ -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) {
+1 -1
View File
@@ -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);
+2 -2
View File
@@ -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;
+1 -1
View File
@@ -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);
+75 -3
View File
@@ -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);
+36 -9
View File
@@ -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,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: