Merge branch 'main' into joshmgross/validate-inputs-defined
This commit is contained in:
@@ -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,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
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.1.32"
|
||||
"version": "0.1.33"
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user