diff --git a/languageservice/src/complete.expressions.test.ts b/languageservice/src/complete.expressions.test.ts index d0dfdee..fc16165 100644 --- a/languageservice/src/complete.expressions.test.ts +++ b/languageservice/src/complete.expressions.test.ts @@ -729,6 +729,107 @@ jobs: }); }); + describe("jobs context", () => { + it("includes defined jobs", async () => { + const input = ` +on: + workflow_call: + # Map the workflow outputs to job outputs + outputs: + result: + description: "one or two" + value: \${{ jobs.| }} +jobs: + one: + runs-on: ubuntu-latest + steps: + - id: a + run: echo hello a + two: + runs-on: ubuntu-latest + steps: + - id: b + run: echo hello b +`; + const result = await complete(...getPositionFromCursor(input), {contextProviderConfig}); + + expect(result.map(x => x.label)).toEqual(["one", "two"]); + }); + + it("jobs..result only", async () => { + const input = ` +on: + workflow_call: + # Map the workflow outputs to job outputs + outputs: + result: + description: "The result" + value: \${{ jobs.one.| }} +jobs: + one: + runs-on: ubuntu-latest + steps: + - id: a + run: echo hello a +`; + const result = await complete(...getPositionFromCursor(input), {contextProviderConfig}); + + expect(result.map(x => x.label)).toEqual(["result"]); + }); + + it("jobs.", async () => { + const input = ` +on: + workflow_call: + outputs: + output1: + description: "A greeting" + value: \${{ jobs.example_job.| }} + +jobs: + example_job: + name: Generate output + runs-on: ubuntu-latest + # Map the job outputs to step outputs + outputs: + output1: "\${{ steps.a.outputs.greeting }}" + steps: + - id: a + run: echo "greeting=hello" >> $GITHUB_OUTPUT +`; + const result = await complete(...getPositionFromCursor(input), {contextProviderConfig}); + + expect(result.map(x => x.label)).toEqual(["outputs", "result"]); + }); + it("jobs..outputs", async () => { + const input = ` +on: + workflow_call: + outputs: + output1: + description: "A greeting" + value: \${{ jobs.example_job.outputs.| }} + +jobs: + example_job: + name: Generate output + runs-on: ubuntu-latest + # Map the job outputs to step outputs + outputs: + output1: "\${{ steps.a.outputs.greeting }}" + output2: "\${{ steps.b.outputs.greeting }}" + steps: + - id: a + run: echo "greeting=hello" >> $GITHUB_OUTPUT + - id: b + run: echo "greeting=hello" >> $GITHUB_OUTPUT +`; + const result = await complete(...getPositionFromCursor(input), {contextProviderConfig}); + + expect(result.map(x => x.label)).toEqual(["output1", "output2"]); + }); + }); + describe("strategy context", () => { it("strategy is not suggested when outside of a matrix job", async () => { const input = ` diff --git a/languageservice/src/context-providers/default.ts b/languageservice/src/context-providers/default.ts index d8732e3..da0f169 100644 --- a/languageservice/src/context-providers/default.ts +++ b/languageservice/src/context-providers/default.ts @@ -7,6 +7,7 @@ import {getEnvContext} from "./env"; import {getGithubContext} from "./github"; import {getInputsContext} from "./inputs"; import {getJobContext} from "./job"; +import {getJobsContext} from "./jobs"; import {getMatrixContext} from "./matrix"; import {getNeedsContext} from "./needs"; import {getStepsContext} from "./steps"; @@ -61,6 +62,9 @@ function getDefaultContext(name: string, workflowContext: WorkflowContext, mode: case "job": return getJobContext(workflowContext); + case "jobs": + return getJobsContext(workflowContext); + case "matrix": return getMatrixContext(workflowContext, mode); diff --git a/languageservice/src/context-providers/descriptions.json b/languageservice/src/context-providers/descriptions.json index 176871c..bb69918 100644 --- a/languageservice/src/context-providers/descriptions.json +++ b/languageservice/src/context-providers/descriptions.json @@ -172,6 +172,14 @@ "description": "`GITHUB_TOKEN` is a secret that is automatically created for every workflow run, and is always included in the secrets context. For more information, see [Automatic token authentication](https://docs.github.com/actions/security-guides/automatic-token-authentication)." } }, + "jobs": { + "outputs": { + "description": "The set of outputs of a job in a reusable workflow." + }, + "result": { + "description": "The result of a job in the reusable workflow. Possible values are `success`, `failure`, `cancelled`, or `skipped`." + } + }, "steps": { "outputs": { "description": "The set of outputs defined for the step." diff --git a/languageservice/src/context-providers/jobs.ts b/languageservice/src/context-providers/jobs.ts new file mode 100644 index 0000000..b889364 --- /dev/null +++ b/languageservice/src/context-providers/jobs.ts @@ -0,0 +1,40 @@ +import {data, DescriptionDictionary} from "@github/actions-expressions"; +import {StringData} from "@github/actions-expressions/data/string"; +import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token"; +import {WorkflowContext} from "../context/workflow-context"; +import {getDescription} from "./descriptions"; + +export function getJobsContext(workflowContext: WorkflowContext): DescriptionDictionary { + // https://docs.github.com/en/actions/learn-github-actions/contexts#jobs-context + const jobsContext = new DescriptionDictionary(); + + const jobs = workflowContext.template?.jobs; + if (!jobs) { + return jobsContext; + } + for (const job of jobs) { + const jobContext = new DescriptionDictionary(); + jobContext.add("result", new data.Null(), getDescription("jobs", "result")); + + const outputs = job.outputs || new data.Null(); + if (outputs instanceof MappingToken) { + jobContext.add("outputs", createOutputsContext(outputs as MappingToken), getDescription("jobs", "outputs")); + } + + jobsContext.add(job.id.toString(), jobContext); + } + + return jobsContext; +} + +function createOutputsContext(outputs: MappingToken): DescriptionDictionary { + const outputsContext = new DescriptionDictionary(); + for (const output of outputs) { + outputsContext.add( + output.key.toString(), + new StringData(output.value.toString()), + getDescription(output.key.toString(), "output") + ); + } + return outputsContext; +} diff --git a/languageservice/src/validate.expressions.test.ts b/languageservice/src/validate.expressions.test.ts index 7ca5fcb..9d1bc3a 100644 --- a/languageservice/src/validate.expressions.test.ts +++ b/languageservice/src/validate.expressions.test.ts @@ -404,6 +404,55 @@ jobs: }); }); + describe("jobs context", () => { + it("jobs..result", async () => { + const input = ` +on: + workflow_call: + # Map the workflow outputs to job outputs + outputs: + successful: + description: "Was job successful" + value: \${{ jobs.example_job.result }} + +jobs: + example_job: + runs-on: ubuntu-latest + steps: + - id: a + run: echo hello world`; + + const result = await validate(createDocument("wf.yaml", input)); + + expect(result).toEqual([]); + }); + + it("jobs..outputs.", async () => { + const input = ` +on: + workflow_call: + outputs: + output1: + description: "A greeting" + value: \${{ jobs.example_job.outputs.output1 }} + +jobs: + example_job: + name: Generate output + runs-on: ubuntu-latest + # Map the job outputs to step outputs + outputs: + output1: "\${{ steps.a.outputs.greeting }}" + steps: + - id: a + run: echo "greeting=hello" >> $GITHUB_OUTPUT`; + + const result = await validate(createDocument("wf.yaml", input)); + + expect(result).toEqual([]); + }); + }); + describe("env context", () => { it("references env within scope", async () => { const input = `