From cd38b2fe7690336c2fb07a5d02da373463dc68f9 Mon Sep 17 00:00:00 2001 From: Christopher Schleiden Date: Tue, 7 Feb 2023 14:52:12 -0800 Subject: [PATCH] Support `needs` for reusable jobs --- .../src/context-providers/needs.test.ts | 75 +++++++++++++++++++ .../src/context-providers/needs.ts | 7 +- .../src/context/workflow-context.test.ts | 27 +------ .../src/test-utils/test-workflow-context.ts | 26 +++++++ .../src/model/converter/job.ts | 6 +- 5 files changed, 110 insertions(+), 31 deletions(-) create mode 100644 actions-languageservice/src/context-providers/needs.test.ts create mode 100644 actions-languageservice/src/test-utils/test-workflow-context.ts diff --git a/actions-languageservice/src/context-providers/needs.test.ts b/actions-languageservice/src/context-providers/needs.test.ts new file mode 100644 index 0000000..31f8c24 --- /dev/null +++ b/actions-languageservice/src/context-providers/needs.test.ts @@ -0,0 +1,75 @@ +import {DescriptionDictionary} from "@github/actions-expressions/."; +import {WorkflowContext} from "../context/workflow-context"; +import {testGetWorkflowContext} from "../test-utils/test-workflow-context"; +import {getNeedsContext} from "./needs"; + +describe("needs context", () => { + describe("invalid workflow context", () => { + it("jobs not defined", () => { + const workflowContext = {} as WorkflowContext; + expect(workflowContext.job).toBeUndefined(); + expect(workflowContext.reusableWorkflowJob).toBeUndefined(); + + const context = getNeedsContext(workflowContext); + expect(context).toEqual(new DescriptionDictionary()); + }); + }); + + it("job without needs", async () => { + const workflowContext = await testGetWorkflowContext(`on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - run: ec|ho`); + + const context = getNeedsContext(workflowContext); + expect(context).toEqual(new DescriptionDictionary()); + }); + + it("job with needs", async () => { + const workflowContext = await testGetWorkflowContext(`on: push +jobs: + test: + runs-on: ubuntu-latest + steps: + - run: echo + build: + runs-on: ubuntu-latest + needs: [test] + steps: + - run: ec|ho`); + + const context = getNeedsContext(workflowContext); + expect(context.pairs().map(x => x.key)).toEqual(["test"]); + }); + + it("reusable job without needs", async () => { + const workflowContext = await testGetWorkflowContext(`on: push +jobs: + test: + runs-on: ubuntu-latest + steps: + - run: echo + build: + uses: ./.github/workflows/some-reusable-wor|kflow.yml`); + + const context = getNeedsContext(workflowContext); + expect(context).toEqual(new DescriptionDictionary()); + }); + + it("reusable job with needs", async () => { + const workflowContext = await testGetWorkflowContext(`on: push +jobs: + test: + runs-on: ubuntu-latest + steps: + - run: echo + build: + uses: ./.github/workflows/some-reusable-wor|kflow.yml + needs: [test]`); + + const context = getNeedsContext(workflowContext); + expect(context.pairs().map(x => x.key)).toEqual(["test"]); + }); +}); diff --git a/actions-languageservice/src/context-providers/needs.ts b/actions-languageservice/src/context-providers/needs.ts index e261b0d..23d9aa8 100644 --- a/actions-languageservice/src/context-providers/needs.ts +++ b/actions-languageservice/src/context-providers/needs.ts @@ -6,11 +6,14 @@ import {WorkflowContext} from "../context/workflow-context"; export function getNeedsContext(workflowContext: WorkflowContext): DescriptionDictionary { const d = new DescriptionDictionary(); - if (!workflowContext.job || !workflowContext.job.needs) { + + const job = workflowContext.job || workflowContext.reusableWorkflowJob; + + if (!job?.needs) { return d; } - for (const jobID of workflowContext.job.needs) { + for (const jobID of job.needs) { const job = workflowContext.template?.jobs.find(job => job.id.value === jobID.value); d.add(jobID.value, needsJobContext(job)); } diff --git a/actions-languageservice/src/context/workflow-context.test.ts b/actions-languageservice/src/context/workflow-context.test.ts index 1f2a3ad..6e99cdd 100644 --- a/actions-languageservice/src/context/workflow-context.test.ts +++ b/actions-languageservice/src/context/workflow-context.test.ts @@ -1,30 +1,5 @@ -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"; - -async function testGetWorkflowContext(input: string): Promise { - const [textDocument, pos] = getPositionFromCursor(input); - const result = parseWorkflow( - { - content: textDocument.getText(), - name: "wf.yaml" - }, - nullTrace - ); - - let template: WorkflowTemplate | undefined; - - if (result.value) { - template = await convertWorkflowTemplate(result.context, result.value); - } - - const {path} = findToken(pos, result.value); - - return getWorkflowContext(textDocument.uri, template, path); -} +import {testGetWorkflowContext} from "../test-utils/test-workflow-context"; describe("getWorkflowContext", () => { it("context for workflow", async () => { diff --git a/actions-languageservice/src/test-utils/test-workflow-context.ts b/actions-languageservice/src/test-utils/test-workflow-context.ts new file mode 100644 index 0000000..c5a1a7b --- /dev/null +++ b/actions-languageservice/src/test-utils/test-workflow-context.ts @@ -0,0 +1,26 @@ +import {convertWorkflowTemplate, parseWorkflow, WorkflowTemplate} from "@github/actions-workflow-parser"; +import {getWorkflowContext, WorkflowContext} from "../context/workflow-context"; +import {nullTrace} from "../nulltrace"; +import {findToken} from "../utils/find-token"; +import {getPositionFromCursor} from "./cursor-position"; + +export async function testGetWorkflowContext(input: string): Promise { + const [textDocument, pos] = getPositionFromCursor(input); + const result = parseWorkflow( + { + content: textDocument.getText(), + name: "wf.yaml" + }, + nullTrace + ); + + let template: WorkflowTemplate | undefined; + + if (result.value) { + template = await convertWorkflowTemplate(result.context, result.value); + } + + const {path} = findToken(pos, result.value); + + return getWorkflowContext(textDocument.uri, template, path); +} diff --git a/actions-workflow-parser/src/model/converter/job.ts b/actions-workflow-parser/src/model/converter/job.ts index ee5f30e..4d01148 100644 --- a/actions-workflow-parser/src/model/converter/job.ts +++ b/actions-workflow-parser/src/model/converter/job.ts @@ -1,7 +1,7 @@ import {TemplateContext} from "../../templates/template-context"; -import {StringToken, MappingToken, BasicExpressionToken, TemplateToken, ScalarToken} from "../../templates/tokens"; +import {BasicExpressionToken, MappingToken, ScalarToken, StringToken, TemplateToken} from "../../templates/tokens"; import {isSequence, isString} from "../../templates/tokens/type-guards"; -import {WorkflowJob, Step} from "../workflow-template"; +import {Step, WorkflowJob} from "../workflow-template"; import {convertConcurrency} from "./concurrency"; import {convertToJobContainer, convertToJobServices} from "./container"; import {handleTemplateTokenErrors} from "./handle-errors"; @@ -103,7 +103,7 @@ export function convertJob(context: TemplateContext, jobKey: StringToken, token: type: "reusableWorkflowJob", id: jobKey, name: jobName(name, jobKey), - needs: needs ?? [], + needs, if: new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined), ref: workflowJobRef, "input-definitions": undefined,