Merge branch 'main' into joshmgross/validate-inputs-defined

This commit is contained in:
Christopher Schleiden
2022-12-05 15:20:13 -08:00
committed by GitHub
11 changed files with 328 additions and 22 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@github/actions-languageserver",
"version": "0.1.32",
"version": "0.1.33",
"description": "Language server for GitHub Actions",
"license": "MIT",
"type": "module",
@@ -38,7 +38,7 @@
"prettier-fix": "prettier --write ."
},
"dependencies": {
"@github/actions-languageservice": "^0.1.32",
"@github/actions-languageservice": "^0.1.33",
"@octokit/rest": "^19.0.5",
"vscode-languageserver": "^8.0.2",
"vscode-languageserver-textdocument": "^1.0.7"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@github/actions-languageservice",
"version": "0.1.32",
"version": "0.1.33",
"description": "Language service for GitHub Actions",
"license": "MIT",
"type": "module",
@@ -262,4 +262,78 @@ jobs:
expect(result).toEqual([]);
});
describe("steps context", () => {
it("includes defined step IDs", async () => {
const input = `
on: push
jobs:
one:
runs-on: ubuntu-latest
steps:
- id: a
run: echo hello a
- id: b
run: echo hello b
- id: c
run: echo "hello \${{ steps.|
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["a", "b"]);
});
it("step.<step_id>", async () => {
const input = `
on: push
jobs:
one:
runs-on: ubuntu-latest
steps:
- id: a
run: echo hello a
- run: echo "hello \${{ steps.a.|
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["conclusion", "outcome", "outputs"]);
});
it("ignores IDs from later steps", async () => {
const input = `
on: push
jobs:
one:
runs-on: ubuntu-latest
steps:
- id: a
run: echo hello a
- id: b
run: echo "hello \${{ steps.|
- id: c
run: echo hello c
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["a"]);
});
});
// The workflow parser doesn't currently generate IDs, so we can't test this yet
it.failing("Ignores generated IDs", async () => {
const input = `
on: push
jobs:
one:
runs-on: ubuntu-latest
steps:
- run: echo hello a
- id: b
run: echo hello b
- run: echo "hello \${{ steps.|
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["b"]);
});
});
@@ -3,6 +3,7 @@ import {WorkflowContext} from "../context/workflow-context";
import {ContextProviderConfig} from "./config";
import {getInputsContext} from "./inputs";
import {getNeedsContext} from "./needs";
import {getStepsContext} from "./steps";
export async function getContext(
names: string[],
@@ -46,6 +47,9 @@ async function getDefaultContext(name: string, workflowContext: WorkflowContext)
case "inputs":
return getInputsContext(workflowContext);
case "steps":
return getStepsContext(workflowContext);
}
return undefined;
@@ -21,7 +21,6 @@ function needsJobContext(job?: Job): data.Dictionary {
// https://docs.github.com/en/actions/learn-github-actions/contexts#needs-context
const d = new data.Dictionary();
// Once job config contains outputs, this can be populated
d.add("outputs", jobOutputs(job));
// Can be "success", "failure", "cancelled", or "skipped"
@@ -0,0 +1,46 @@
import {data} from "@github/actions-expressions";
import {Step} from "@github/actions-workflow-parser/model/workflow-template";
import {WorkflowContext} from "../context/workflow-context";
export function getStepsContext(workflowContext: WorkflowContext): data.Dictionary {
const d = new data.Dictionary();
if (!workflowContext.job?.steps) {
return d;
}
const currentStep = workflowContext.step?.id;
for (const step of workflowContext.job.steps) {
// We can't reference context from the current step or later steps
if (currentStep && step.id === currentStep) {
break;
}
if (isGenerated(step)) {
continue;
}
d.add(step.id, stepContext());
}
return d;
}
function stepContext(): data.Dictionary {
// https://docs.github.com/en/actions/learn-github-actions/contexts#steps-context
const d = new data.Dictionary();
d.add("outputs", new data.Null());
// Can be "success", "failure", "cancelled", or "skipped"
d.add("conclusion", new data.Null());
d.add("outcome", new data.Null());
return d;
}
function isGenerated(step: Step): boolean {
// Steps need to explicitly set an ID to be referenced in the context
// Generated IDs always start with "__", which is not allowed by user-defined IDs
return step.id.startsWith("__");
}
@@ -1,10 +1,11 @@
import {convertWorkflowTemplate, parseWorkflow, WorkflowTemplate} from "@github/actions-workflow-parser";
import {ActionStep, RunStep} from "@github/actions-workflow-parser/model/workflow-template";
import {nullTrace} from "../nulltrace";
import {getPositionFromCursor} from "../test-utils/cursor-position";
import {findToken} from "../utils/find-token";
import {getWorkflowContext, WorkflowContext} from "./workflow-context";
function testGetWorkflowContext(input: string): [context: WorkflowContext, template?: WorkflowTemplate] {
function testGetWorkflowContext(input: string): WorkflowContext {
const [textDocument, pos] = getPositionFromCursor(input);
const result = parseWorkflow(
"wf.yaml",
@@ -25,12 +26,12 @@ function testGetWorkflowContext(input: string): [context: WorkflowContext, templ
const {path} = findToken(pos, result.value);
return [getWorkflowContext(textDocument.uri, template, path), template];
return getWorkflowContext(textDocument.uri, template, path);
}
describe("getWorkflowContext", () => {
it("context for workflow", () => {
const [context, template] = testGetWorkflowContext(`on: push
const context = testGetWorkflowContext(`on: push
name: te|st
jobs:
build:
@@ -44,7 +45,7 @@ jobs:
});
it("context for workflow job", () => {
const [context, template] = testGetWorkflowContext(`on: push
const context = testGetWorkflowContext(`on: push
jobs:
build:
runs-on: ubuntu-lat|est
@@ -55,4 +56,38 @@ jobs:
expect(context.job).not.toBeUndefined();
expect(context.step).toBeUndefined();
});
it("context for workflow run step", () => {
const context = testGetWorkflowContext(`on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo |Hello
- uses: actions/checkout@v2`);
expect(context.uri).not.toBe("");
expect(context.template).not.toBeUndefined();
expect(context.job).not.toBeUndefined();
const step = context.step as RunStep;
expect(step).not.toBeUndefined();
expect(step.run.toDisplayString()).toBe("echo Hello");
});
it("context for workflow uses step", () => {
const context = testGetWorkflowContext(`on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo Hello
- uses: actions/checkout@v2|`);
expect(context.uri).not.toBe("");
expect(context.template).not.toBeUndefined();
expect(context.job).not.toBeUndefined();
const step = context.step as ActionStep;
expect(step).not.toBeUndefined();
expect(step.uses.value).toBe("actions/checkout@v2");
});
});
@@ -1,5 +1,7 @@
import {WorkflowTemplate} from "@github/actions-workflow-parser";
import {isMapping, isSequence, WorkflowTemplate} from "@github/actions-workflow-parser";
import {Job, Step} from "@github/actions-workflow-parser/model/workflow-template";
import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token";
import {SequenceToken} from "@github/actions-workflow-parser/templates/tokens/sequence-token";
import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token";
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
@@ -21,20 +23,60 @@ export function getWorkflowContext(
tokenPath: TemplateToken[]
): WorkflowContext {
const context: WorkflowContext = {uri: uri, template};
if (!template) {
return context;
}
if (template) {
// Iterate through the token path to find the job and step
for (let i = 0; i < tokenPath.length; ++i) {
const token = tokenPath[i];
let stepsSequence: SequenceToken | undefined = undefined;
let stepToken: MappingToken | undefined = undefined;
switch (token.definition?.key) {
case "job-id": {
const jobID = (token as StringToken).value;
context.job = template.jobs.find(job => job.id.value === jobID);
// Iterate through the token path to find the job and step
for (let i = 0; i < tokenPath.length; ++i) {
const token = tokenPath[i];
switch (token.definition?.key) {
case "job-id": {
const jobID = (token as StringToken).value;
context.job = template.jobs.find(job => job.id.value === jobID);
break;
}
case "steps": {
if (isSequence(token)) {
stepsSequence = token;
}
break;
}
case "regular-step":
case "run-step": {
if (isMapping(token)) {
stepToken = token;
}
break;
}
}
}
context.step = findStep(context.job?.steps, stepsSequence, stepToken);
return context;
}
function findStep(steps?: Step[], stepSequence?: SequenceToken, stepToken?: MappingToken): Step | undefined {
if (!steps || !stepSequence || !stepToken) {
return undefined;
}
// Steps may not define an ID, so find the step by index
let stepIndex = -1;
for (let i = 0; i < stepSequence.count; i++) {
if (stepSequence.get(i) === stepToken) {
stepIndex = i;
break;
}
}
if (stepIndex === -1 || stepIndex >= steps.length) {
return undefined;
}
return steps[stepIndex];
}
@@ -113,4 +113,110 @@ jobs:
]);
});
});
describe("steps context", () => {
it("steps.<step_id>", async () => {
const input = `
on: push
jobs:
a:
runs-on: ubuntu-latest
steps:
- id: a
run: echo hello a
- id: b
run: echo \${{ steps.a }}
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toEqual([]);
});
it("steps.<step_id>.outputs.<output_id>", async () => {
const input = `
on: push
jobs:
a:
runs-on: ubuntu-latest
steps:
- id: a
run: echo hello a
- id: b
run: echo \${{ steps.a.outputs.anything }}
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toEqual([]);
});
it("invalid reference of later step", async () => {
const input = `
on: push
jobs:
a:
runs-on: ubuntu-latest
steps:
- id: a
run: echo hello a
- id: b
run: echo \${{ steps.c }}
- id: c
run: echo hello c
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toEqual([
{
message: "Context access might be invalid: c",
range: {
end: {
character: 36,
line: 9
},
start: {
character: 22,
line: 9
}
},
severity: DiagnosticSeverity.Warning
}
]);
});
it("invalid reference of generated step name", async () => {
const input = `
on: push
jobs:
a:
runs-on: ubuntu-latest
steps:
- id: a
run: echo hello a
- id: b
run: echo \${{ steps.__run }}
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toEqual([
{
message: "Context access might be invalid: __run",
range: {
end: {
character: 40,
line: 9
},
start: {
character: 22,
line: 9
}
},
severity: DiagnosticSeverity.Warning
}
]);
});
});
});
+1 -1
View File
@@ -1,5 +1,5 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"useWorkspaces": true,
"version": "0.1.32"
"version": "0.1.33"
}
+4 -4
View File
@@ -15,10 +15,10 @@
},
"actions-languageserver": {
"name": "@github/actions-languageserver",
"version": "0.1.32",
"version": "0.1.33",
"license": "MIT",
"dependencies": {
"@github/actions-languageservice": "^0.1.32",
"@github/actions-languageservice": "^0.1.33",
"@octokit/rest": "^19.0.5",
"vscode-languageserver": "^8.0.2",
"vscode-languageserver-textdocument": "^1.0.7"
@@ -37,7 +37,7 @@
},
"actions-languageservice": {
"name": "@github/actions-languageservice",
"version": "0.1.32",
"version": "0.1.33",
"license": "MIT",
"dependencies": {
"@github/actions-workflow-parser": "*",
@@ -10899,7 +10899,7 @@
"@github/actions-languageserver": {
"version": "file:actions-languageserver",
"requires": {
"@github/actions-languageservice": "^0.1.32",
"@github/actions-languageservice": "^0.1.33",
"@octokit/rest": "^19.0.5",
"@types/jest": "^29.0.3",
"jest": "^29.0.3",