diff --git a/languageservice/src/complete.expressions.test.ts b/languageservice/src/complete.expressions.test.ts index 0a2f2761..09767c8c 100644 --- a/languageservice/src/complete.expressions.test.ts +++ b/languageservice/src/complete.expressions.test.ts @@ -1164,7 +1164,16 @@ jobs: `; const result = await complete(...getPositionFromCursor(input), {contextProviderConfig}); - expect(result.map(x => x.label)).toEqual(["check_run_id", "container", "services", "status"]); + expect(result.map(x => x.label)).toEqual([ + "check_run_id", + "container", + "services", + "status", + "workflow_file_path", + "workflow_ref", + "workflow_repository", + "workflow_sha" + ]); }); it("job context is suggested within a job output", async () => { diff --git a/languageservice/src/context-providers/descriptions.json b/languageservice/src/context-providers/descriptions.json index b43af228..c8326f64 100644 --- a/languageservice/src/context-providers/descriptions.json +++ b/languageservice/src/context-providers/descriptions.json @@ -218,6 +218,18 @@ }, "check_run_id": { "description": "The unique identifier of the check run for this job." + }, + "workflow_file_path": { + "description": "The path of the workflow file that contains the job. For example, `.github/workflows/my-workflow.yml`." + }, + "workflow_ref": { + "description": "The ref path to the workflow file that contains the job. For example, `octocat/hello-world/.github/workflows/my-workflow.yml@refs/heads/my_branch`." + }, + "workflow_repository": { + "description": "The owner and repository name of the workflow file that contains the job. For example, `octocat/Hello-World`." + }, + "workflow_sha": { + "description": "The commit SHA of the workflow file that contains the job." } }, "secrets": { diff --git a/languageservice/src/context-providers/job.test.ts b/languageservice/src/context-providers/job.test.ts index 4f16596f..0e558443 100644 --- a/languageservice/src/context-providers/job.test.ts +++ b/languageservice/src/context-providers/job.test.ts @@ -18,12 +18,16 @@ describe("job context", () => { expect(context.pairs().length).toBe(0); }); - it("returns status and check_run_id when job has no container or services", () => { + it("returns status, check_run_id, and workflow fields when job has no container or services", () => { const workflowContext = {job: {}} as WorkflowContext; const context = getJobContext(workflowContext); expect(context.get("status")).toBeDefined(); expect(context.get("check_run_id")).toBeDefined(); + expect(context.get("workflow_ref")).toBeDefined(); + expect(context.get("workflow_sha")).toBeDefined(); + expect(context.get("workflow_repository")).toBeDefined(); + expect(context.get("workflow_file_path")).toBeDefined(); expect(context.get("container")).toBeUndefined(); expect(context.get("services")).toBeUndefined(); }); @@ -173,4 +177,21 @@ describe("job context", () => { expect(redis.getDescription("ports")).toBeDefined(); }); }); + + describe("workflow context fields", () => { + it("includes workflow context fields with descriptions", () => { + const workflowContext = {job: {}} as WorkflowContext; + const context = getJobContext(workflowContext); + + expect(context.get("workflow_ref")).toBeDefined(); + expect(context.get("workflow_sha")).toBeDefined(); + expect(context.get("workflow_repository")).toBeDefined(); + expect(context.get("workflow_file_path")).toBeDefined(); + + expect(context.getDescription("workflow_ref")).toBeDefined(); + expect(context.getDescription("workflow_sha")).toBeDefined(); + expect(context.getDescription("workflow_repository")).toBeDefined(); + expect(context.getDescription("workflow_file_path")).toBeDefined(); + }); + }); }); diff --git a/languageservice/src/context-providers/job.ts b/languageservice/src/context-providers/job.ts index d0846671..2e066e0c 100644 --- a/languageservice/src/context-providers/job.ts +++ b/languageservice/src/context-providers/job.ts @@ -5,7 +5,7 @@ import {WorkflowContext} from "../context/workflow-context.js"; import {getDescription} from "./descriptions.js"; /** - * Returns the job context with container, services, status, and check_run_id. + * Returns the job context with container, services, status, check_run_id, and workflow identity fields. */ export function getJobContext(workflowContext: WorkflowContext): DescriptionDictionary { // https://docs.github.com/en/actions/learn-github-actions/contexts#job-context @@ -42,6 +42,12 @@ export function getJobContext(workflowContext: WorkflowContext): DescriptionDict // Check run ID jobContext.add("check_run_id", new data.StringData(""), getDescription("job", "check_run_id")); + // Workflow context fields (populated at runtime for reusable workflow jobs) + jobContext.add("workflow_file_path", new data.StringData(""), getDescription("job", "workflow_file_path")); + jobContext.add("workflow_ref", new data.StringData(""), getDescription("job", "workflow_ref")); + jobContext.add("workflow_repository", new data.StringData(""), getDescription("job", "workflow_repository")); + jobContext.add("workflow_sha", new data.StringData(""), getDescription("job", "workflow_sha")); + return jobContext; } diff --git a/languageservice/src/validate.expressions.test.ts b/languageservice/src/validate.expressions.test.ts index 35302903..850251fc 100644 --- a/languageservice/src/validate.expressions.test.ts +++ b/languageservice/src/validate.expressions.test.ts @@ -432,6 +432,24 @@ jobs: expect(result).toEqual([]); }); + it("job.workflow_* fields", async () => { + const input = ` +on: push + +jobs: + test: + runs-on: ubuntu-latest + steps: + - run: echo \${{ job.workflow_ref }} + - run: echo \${{ job.workflow_sha }} + - run: echo \${{ job.workflow_repository }} + - run: echo \${{ job.workflow_file_path }} +`; + const result = await validate(createDocument("wf.yaml", input)); + + expect(result).toEqual([]); + }); + it("job.services.", async () => { const input = ` on: push