Merge pull request #131 from github/joshmgross/validate-wf-refs

Validate workflow references and inputs
This commit is contained in:
Josh Gross
2023-02-07 16:07:45 -05:00
committed by GitHub
3 changed files with 231 additions and 9 deletions
+11 -1
View File
@@ -14,6 +14,7 @@ import {BasicExpressionToken} from "@github/actions-workflow-parser/templates/to
import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token";
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
import {File} from "@github/actions-workflow-parser/workflows/file";
import {FileProvider} from "@github/actions-workflow-parser/workflows/file-provider";
import {TextDocument} from "vscode-languageserver-textdocument";
import {Diagnostic, DiagnosticSeverity, URI} from "vscode-languageserver-types";
import {ActionInputs, ActionReference} from "./action";
@@ -35,6 +36,7 @@ export type ValidationConfig = {
valueProviderConfig?: ValueProviderConfig;
contextProviderConfig?: ContextProviderConfig;
getActionInputs?(action: ActionReference): Promise<ActionInputs | undefined>;
fileProvider?: FileProvider;
};
/**
@@ -55,7 +57,15 @@ export async function validate(textDocument: TextDocument, config?: ValidationCo
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 = await convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
const template = await convertWorkflowTemplate(
result.context,
result.value,
ErrorPolicy.TryConversion,
config?.fileProvider,
{
fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0
}
);
// Validate expressions and value providers
await additionalValidations(diagnostics, textDocument.uri, template, result.value, config);
@@ -0,0 +1,208 @@
import {FileProvider} from "@github/actions-workflow-parser/workflows/file-provider";
import {fileIdentifier} from "@github/actions-workflow-parser/workflows/file-reference";
import {createDocument} from "./test-utils/document";
import {validate} from "./validate";
const testFileProvider: FileProvider = {
getFileContent: async ref => {
switch (fileIdentifier(ref)) {
case "monalisa/octocat/workflow.yaml@main":
return {
name: "monalisa/octocat/workflow.yaml",
content: `
on: workflow_call
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
`
};
case "monalisa/octocat/.github/workflows/non-reusable-workflow.yaml@main":
return {
name: "monalisa/octocat/.github/workflows/non-reusable-workflow.yaml",
content: `
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
`
};
case "./reusable-workflow.yaml":
return {
name: "reusable-workflow.yaml",
content: `
on: workflow_call
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
`
};
case "./reusable-workflow-with-inputs.yaml":
return {
name: "reusable-workflow-with-inputs.yaml",
content: `
on:
workflow_call:
inputs:
username:
description: 'A username passed from the caller workflow'
required: true
type: string
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
`
};
default:
throw new Error("File not found");
}
}
};
describe("workflow references validation", () => {
it("invalid workflow reference", async () => {
const input = `
on: push
jobs:
build:
uses: monalisa/octocat/.github/workflows/non-reusable-workflow.yaml@main
`;
const result = await validate(createDocument("wf.yaml", input), {
fileProvider: testFileProvider
});
expect(result).toEqual([
{
message: "workflow_call key is not defined in the referenced workflow.",
range: {
start: {
character: 10,
line: 5
},
end: {
character: 76,
line: 5
}
}
}
]);
});
it("reference to a non-reusable workflow", async () => {
const input = `
on: push
jobs:
build:
uses: monalisa/octocat/workflow.yaml@not-a-branch
`;
const result = await validate(createDocument("wf.yaml", input), {
fileProvider: testFileProvider
});
expect(result).toEqual([
{
message: "Unable to find reusable workflow",
range: {
start: {
character: 10,
line: 5
},
end: {
character: 53,
line: 5
}
}
}
]);
});
it("valid reference to a reusable workflow", async () => {
const input = `
on: push
jobs:
build:
uses: monalisa/octocat/workflow.yaml@main
`;
const result = await validate(createDocument("wf.yaml", input), {
fileProvider: testFileProvider
});
expect(result).toEqual([]);
});
it("valid reference to a local workflow", async () => {
const input = `
on: push
jobs:
build:
uses: ./reusable-workflow.yaml
`;
const result = await validate(createDocument("wf.yaml", input), {
fileProvider: testFileProvider
});
expect(result).toEqual([]);
});
it("workflow reference without required inputs", async () => {
const input = `
on: push
jobs:
build:
uses: ./reusable-workflow-with-inputs.yaml
`;
const result = await validate(createDocument("wf.yaml", input), {
fileProvider: testFileProvider
});
expect(result).toEqual([
{
message: "Input username is required, but not provided while calling.",
range: {
start: {
character: 10,
line: 5
},
end: {
character: 46,
line: 5
}
}
}
]);
});
it("workflow reference with required inputs", async () => {
const input = `
on: push
jobs:
build:
uses: ./reusable-workflow-with-inputs.yaml
with:
username: monalisa
`;
const result = await validate(createDocument("wf.yaml", input), {
fileProvider: testFileProvider
});
expect(result).toEqual([]);
});
});
@@ -13,18 +13,21 @@ export function convertReferencedWorkflow(
) {
const mapping = referencedWorkflow.assertMapping("root");
let onToken: TemplateToken | undefined;
// The language service doesn't currently handles on other documents,
// So use the ref in the original workflow as the error location
const tokenForErrors = job.ref;
for (const pair of mapping) {
const key = pair.key.assertString("root key");
switch (key.value) {
case "on": {
handleTemplateTokenErrors(mapping, context, undefined, () =>
handleTemplateTokenErrors(tokenForErrors, context, undefined, () =>
convertReferencedWorkflowOn(context, pair.value, job)
);
break;
}
case "jobs": {
job.jobs = handleTemplateTokenErrors(mapping, context, [], () => convertJobs(context, pair.value));
job.jobs = handleTemplateTokenErrors(tokenForErrors, context, [], () => convertJobs(context, pair.value));
break;
}
}
@@ -32,11 +35,12 @@ export function convertReferencedWorkflow(
}
function convertReferencedWorkflowOn(context: TemplateContext, on: TemplateToken, job: ReusableWorkflowJob) {
const tokenForErrors = job.ref;
switch (on.templateTokenType) {
case TokenType.String: {
const event = on.assertString("Reference workflow on value").value;
if (event === "workflow_call") {
handleTemplateTokenErrors(on, context, undefined, () => convertWorkflowJobInputs(context, job));
handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobInputs(context, job));
return;
}
break;
@@ -47,7 +51,7 @@ function convertReferencedWorkflowOn(context: TemplateContext, on: TemplateToken
for (const eventToken of events) {
const event = eventToken.assertString(`Reference workflow on value ${eventToken}`).value;
if (event === "workflow_call") {
handleTemplateTokenErrors(on, context, undefined, () => convertWorkflowJobInputs(context, job));
handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobInputs(context, job));
return;
}
}
@@ -64,7 +68,7 @@ function convertReferencedWorkflowOn(context: TemplateContext, on: TemplateToken
}
if (pair.value.templateTokenType === TokenType.Null) {
handleTemplateTokenErrors(on, context, undefined, () => convertWorkflowJobInputs(context, job));
handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobInputs(context, job));
return;
}
@@ -82,12 +86,12 @@ function convertReferencedWorkflowOn(context: TemplateContext, on: TemplateToken
}
}
handleTemplateTokenErrors(on, context, undefined, () => convertWorkflowJobInputs(context, job));
handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobInputs(context, job));
return;
}
break;
}
}
context.error(on, "workflow_call key is not defined in the referenced workflow.");
context.error(tokenForErrors, "workflow_call key is not defined in the referenced workflow.");
}