diff --git a/actions-languageservice/src/complete.expressions.test.ts b/actions-languageservice/src/complete.expressions.test.ts index 299ba69..87692dd 100644 --- a/actions-languageservice/src/complete.expressions.test.ts +++ b/actions-languageservice/src/complete.expressions.test.ts @@ -321,22 +321,19 @@ on: default: some value another-name: type: string + workflow_call: + inputs: + third-name: + type: boolean jobs: a: - outputs: - build_id: my-build-id - runs-on: ubuntu-latest - steps: - - run: echo hello a - b: - needs: [a] runs-on: ubuntu-latest steps: - run: echo "hello \${{ inputs.| `; const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig); - expect(result.map(x => x.label)).toEqual(["another-name", "name"]); + expect(result.map(x => x.label)).toEqual(["another-name", "name", "third-name"]); }); it("no inputs", async () => { @@ -345,13 +342,6 @@ on: workflow_dispatch: jobs: a: - outputs: - build_id: my-build-id - runs-on: ubuntu-latest - steps: - - run: echo hello a - b: - needs: [a] runs-on: ubuntu-latest steps: - run: echo "hello \${{ inputs.| @@ -361,21 +351,85 @@ jobs: expect(result).toEqual([]); }); - it("github context includes expected keys", async () => { - const input = ` -on: push + describe("github context", () => { + it("includes expected keys", async () => { + const input = ` + on: push + + jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: echo \${{ github.| }} + `; -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - run: echo \${{ github.| }} + const result = await complete(...getPositionFromCursor(input), undefined, undefined); + + expect(result.map(x => x.label)).toContain("actor"); + }); + + it("includes event inputs", async () => { + const input = ` + on: + workflow_dispatch: + inputs: + name: + type: string + default: some value + another-name: + type: string + workflow_call: + inputs: + third-name: + type: boolean + jobs: + a: + runs-on: ubuntu-latest + steps: + - run: echo "hello \${{ github.event.inputs.| `; + const result = await complete(...getPositionFromCursor(input), undefined, undefined); - const result = await complete(...getPositionFromCursor(input), undefined, undefined); + expect(result.map(x => x.label)).toEqual(["another-name", "name", "third-name"]); + }); - expect(result.map(x => x.label)).toContain("actor"); + it("excludes event inputs and cron when no relevant events", async () => { + const input = ` + on: push + + jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: echo \${{ github.event.| }} + `; + + const result = await complete(...getPositionFromCursor(input), undefined, undefined); + + expect(result.map(x => x.label)).not.toContain("inputs"); + expect(result.map(x => x.label)).not.toContain("cron"); + }); + + it("includes cron schedules", async () => { + const input = ` + on: + schedule: + - cron: '0 0 * * *' + + jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: echo \${{ github.event.| }} + `; + + const result = await complete(...getPositionFromCursor(input), undefined, undefined); + + expect(result.map(x => x.label)).toContain("cron"); + }); }); describe("steps context", () => { diff --git a/actions-languageservice/src/context-providers/default.ts b/actions-languageservice/src/context-providers/default.ts index e80cd45..4e8e62d 100644 --- a/actions-languageservice/src/context-providers/default.ts +++ b/actions-languageservice/src/context-providers/default.ts @@ -47,7 +47,7 @@ export async function getContext( function getDefaultContext(name: string, workflowContext: WorkflowContext, mode: Mode): ContextValue | undefined { switch (name) { case "github": - return getGithubContext(); + return getGithubContext(workflowContext); case "inputs": return getInputsContext(workflowContext); diff --git a/actions-languageservice/src/context-providers/github.ts b/actions-languageservice/src/context-providers/github.ts index 0422bb9..bd0917c 100644 --- a/actions-languageservice/src/context-providers/github.ts +++ b/actions-languageservice/src/context-providers/github.ts @@ -1,6 +1,9 @@ import {data} from "@github/actions-expressions"; +import {ExpressionData} from "@github/actions-expressions/data/expressiondata"; +import {WorkflowContext} from "../context/workflow-context"; +import {getInputsContext} from "./inputs"; -export function getGithubContext(): data.Dictionary { +export function getGithubContext(workflowContext: WorkflowContext): data.Dictionary { // https://docs.github.com/en/actions/learn-github-actions/contexts#github-context const keys = [ "action", @@ -41,7 +44,34 @@ export function getGithubContext(): data.Dictionary { return new data.Dictionary( ...keys.map(key => { + if (key == "event") { + return {key, value: getEventContext(workflowContext)}; + } + return {key, value: new data.Null()}; }) ); } + +function getEventContext(workflowContext: WorkflowContext): ExpressionData { + const d = new data.Dictionary(); + const events = workflowContext?.template?.events; + + if (!events) { + return d; + } + + const inputs = getInputsContext(workflowContext); + if (inputs.values().length > 0) { + d.add("inputs", inputs); + } + + const schedule = events["schedule"]; + if (schedule && schedule.length > 0) { + const default_cron = schedule[0].cron; + // For now, default to the first cron expression only + d.add("cron", new data.StringData(default_cron)); + } + + return d; +} diff --git a/actions-languageservice/src/context-providers/inputs.ts b/actions-languageservice/src/context-providers/inputs.ts index 167669f..22d08f7 100644 --- a/actions-languageservice/src/context-providers/inputs.ts +++ b/actions-languageservice/src/context-providers/inputs.ts @@ -1,21 +1,29 @@ import {data} from "@github/actions-expressions"; +import {InputConfig} from "@github/actions-workflow-parser/model/workflow-template"; import {WorkflowContext} from "../context/workflow-context"; export function getInputsContext(workflowContext: WorkflowContext): data.Dictionary { const d = new data.Dictionary(); - if (!workflowContext?.template?.events) { + + const events = workflowContext?.template?.events; + if (!events) { return d; } - const event = workflowContext.template.events["workflow_dispatch"]; - if (!event) { - return d; + const dispatch = events["workflow_dispatch"]; + if (dispatch?.inputs) { + addInputs(d, dispatch.inputs); } - const inputs = event.inputs; - if (!inputs) { - return d; + const call = events["workflow_call"]; + if (call?.inputs) { + addInputs(d, call.inputs); } + + return d; +} + +function addInputs(d: data.Dictionary, inputs: {[inputName: string]: InputConfig}) { for (const inputName of Object.keys(inputs)) { const input = inputs[inputName]; switch (input.type) { @@ -49,6 +57,4 @@ export function getInputsContext(workflowContext: WorkflowContext): data.Diction break; } } - - return d; } diff --git a/actions-languageservice/src/validate.expressions.test.ts b/actions-languageservice/src/validate.expressions.test.ts index ace4d85..f9777f7 100644 --- a/actions-languageservice/src/validate.expressions.test.ts +++ b/actions-languageservice/src/validate.expressions.test.ts @@ -988,4 +988,79 @@ jobs: ]); }); }); + + describe("github context", () => { + it("includes only expected keys", async () => { + const input = ` +on: push + +jobs: + test: + runs-on: ubuntu-latest + steps: + - run: echo \${{ github.action }} + - run: echo \${{ github.steps }} +`; + + const result = await validate(createDocument("wf.yaml", input)); + + expect(result).toEqual([ + { + message: "Context access might be invalid: steps", + range: { + end: { + character: 37, + line: 8 + }, + start: { + character: 18, + line: 8 + } + }, + severity: DiagnosticSeverity.Warning + } + ]); + }); + it("validates event inputs", async () => { + const input = ` +on: + workflow_dispatch: + inputs: + name: + type: string + default: some value + another-name: + type: string + workflow_call: + inputs: + third-name: + type: boolean +jobs: + a: + runs-on: ubuntu-latest + steps: + - run: echo "hello \${{ github.event.inputs.name }}" + - run: echo "hello \${{ github.event.inputs.third-name }}" + - run: echo "hello \${{ github.event.inputs.random }}" +`; + const result = await validate(createDocument("wf.yaml", input)); + + expect(result).toEqual([ + { + message: "Context access might be invalid: random", + range: { + end: { + character: 56, + line: 19 + }, + start: { + character: 23, + line: 19 + } + }, + severity: DiagnosticSeverity.Warning + } + ]); + }); + }); });