Merge branch 'main' into joshmgross/validate-inputs-defined
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@github/actions-languageserver",
|
"name": "@github/actions-languageserver",
|
||||||
"version": "0.1.32",
|
"version": "0.1.33",
|
||||||
"description": "Language server for GitHub Actions",
|
"description": "Language server for GitHub Actions",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"prettier-fix": "prettier --write ."
|
"prettier-fix": "prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@github/actions-languageservice": "^0.1.32",
|
"@github/actions-languageservice": "^0.1.33",
|
||||||
"@octokit/rest": "^19.0.5",
|
"@octokit/rest": "^19.0.5",
|
||||||
"vscode-languageserver": "^8.0.2",
|
"vscode-languageserver": "^8.0.2",
|
||||||
"vscode-languageserver-textdocument": "^1.0.7"
|
"vscode-languageserver-textdocument": "^1.0.7"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@github/actions-languageservice",
|
"name": "@github/actions-languageservice",
|
||||||
"version": "0.1.32",
|
"version": "0.1.33",
|
||||||
"description": "Language service for GitHub Actions",
|
"description": "Language service for GitHub Actions",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -262,4 +262,78 @@ jobs:
|
|||||||
|
|
||||||
expect(result).toEqual([]);
|
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 {ContextProviderConfig} from "./config";
|
||||||
import {getInputsContext} from "./inputs";
|
import {getInputsContext} from "./inputs";
|
||||||
import {getNeedsContext} from "./needs";
|
import {getNeedsContext} from "./needs";
|
||||||
|
import {getStepsContext} from "./steps";
|
||||||
|
|
||||||
export async function getContext(
|
export async function getContext(
|
||||||
names: string[],
|
names: string[],
|
||||||
@@ -46,6 +47,9 @@ async function getDefaultContext(name: string, workflowContext: WorkflowContext)
|
|||||||
|
|
||||||
case "inputs":
|
case "inputs":
|
||||||
return getInputsContext(workflowContext);
|
return getInputsContext(workflowContext);
|
||||||
|
|
||||||
|
case "steps":
|
||||||
|
return getStepsContext(workflowContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ function needsJobContext(job?: Job): data.Dictionary {
|
|||||||
// https://docs.github.com/en/actions/learn-github-actions/contexts#needs-context
|
// https://docs.github.com/en/actions/learn-github-actions/contexts#needs-context
|
||||||
const d = new data.Dictionary();
|
const d = new data.Dictionary();
|
||||||
|
|
||||||
// Once job config contains outputs, this can be populated
|
|
||||||
d.add("outputs", jobOutputs(job));
|
d.add("outputs", jobOutputs(job));
|
||||||
|
|
||||||
// Can be "success", "failure", "cancelled", or "skipped"
|
// 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 {convertWorkflowTemplate, parseWorkflow, WorkflowTemplate} from "@github/actions-workflow-parser";
|
||||||
|
import {ActionStep, RunStep} from "@github/actions-workflow-parser/model/workflow-template";
|
||||||
import {nullTrace} from "../nulltrace";
|
import {nullTrace} from "../nulltrace";
|
||||||
import {getPositionFromCursor} from "../test-utils/cursor-position";
|
import {getPositionFromCursor} from "../test-utils/cursor-position";
|
||||||
import {findToken} from "../utils/find-token";
|
import {findToken} from "../utils/find-token";
|
||||||
import {getWorkflowContext, WorkflowContext} from "./workflow-context";
|
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 [textDocument, pos] = getPositionFromCursor(input);
|
||||||
const result = parseWorkflow(
|
const result = parseWorkflow(
|
||||||
"wf.yaml",
|
"wf.yaml",
|
||||||
@@ -25,12 +26,12 @@ function testGetWorkflowContext(input: string): [context: WorkflowContext, templ
|
|||||||
|
|
||||||
const {path} = findToken(pos, result.value);
|
const {path} = findToken(pos, result.value);
|
||||||
|
|
||||||
return [getWorkflowContext(textDocument.uri, template, path), template];
|
return getWorkflowContext(textDocument.uri, template, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("getWorkflowContext", () => {
|
describe("getWorkflowContext", () => {
|
||||||
it("context for workflow", () => {
|
it("context for workflow", () => {
|
||||||
const [context, template] = testGetWorkflowContext(`on: push
|
const context = testGetWorkflowContext(`on: push
|
||||||
name: te|st
|
name: te|st
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -44,7 +45,7 @@ jobs:
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("context for workflow job", () => {
|
it("context for workflow job", () => {
|
||||||
const [context, template] = testGetWorkflowContext(`on: push
|
const context = testGetWorkflowContext(`on: push
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-lat|est
|
runs-on: ubuntu-lat|est
|
||||||
@@ -55,4 +56,38 @@ jobs:
|
|||||||
expect(context.job).not.toBeUndefined();
|
expect(context.job).not.toBeUndefined();
|
||||||
expect(context.step).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 {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 {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token";
|
||||||
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
|
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
|
||||||
|
|
||||||
@@ -21,20 +23,60 @@ export function getWorkflowContext(
|
|||||||
tokenPath: TemplateToken[]
|
tokenPath: TemplateToken[]
|
||||||
): WorkflowContext {
|
): WorkflowContext {
|
||||||
const context: WorkflowContext = {uri: uri, template};
|
const context: WorkflowContext = {uri: uri, template};
|
||||||
|
if (!template) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
if (template) {
|
let stepsSequence: SequenceToken | undefined = undefined;
|
||||||
// Iterate through the token path to find the job and step
|
let stepToken: MappingToken | undefined = undefined;
|
||||||
for (let i = 0; i < tokenPath.length; ++i) {
|
|
||||||
const token = tokenPath[i];
|
|
||||||
|
|
||||||
switch (token.definition?.key) {
|
// Iterate through the token path to find the job and step
|
||||||
case "job-id": {
|
for (let i = 0; i < tokenPath.length; ++i) {
|
||||||
const jobID = (token as StringToken).value;
|
const token = tokenPath[i];
|
||||||
context.job = template.jobs.find(job => job.id.value === jobID);
|
|
||||||
|
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;
|
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",
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||||
"useWorkspaces": true,
|
"useWorkspaces": true,
|
||||||
"version": "0.1.32"
|
"version": "0.1.33"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+4
-4
@@ -15,10 +15,10 @@
|
|||||||
},
|
},
|
||||||
"actions-languageserver": {
|
"actions-languageserver": {
|
||||||
"name": "@github/actions-languageserver",
|
"name": "@github/actions-languageserver",
|
||||||
"version": "0.1.32",
|
"version": "0.1.33",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@github/actions-languageservice": "^0.1.32",
|
"@github/actions-languageservice": "^0.1.33",
|
||||||
"@octokit/rest": "^19.0.5",
|
"@octokit/rest": "^19.0.5",
|
||||||
"vscode-languageserver": "^8.0.2",
|
"vscode-languageserver": "^8.0.2",
|
||||||
"vscode-languageserver-textdocument": "^1.0.7"
|
"vscode-languageserver-textdocument": "^1.0.7"
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
},
|
},
|
||||||
"actions-languageservice": {
|
"actions-languageservice": {
|
||||||
"name": "@github/actions-languageservice",
|
"name": "@github/actions-languageservice",
|
||||||
"version": "0.1.32",
|
"version": "0.1.33",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@github/actions-workflow-parser": "*",
|
"@github/actions-workflow-parser": "*",
|
||||||
@@ -10899,7 +10899,7 @@
|
|||||||
"@github/actions-languageserver": {
|
"@github/actions-languageserver": {
|
||||||
"version": "file:actions-languageserver",
|
"version": "file:actions-languageserver",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@github/actions-languageservice": "^0.1.32",
|
"@github/actions-languageservice": "^0.1.33",
|
||||||
"@octokit/rest": "^19.0.5",
|
"@octokit/rest": "^19.0.5",
|
||||||
"@types/jest": "^29.0.3",
|
"@types/jest": "^29.0.3",
|
||||||
"jest": "^29.0.3",
|
"jest": "^29.0.3",
|
||||||
|
|||||||
Reference in New Issue
Block a user