From fe16d1a1ed13d605e6a958a16b56c2d899a69a7c Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Fri, 3 Feb 2023 16:57:28 -0500 Subject: [PATCH 1/8] Add initial support for converting reusable wf jobs --- .../src/context-providers/env.ts | 6 +- .../src/context-providers/job.ts | 3 +- .../src/context-providers/needs.ts | 14 +- .../src/context-providers/steps.ts | 3 +- .../src/context/workflow-context.ts | 10 +- actions-languageservice/src/document-links.ts | 6 +- .../src/workflow-provider.ts | 5 + .../src/model/converter/base-job.ts | 66 ++++++ .../src/model/converter/job.ts | 70 ++++++ .../src/model/converter/job/runs-on.ts | 102 +++++++++ .../src/model/converter/jobs.ts | 207 +----------------- .../src/model/converter/reusable-job.ts | 34 +++ .../src/model/type-guards.ts | 10 +- .../src/model/workflow-template.ts | 23 +- 14 files changed, 335 insertions(+), 224 deletions(-) create mode 100644 actions-languageservice/src/workflow-provider.ts create mode 100644 actions-workflow-parser/src/model/converter/base-job.ts create mode 100644 actions-workflow-parser/src/model/converter/job.ts create mode 100644 actions-workflow-parser/src/model/converter/job/runs-on.ts create mode 100644 actions-workflow-parser/src/model/converter/reusable-job.ts diff --git a/actions-languageservice/src/context-providers/env.ts b/actions-languageservice/src/context-providers/env.ts index 5aa22cb..8aaeaf4 100644 --- a/actions-languageservice/src/context-providers/env.ts +++ b/actions-languageservice/src/context-providers/env.ts @@ -1,5 +1,6 @@ import {data, DescriptionDictionary} from "@github/actions-expressions"; import {isScalar, isString} from "@github/actions-workflow-parser"; +import {isJob} from "@github/actions-workflow-parser/model/type-guards"; import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token"; import {WorkflowContext} from "../context/workflow-context"; @@ -12,8 +13,9 @@ export function getEnvContext(workflowContext: WorkflowContext): DescriptionDict } //job env - if (workflowContext.job && workflowContext.job.env) { - envContext(workflowContext.job.env, d); + const job = workflowContext.job; + if (job && isJob(job) && job.env) { + envContext(job.env, d); } //workflow env diff --git a/actions-languageservice/src/context-providers/job.ts b/actions-languageservice/src/context-providers/job.ts index 6c37ab6..5adf7e4 100644 --- a/actions-languageservice/src/context-providers/job.ts +++ b/actions-languageservice/src/context-providers/job.ts @@ -1,5 +1,6 @@ import {data, DescriptionDictionary} from "@github/actions-expressions"; import {isMapping, isSequence} from "@github/actions-workflow-parser"; +import {isReusableWorkflowJob} from "@github/actions-workflow-parser/model/type-guards"; import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token"; import {WorkflowContext} from "../context/workflow-context"; @@ -7,7 +8,7 @@ export function getJobContext(workflowContext: WorkflowContext): DescriptionDict // https://docs.github.com/en/actions/learn-github-actions/contexts#job-context const jobContext = new DescriptionDictionary(); const job = workflowContext.job; - if (!job) { + if (!job || isReusableWorkflowJob(job)) { return jobContext; } diff --git a/actions-languageservice/src/context-providers/needs.ts b/actions-languageservice/src/context-providers/needs.ts index e6cb66d..455bfd5 100644 --- a/actions-languageservice/src/context-providers/needs.ts +++ b/actions-languageservice/src/context-providers/needs.ts @@ -1,6 +1,7 @@ import {data, DescriptionDictionary} from "@github/actions-expressions"; import {isScalar, isString} from "@github/actions-workflow-parser"; -import {Job} from "@github/actions-workflow-parser/model/workflow-template"; +import {isJob} from "@github/actions-workflow-parser/model/type-guards"; +import {Job, WorkflowJob} from "@github/actions-workflow-parser/model/workflow-template"; import {WorkflowContext} from "../context/workflow-context"; export function getNeedsContext(workflowContext: WorkflowContext): DescriptionDictionary { @@ -17,20 +18,21 @@ export function getNeedsContext(workflowContext: WorkflowContext): DescriptionDi return d; } -function needsJobContext(job?: Job): DescriptionDictionary { +function needsJobContext(job?: WorkflowJob): DescriptionDictionary { // https://docs.github.com/en/actions/learn-github-actions/contexts#needs-context const d = new DescriptionDictionary(); - d.add("outputs", jobOutputs(job)); - + if (job && isJob(job)) { + d.add("outputs", jobOutputs(job)); + } // Can be "success", "failure", "cancelled", or "skipped" d.add("result", new data.Null()); return d; } -function jobOutputs(job?: Job): DescriptionDictionary { +function jobOutputs(job: Job): DescriptionDictionary { const d = new DescriptionDictionary(); - if (!job?.outputs) { + if (!job.outputs) { return d; } diff --git a/actions-languageservice/src/context-providers/steps.ts b/actions-languageservice/src/context-providers/steps.ts index b07b18e..fb2d76d 100644 --- a/actions-languageservice/src/context-providers/steps.ts +++ b/actions-languageservice/src/context-providers/steps.ts @@ -1,11 +1,12 @@ import {data, DescriptionDictionary} from "@github/actions-expressions"; +import {isReusableWorkflowJob} from "@github/actions-workflow-parser/model/type-guards"; import {Step} from "@github/actions-workflow-parser/model/workflow-template"; import {WorkflowContext} from "../context/workflow-context"; import {getDescription} from "./descriptions"; export function getStepsContext(workflowContext: WorkflowContext): DescriptionDictionary { const d = new DescriptionDictionary(); - if (!workflowContext.job?.steps) { + if (!workflowContext.job || isReusableWorkflowJob(workflowContext.job)) { return d; } diff --git a/actions-languageservice/src/context/workflow-context.ts b/actions-languageservice/src/context/workflow-context.ts index 6ba3f28..2b77a24 100644 --- a/actions-languageservice/src/context/workflow-context.ts +++ b/actions-languageservice/src/context/workflow-context.ts @@ -1,5 +1,6 @@ import {isMapping, isSequence, WorkflowTemplate} from "@github/actions-workflow-parser"; -import {Job, Step} from "@github/actions-workflow-parser/model/workflow-template"; +import {isJob, isReusableWorkflowJob} from "@github/actions-workflow-parser/model/type-guards"; +import {Step, WorkflowJob} 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"; @@ -11,7 +12,7 @@ export interface WorkflowContext { template: WorkflowTemplate | undefined; /** If the context is for a position within a job, this will be the job */ - job?: Job; + job?: WorkflowJob; /** If the context is for a position within a step, this will be the step */ step?: Step; @@ -54,7 +55,10 @@ export function getWorkflowContext( } } - context.step = findStep(context.job?.steps, stepsSequence, stepToken); + if (context.job && isJob(context.job)) { + context.step = findStep(context.job.steps, stepsSequence, stepToken); + } + return context; } diff --git a/actions-languageservice/src/document-links.ts b/actions-languageservice/src/document-links.ts index 8bc17cb..cd739c0 100644 --- a/actions-languageservice/src/document-links.ts +++ b/actions-languageservice/src/document-links.ts @@ -1,5 +1,6 @@ import {convertWorkflowTemplate, parseWorkflow} from "@github/actions-workflow-parser"; import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert"; +import {isJob} from "@github/actions-workflow-parser/model/type-guards"; import {File} from "@github/actions-workflow-parser/workflows/file"; import {TextDocument} from "vscode-languageserver-textdocument"; import {DocumentLink} from "vscode-languageserver-types"; @@ -27,7 +28,10 @@ export async function documentLinks(document: TextDocument): Promise Promise; +}; diff --git a/actions-workflow-parser/src/model/converter/base-job.ts b/actions-workflow-parser/src/model/converter/base-job.ts new file mode 100644 index 0000000..0b1d02e --- /dev/null +++ b/actions-workflow-parser/src/model/converter/base-job.ts @@ -0,0 +1,66 @@ +import {TemplateContext} from "../../templates/template-context"; +import {StringToken, MappingToken, BasicExpressionToken} from "../../templates/tokens"; +import {isSequence, isString} from "../../templates/tokens/type-guards"; +import {BaseJob} from "../workflow-template"; +import {convertConcurrency} from "./concurrency"; +import {handleTemplateTokenErrors} from "./handle-errors"; +import {IdBuilder} from "./id-builder"; + +export function convertBaseJob( + context: TemplateContext, + jobKey: StringToken, + token: MappingToken +): Omit { + const error = new IdBuilder().tryAddKnownId(jobKey.value); + if (error) { + context.error(jobKey, error); + } + + const result: Omit = { + id: jobKey, + name: undefined, + needs: undefined, + if: new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined), + concurrency: undefined, + strategy: undefined + }; + + for (const item of token) { + const propertyName = item.key.assertString("job property name"); + switch (propertyName.value) { + case "concurrency": + handleTemplateTokenErrors(item.value, context, undefined, () => convertConcurrency(context, item.value)); + result.concurrency = item.value; + break; + + case "name": + result.name = item.value.assertScalar("job name"); + break; + + case "needs": + result.needs = []; + if (isString(item.value)) { + const jobNeeds = item.value.assertString("job needs id"); + result.needs.push(jobNeeds); + } + + if (isSequence(item.value)) { + for (const seqItem of item.value) { + const jobNeeds = seqItem.assertString("job needs id"); + result.needs.push(jobNeeds); + } + } + break; + + case "strategy": + result.strategy = item.value; + break; + } + } + + if (!result.name) { + result.name = result.id; + } + + return result; +} diff --git a/actions-workflow-parser/src/model/converter/job.ts b/actions-workflow-parser/src/model/converter/job.ts new file mode 100644 index 0000000..4fda96b --- /dev/null +++ b/actions-workflow-parser/src/model/converter/job.ts @@ -0,0 +1,70 @@ +import {TemplateContext} from "../../templates/template-context"; +import {StringToken, MappingToken} from "../../templates/tokens"; +import {Job} from "../workflow-template"; +import {convertBaseJob} from "./base-job"; +import {convertToJobContainer, convertToJobServices} from "./container"; +import {handleTemplateTokenErrors} from "./handle-errors"; +import {convertToActionsEnvironmentRef} from "./job/environment"; +import {convertRunsOn} from "./job/runs-on"; +import {convertSteps} from "./steps"; + +export function convertJob(context: TemplateContext, jobKey: StringToken, token: MappingToken): Job { + const base = convertBaseJob(context, jobKey, token); + const result: Job = { + type: "job", + id: base.id, + name: base.name, + needs: base.needs, + if: base.if, + env: undefined, + concurrency: base.concurrency, + environment: undefined, + strategy: base.strategy, + "runs-on": undefined, + container: undefined, + services: undefined, + outputs: undefined, + steps: [] + }; + + for (const item of token) { + const propertyName = item.key.assertString("job property name"); + switch (propertyName.value) { + case "container": + convertToJobContainer(context, item.value); + result.container = item.value; + break; + + case "env": + result.env = item.value.assertMapping("job env"); + break; + + case "environment": + handleTemplateTokenErrors(item.value, context, undefined, () => + convertToActionsEnvironmentRef(context, item.value) + ); + result.environment = item.value; + break; + + case "outputs": + result.outputs = item.value.assertMapping("job outputs"); + break; + + case "runs-on": + handleTemplateTokenErrors(item.value, context, undefined, () => convertRunsOn(context, item.value)); + result["runs-on"] = item.value; + break; + + case "services": + convertToJobServices(context, item.value); + result.services = item.value; + break; + + case "steps": + result.steps = convertSteps(context, item.value); + break; + } + } + + return result; +} diff --git a/actions-workflow-parser/src/model/converter/job/runs-on.ts b/actions-workflow-parser/src/model/converter/job/runs-on.ts new file mode 100644 index 0000000..c96da91 --- /dev/null +++ b/actions-workflow-parser/src/model/converter/job/runs-on.ts @@ -0,0 +1,102 @@ +import {TemplateContext} from "../../../templates/template-context"; +import {TemplateToken} from "../../../templates/tokens"; +import {isMapping, isString, isSequence} from "../../../templates/tokens/type-guards"; + +type RunsOn = { + labels: Set; + group: string; +}; + +export function convertRunsOn(context: TemplateContext, token: TemplateToken): RunsOn { + const labels = convertRunsOnLabels(token); + + if (!isMapping(token)) { + return { + labels, + group: "" + }; + } + + let group = ""; + + for (const item of token) { + const key = item.key.assertString("job runs-on property name"); + switch (key.value) { + case "group": { + if (item.value.isExpression) { + continue; + } + + const groupName = item.value.assertString("job runs-on group name").value; + const names = groupName.split("/"); + switch (names.length) { + case 1: { + group = groupName; + break; + } + case 2: { + if (!["org", "organization", "ent", "enterprise"].includes(names[0])) { + context.error( + item.value, + `Invalid runs-on group name '${groupName}. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.'` + ); + continue; + } + if (!names[1]) { + context.error(item.value, `Invalid runs-on group name '${groupName}'.`); + continue; + } + + group = groupName; + break; + } + default: { + context.error( + item.value, + `Invalid runs-on group name '${groupName}. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.'` + ); + break; + } + } + break; + } + case "labels": { + const mapLabels = convertRunsOnLabels(item.value); + for (const label of mapLabels) { + labels.add(label); + } + break; + } + } + } + + return { + labels, + group + }; +} + +function convertRunsOnLabels(token: TemplateToken): Set { + const labels = new Set(); + if (token.isExpression) { + return labels; + } + + if (isString(token)) { + labels.add(token.value); + return labels; + } + + if (isSequence(token)) { + for (const item of token) { + if (item.isExpression) { + continue; + } + + const label = item.assertString("job runs-on label sequence item"); + labels.add(label.value); + } + } + + return labels; +} diff --git a/actions-workflow-parser/src/model/converter/jobs.ts b/actions-workflow-parser/src/model/converter/jobs.ts index ac31142..5aab4c5 100644 --- a/actions-workflow-parser/src/model/converter/jobs.ts +++ b/actions-workflow-parser/src/model/converter/jobs.ts @@ -1,14 +1,10 @@ import {TemplateContext} from "../../templates/template-context"; -import {BasicExpressionToken, MappingToken, StringToken} from "../../templates/tokens"; +import {StringToken} from "../../templates/tokens"; import {TemplateToken} from "../../templates/tokens/template-token"; -import {isMapping, isSequence, isString} from "../../templates/tokens/type-guards"; +import {isMapping} from "../../templates/tokens/type-guards"; import {Job} from "../workflow-template"; -import {convertConcurrency} from "./concurrency"; -import {convertToJobContainer, convertToJobServices} from "./container"; import {handleTemplateTokenErrors} from "./handle-errors"; -import {IdBuilder} from "./id-builder"; -import {convertToActionsEnvironmentRef} from "./job/environment"; -import {convertSteps} from "./steps"; +import {convertJob} from "./job"; type nodeInfo = { name: string; @@ -100,200 +96,3 @@ function validateNeeds( } } } - -function convertJob(context: TemplateContext, jobKey: StringToken, token: MappingToken): Job { - const error = new IdBuilder().tryAddKnownId(jobKey.value); - if (error) { - context.error(jobKey, error); - } - const result: Job = { - type: "job", - id: jobKey, - name: undefined, - needs: undefined, - if: new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined), - env: undefined, - concurrency: undefined, - environment: undefined, - strategy: undefined, - "runs-on": undefined, - container: undefined, - services: undefined, - outputs: undefined, - steps: [] - }; - - for (const item of token) { - const propertyName = item.key.assertString("job property name"); - switch (propertyName.value) { - case "concurrency": - handleTemplateTokenErrors(item.value, context, undefined, () => convertConcurrency(context, item.value)); - result.concurrency = item.value; - break; - - case "container": - // Do early validation, but don't convert - convertToJobContainer(context, item.value); - result.container = item.value; - break; - - case "env": - result.env = item.value.assertMapping("job env"); - break; - - case "environment": - handleTemplateTokenErrors(item.value, context, undefined, () => - convertToActionsEnvironmentRef(context, item.value) - ); - result.environment = item.value; - break; - - case "name": - result.name = item.value.assertScalar("job name"); - break; - - case "needs": - result.needs = []; - if (isString(item.value)) { - const jobNeeds = item.value.assertString("job needs id"); - result.needs.push(jobNeeds); - } - - if (isSequence(item.value)) { - for (const seqItem of item.value) { - const jobNeeds = seqItem.assertString("job needs id"); - result.needs.push(jobNeeds); - } - } - break; - - case "outputs": - result.outputs = item.value.assertMapping("job outputs"); - break; - - case "runs-on": - handleTemplateTokenErrors(item.value, context, undefined, () => convertRunsOn(context, item.value)); - result["runs-on"] = item.value; - break; - - case "services": - // Do early validation, but don't convert - convertToJobServices(context, item.value); - result.services = item.value; - break; - - case "steps": - result.steps = convertSteps(context, item.value); - break; - - case "strategy": - result.strategy = item.value; - break; - } - } - - if (!result.name) { - result.name = result.id; - } - - return result; -} - -type RunsOn = { - labels: Set; - group: string; -}; - -function convertRunsOn(context: TemplateContext, token: TemplateToken): RunsOn { - const labels = convertRunsOnLabels(token); - - if (!isMapping(token)) { - return { - labels, - group: "" - }; - } - - let group = ""; - - for (const item of token) { - const key = item.key.assertString("job runs-on property name"); - switch (key.value) { - case "group": { - if (item.value.isExpression) { - continue; - } - - const groupName = item.value.assertString("job runs-on group name").value; - const names = groupName.split("/"); - switch (names.length) { - case 1: { - group = groupName; - break; - } - case 2: { - if (!["org", "organization", "ent", "enterprise"].includes(names[0])) { - context.error( - item.value, - `Invalid runs-on group name '${groupName}. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.'` - ); - continue; - } - if (!names[1]) { - context.error(item.value, `Invalid runs-on group name '${groupName}'.`); - continue; - } - - group = groupName; - break; - } - default: { - context.error( - item.value, - `Invalid runs-on group name '${groupName}. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.'` - ); - break; - } - } - break; - } - case "labels": { - const mapLabels = convertRunsOnLabels(item.value); - for (const label of mapLabels) { - labels.add(label); - } - break; - } - } - } - - return { - labels, - group - }; -} - -function convertRunsOnLabels(token: TemplateToken): Set { - const labels = new Set(); - if (token.isExpression) { - return labels; - } - - if (isString(token)) { - labels.add(token.value); - return labels; - } - - if (isSequence(token)) { - for (const item of token) { - if (item.isExpression) { - continue; - } - - const label = item.assertString("job runs-on label sequence item"); - labels.add(label.value); - } - } - - return labels; -} diff --git a/actions-workflow-parser/src/model/converter/reusable-job.ts b/actions-workflow-parser/src/model/converter/reusable-job.ts new file mode 100644 index 0000000..6c75ff3 --- /dev/null +++ b/actions-workflow-parser/src/model/converter/reusable-job.ts @@ -0,0 +1,34 @@ +import {TemplateContext} from "../../templates/template-context"; +import {MappingToken, StringToken} from "../../templates/tokens"; +import {ReusableWorkflowJob} from "../workflow-template"; +import {convertBaseJob} from "./base-job"; + +export function convertResuableWorkflowJob( + context: TemplateContext, + jobKey: StringToken, + token: MappingToken +): ReusableWorkflowJob { + const base = convertBaseJob(context, jobKey, token); + + const result: ReusableWorkflowJob = { + type: "job", + id: base.id, + name: base.name, + needs: base.needs, + if: base.if, + concurrency: base.concurrency, + strategy: base.strategy, + uses: new StringToken(undefined, undefined, "undefined", undefined, undefined) + }; + + for (const item of token) { + const propertyName = item.key.assertString("job property name"); + switch (propertyName.value) { + case "uses": + result.uses = item.value.assertString("job item uses"); + break; + } + } + + return result; +} diff --git a/actions-workflow-parser/src/model/type-guards.ts b/actions-workflow-parser/src/model/type-guards.ts index a1e09c0..bd201c1 100644 --- a/actions-workflow-parser/src/model/type-guards.ts +++ b/actions-workflow-parser/src/model/type-guards.ts @@ -1,4 +1,4 @@ -import {ActionStep, RunStep, Step} from "./workflow-template"; +import {ActionStep, Job, ReusableWorkflowJob, RunStep, Step} from "./workflow-template"; export function isRunStep(step: Step): step is RunStep { return (step as RunStep).run !== undefined; @@ -7,3 +7,11 @@ export function isRunStep(step: Step): step is RunStep { export function isActionStep(step: Step): step is ActionStep { return (step as ActionStep).uses !== undefined; } + +export function isJob(job: Job | ReusableWorkflowJob): job is Job { + return job.type === "job"; +} + +export function isReusableWorkflowJob(job: Job | ReusableWorkflowJob): job is ReusableWorkflowJob { + return job.type === "reusableWorkflowJob"; +} diff --git a/actions-workflow-parser/src/model/workflow-template.ts b/actions-workflow-parser/src/model/workflow-template.ts index 2ede8f5..c50a20e 100644 --- a/actions-workflow-parser/src/model/workflow-template.ts +++ b/actions-workflow-parser/src/model/workflow-template.ts @@ -9,7 +9,7 @@ import { export type WorkflowTemplate = { events: EventsConfig; - jobs: Job[]; + jobs: WorkflowJob[]; concurrency: TemplateToken; env: TemplateToken; @@ -28,16 +28,24 @@ export type ActionsEnvironmentReference = { url?: TemplateToken; }; -export type Job = { - type: string; +export type WorkflowJob = Job | ReusableWorkflowJob; + +export type JobType = "job" | "reusableWorkflowJob"; + +export type BaseJob = { + type: JobType; id: StringToken; name?: ScalarToken; needs?: StringToken[]; if: BasicExpressionToken; - env?: MappingToken; concurrency?: TemplateToken; - environment?: TemplateToken; strategy?: TemplateToken; +}; + +// `job-factory` in the schema +export type Job = BaseJob & { + env?: MappingToken; + environment?: TemplateToken; "runs-on"?: TemplateToken; container?: TemplateToken; services?: TemplateToken; @@ -45,6 +53,11 @@ export type Job = { steps: Step[]; }; +// `workflow-job` in the schema +export type ReusableWorkflowJob = BaseJob & { + uses: StringToken; +}; + export type Container = { image: StringToken; credentials?: Credential; From 6124e78f28536e63ab7592c15a0ddbcc97ac0817 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Fri, 3 Feb 2023 16:57:44 -0500 Subject: [PATCH 2/8] Add missing `.prettierignore` files --- actions-expressions/.prettierignore | 5 +++++ actions-languageserver/.prettierignore | 5 +++++ actions-workflow-parser/.prettierignore | 5 +++++ browser-playground/.prettierignore | 5 +++++ 4 files changed, 20 insertions(+) create mode 100644 actions-expressions/.prettierignore create mode 100644 actions-languageserver/.prettierignore create mode 100644 actions-workflow-parser/.prettierignore create mode 100644 browser-playground/.prettierignore diff --git a/actions-expressions/.prettierignore b/actions-expressions/.prettierignore new file mode 100644 index 0000000..ce98533 --- /dev/null +++ b/actions-expressions/.prettierignore @@ -0,0 +1,5 @@ +node_modules +dist +*.md +*.js +*.json diff --git a/actions-languageserver/.prettierignore b/actions-languageserver/.prettierignore new file mode 100644 index 0000000..ce98533 --- /dev/null +++ b/actions-languageserver/.prettierignore @@ -0,0 +1,5 @@ +node_modules +dist +*.md +*.js +*.json diff --git a/actions-workflow-parser/.prettierignore b/actions-workflow-parser/.prettierignore new file mode 100644 index 0000000..ce98533 --- /dev/null +++ b/actions-workflow-parser/.prettierignore @@ -0,0 +1,5 @@ +node_modules +dist +*.md +*.js +*.json diff --git a/browser-playground/.prettierignore b/browser-playground/.prettierignore new file mode 100644 index 0000000..ce98533 --- /dev/null +++ b/browser-playground/.prettierignore @@ -0,0 +1,5 @@ +node_modules +dist +*.md +*.js +*.json From 574216178b791ab3fd9fed62367ccd88b815d49e Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Fri, 3 Feb 2023 17:24:31 -0500 Subject: [PATCH 3/8] Fix `Job` references in language server --- .../src/context-providers/secrets.ts | 13 ++++++++----- .../src/context-providers/steps.ts | 7 ++++--- .../src/context-providers/variables.ts | 13 ++++++++----- .../src/test-utils/workflow-context.ts | 5 +++-- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/actions-languageserver/src/context-providers/secrets.ts b/actions-languageserver/src/context-providers/secrets.ts index 61c6ca0..85ad93d 100644 --- a/actions-languageserver/src/context-providers/secrets.ts +++ b/actions-languageserver/src/context-providers/secrets.ts @@ -2,6 +2,7 @@ import {data, DescriptionDictionary} from "@github/actions-expressions"; import {StringData} from "@github/actions-expressions/data/string"; import {WorkflowContext} from "@github/actions-languageservice/context/workflow-context"; import {isMapping, isString} from "@github/actions-workflow-parser"; +import {isJob} from "@github/actions-workflow-parser/model/type-guards"; import {Octokit} from "@octokit/rest"; import {RepositoryContext} from "../initializationOptions"; import {TTLCache} from "../utils/cache"; @@ -14,11 +15,13 @@ export async function getSecrets( defaultContext: DescriptionDictionary | undefined ): Promise { let environmentName: string | undefined; - if (workflowContext?.job?.environment) { - if (isString(workflowContext.job.environment)) { - environmentName = workflowContext.job.environment.value; - } else if (isMapping(workflowContext.job.environment)) { - for (const x of workflowContext.job.environment) { + const job = workflowContext?.job; + const environment = job && isJob(job) && job.environment; + if (environment) { + if (isString(environment)) { + environmentName = environment.value; + } else if (isMapping(environment)) { + for (const x of environment) { if (isString(x.key) && x.key.value === "name") { if (isString(x.value)) { environmentName = x.value.value; diff --git a/actions-languageserver/src/context-providers/steps.ts b/actions-languageserver/src/context-providers/steps.ts index eea315b..a3bed5e 100644 --- a/actions-languageserver/src/context-providers/steps.ts +++ b/actions-languageserver/src/context-providers/steps.ts @@ -1,7 +1,7 @@ import {data, DescriptionDictionary, isDescriptionDictionary} from "@github/actions-expressions"; import {parseActionReference} from "@github/actions-languageservice/action"; import {WorkflowContext} from "@github/actions-languageservice/context/workflow-context"; -import {isActionStep} from "@github/actions-workflow-parser/model/type-guards"; +import {isActionStep, isReusableWorkflowJob} from "@github/actions-workflow-parser/model/type-guards"; import {Octokit} from "@octokit/rest"; import {TTLCache} from "../utils/cache"; import {getActionOutputs} from "./action-outputs"; @@ -12,7 +12,8 @@ export async function getStepsContext( defaultContext: DescriptionDictionary | undefined, workflowContext: WorkflowContext ): Promise { - if (!defaultContext || !workflowContext.job) { + const job = workflowContext.job; + if (!defaultContext || !job || isReusableWorkflowJob(job)) { return defaultContext; } @@ -26,7 +27,7 @@ export async function getStepsContext( // Copy the default context for each step // If the step is an action, add the action outputs to the context const stepsContext = new DescriptionDictionary(); - for (const step of workflowContext.job.steps) { + for (const step of job.steps) { if (!contextSteps.has(step.id)) { continue; } diff --git a/actions-languageserver/src/context-providers/variables.ts b/actions-languageserver/src/context-providers/variables.ts index 52babbd..d14b20b 100644 --- a/actions-languageserver/src/context-providers/variables.ts +++ b/actions-languageserver/src/context-providers/variables.ts @@ -6,6 +6,7 @@ import {Pair} from "@github/actions-expressions/data/expressiondata"; import {RepositoryContext} from "../initializationOptions"; import {StringData} from "@github/actions-expressions/data/index"; import {TTLCache} from "../utils/cache"; +import {isJob} from "@github/actions-workflow-parser/model/type-guards"; export async function getVariables( workflowContext: WorkflowContext, @@ -15,11 +16,13 @@ export async function getVariables( defaultContext: DescriptionDictionary | undefined ): Promise { let environmentName: string | undefined; - if (workflowContext?.job?.environment) { - if (isString(workflowContext.job.environment)) { - environmentName = workflowContext.job.environment.value; - } else if (isMapping(workflowContext.job.environment)) { - for (const x of workflowContext.job.environment) { + const job = workflowContext?.job; + const environment = job && isJob(job) && job.environment; + if (environment) { + if (isString(environment)) { + environmentName = environment.value; + } else if (isMapping(environment)) { + for (const x of environment) { if (isString(x.key) && x.key.value === "name") { if (isString(x.value)) { environmentName = x.value.value; diff --git a/actions-languageserver/src/test-utils/workflow-context.ts b/actions-languageserver/src/test-utils/workflow-context.ts index aef5cfe..44b3654 100644 --- a/actions-languageserver/src/test-utils/workflow-context.ts +++ b/actions-languageserver/src/test-utils/workflow-context.ts @@ -1,5 +1,6 @@ import {WorkflowContext} from "@github/actions-languageservice/context/workflow-context"; import {convertWorkflowTemplate, parseWorkflow, TraceWriter} from "@github/actions-workflow-parser"; +import {isJob} from "@github/actions-workflow-parser/model/type-guards"; const nullTrace: TraceWriter = { info: x => {}, @@ -19,8 +20,8 @@ export function createWorkflowContext(workflow: string, job?: string, stepIndex? context.job = template.jobs.find(j => j.id.value === job); } - if (stepIndex !== undefined) { - context.step = context.job?.steps[stepIndex]; + if (stepIndex !== undefined && context.job && isJob(context.job)) { + context.step = context.job.steps[stepIndex]; } return context; From 04475e413f85e745aa6a5477f28aa1fa14630d39 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Fri, 3 Feb 2023 17:27:15 -0500 Subject: [PATCH 4/8] Explicitly define `type` in each job --- actions-workflow-parser/src/model/workflow-template.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/actions-workflow-parser/src/model/workflow-template.ts b/actions-workflow-parser/src/model/workflow-template.ts index c50a20e..508b424 100644 --- a/actions-workflow-parser/src/model/workflow-template.ts +++ b/actions-workflow-parser/src/model/workflow-template.ts @@ -44,6 +44,7 @@ export type BaseJob = { // `job-factory` in the schema export type Job = BaseJob & { + type: "job"; env?: MappingToken; environment?: TemplateToken; "runs-on"?: TemplateToken; @@ -55,6 +56,7 @@ export type Job = BaseJob & { // `workflow-job` in the schema export type ReusableWorkflowJob = BaseJob & { + type: "reusableWorkflowJob"; uses: StringToken; }; From 630a9d124e5556aa2eff12a444af0e1e8e93a839 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Fri, 3 Feb 2023 17:28:48 -0500 Subject: [PATCH 5/8] Use `WorkflowJob` in type guards --- actions-workflow-parser/src/model/type-guards.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/actions-workflow-parser/src/model/type-guards.ts b/actions-workflow-parser/src/model/type-guards.ts index bd201c1..60923f2 100644 --- a/actions-workflow-parser/src/model/type-guards.ts +++ b/actions-workflow-parser/src/model/type-guards.ts @@ -1,4 +1,4 @@ -import {ActionStep, Job, ReusableWorkflowJob, RunStep, Step} from "./workflow-template"; +import {ActionStep, Job, ReusableWorkflowJob, RunStep, Step, WorkflowJob} from "./workflow-template"; export function isRunStep(step: Step): step is RunStep { return (step as RunStep).run !== undefined; @@ -8,10 +8,10 @@ export function isActionStep(step: Step): step is ActionStep { return (step as ActionStep).uses !== undefined; } -export function isJob(job: Job | ReusableWorkflowJob): job is Job { +export function isJob(job: WorkflowJob): job is Job { return job.type === "job"; } -export function isReusableWorkflowJob(job: Job | ReusableWorkflowJob): job is ReusableWorkflowJob { +export function isReusableWorkflowJob(job: WorkflowJob): job is ReusableWorkflowJob { return job.type === "reusableWorkflowJob"; } From 3d5b006138d02c7ce7bb3e4c3898e90bb361141c Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Fri, 3 Feb 2023 17:39:49 -0500 Subject: [PATCH 6/8] Fix reusable workflow job type --- actions-workflow-parser/src/model/converter/reusable-job.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions-workflow-parser/src/model/converter/reusable-job.ts b/actions-workflow-parser/src/model/converter/reusable-job.ts index 6c75ff3..3eef8f3 100644 --- a/actions-workflow-parser/src/model/converter/reusable-job.ts +++ b/actions-workflow-parser/src/model/converter/reusable-job.ts @@ -11,7 +11,7 @@ export function convertResuableWorkflowJob( const base = convertBaseJob(context, jobKey, token); const result: ReusableWorkflowJob = { - type: "job", + type: "reusableWorkflowJob", id: base.id, name: base.name, needs: base.needs, From 986ce1bfe45a27a2f9e622a6d2bb7052fa5c576d Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Fri, 3 Feb 2023 17:52:48 -0500 Subject: [PATCH 7/8] Separate reusable jobs in the workflow context --- .../src/context-providers/secrets.ts | 13 +++++-------- .../src/context-providers/steps.ts | 7 +++---- .../src/context-providers/variables.ts | 13 +++++-------- .../src/test-utils/workflow-context.ts | 5 ++--- .../src/context-providers/env.ts | 6 ++---- .../src/context-providers/job.ts | 3 +-- .../src/context-providers/needs.ts | 5 +++-- .../src/context-providers/steps.ts | 3 +-- .../src/context/workflow-context.ts | 19 +++++++++++++++---- 9 files changed, 37 insertions(+), 37 deletions(-) diff --git a/actions-languageserver/src/context-providers/secrets.ts b/actions-languageserver/src/context-providers/secrets.ts index 85ad93d..61c6ca0 100644 --- a/actions-languageserver/src/context-providers/secrets.ts +++ b/actions-languageserver/src/context-providers/secrets.ts @@ -2,7 +2,6 @@ import {data, DescriptionDictionary} from "@github/actions-expressions"; import {StringData} from "@github/actions-expressions/data/string"; import {WorkflowContext} from "@github/actions-languageservice/context/workflow-context"; import {isMapping, isString} from "@github/actions-workflow-parser"; -import {isJob} from "@github/actions-workflow-parser/model/type-guards"; import {Octokit} from "@octokit/rest"; import {RepositoryContext} from "../initializationOptions"; import {TTLCache} from "../utils/cache"; @@ -15,13 +14,11 @@ export async function getSecrets( defaultContext: DescriptionDictionary | undefined ): Promise { let environmentName: string | undefined; - const job = workflowContext?.job; - const environment = job && isJob(job) && job.environment; - if (environment) { - if (isString(environment)) { - environmentName = environment.value; - } else if (isMapping(environment)) { - for (const x of environment) { + if (workflowContext?.job?.environment) { + if (isString(workflowContext.job.environment)) { + environmentName = workflowContext.job.environment.value; + } else if (isMapping(workflowContext.job.environment)) { + for (const x of workflowContext.job.environment) { if (isString(x.key) && x.key.value === "name") { if (isString(x.value)) { environmentName = x.value.value; diff --git a/actions-languageserver/src/context-providers/steps.ts b/actions-languageserver/src/context-providers/steps.ts index a3bed5e..eea315b 100644 --- a/actions-languageserver/src/context-providers/steps.ts +++ b/actions-languageserver/src/context-providers/steps.ts @@ -1,7 +1,7 @@ import {data, DescriptionDictionary, isDescriptionDictionary} from "@github/actions-expressions"; import {parseActionReference} from "@github/actions-languageservice/action"; import {WorkflowContext} from "@github/actions-languageservice/context/workflow-context"; -import {isActionStep, isReusableWorkflowJob} from "@github/actions-workflow-parser/model/type-guards"; +import {isActionStep} from "@github/actions-workflow-parser/model/type-guards"; import {Octokit} from "@octokit/rest"; import {TTLCache} from "../utils/cache"; import {getActionOutputs} from "./action-outputs"; @@ -12,8 +12,7 @@ export async function getStepsContext( defaultContext: DescriptionDictionary | undefined, workflowContext: WorkflowContext ): Promise { - const job = workflowContext.job; - if (!defaultContext || !job || isReusableWorkflowJob(job)) { + if (!defaultContext || !workflowContext.job) { return defaultContext; } @@ -27,7 +26,7 @@ export async function getStepsContext( // Copy the default context for each step // If the step is an action, add the action outputs to the context const stepsContext = new DescriptionDictionary(); - for (const step of job.steps) { + for (const step of workflowContext.job.steps) { if (!contextSteps.has(step.id)) { continue; } diff --git a/actions-languageserver/src/context-providers/variables.ts b/actions-languageserver/src/context-providers/variables.ts index d14b20b..52babbd 100644 --- a/actions-languageserver/src/context-providers/variables.ts +++ b/actions-languageserver/src/context-providers/variables.ts @@ -6,7 +6,6 @@ import {Pair} from "@github/actions-expressions/data/expressiondata"; import {RepositoryContext} from "../initializationOptions"; import {StringData} from "@github/actions-expressions/data/index"; import {TTLCache} from "../utils/cache"; -import {isJob} from "@github/actions-workflow-parser/model/type-guards"; export async function getVariables( workflowContext: WorkflowContext, @@ -16,13 +15,11 @@ export async function getVariables( defaultContext: DescriptionDictionary | undefined ): Promise { let environmentName: string | undefined; - const job = workflowContext?.job; - const environment = job && isJob(job) && job.environment; - if (environment) { - if (isString(environment)) { - environmentName = environment.value; - } else if (isMapping(environment)) { - for (const x of environment) { + if (workflowContext?.job?.environment) { + if (isString(workflowContext.job.environment)) { + environmentName = workflowContext.job.environment.value; + } else if (isMapping(workflowContext.job.environment)) { + for (const x of workflowContext.job.environment) { if (isString(x.key) && x.key.value === "name") { if (isString(x.value)) { environmentName = x.value.value; diff --git a/actions-languageserver/src/test-utils/workflow-context.ts b/actions-languageserver/src/test-utils/workflow-context.ts index 44b3654..aef5cfe 100644 --- a/actions-languageserver/src/test-utils/workflow-context.ts +++ b/actions-languageserver/src/test-utils/workflow-context.ts @@ -1,6 +1,5 @@ import {WorkflowContext} from "@github/actions-languageservice/context/workflow-context"; import {convertWorkflowTemplate, parseWorkflow, TraceWriter} from "@github/actions-workflow-parser"; -import {isJob} from "@github/actions-workflow-parser/model/type-guards"; const nullTrace: TraceWriter = { info: x => {}, @@ -20,8 +19,8 @@ export function createWorkflowContext(workflow: string, job?: string, stepIndex? context.job = template.jobs.find(j => j.id.value === job); } - if (stepIndex !== undefined && context.job && isJob(context.job)) { - context.step = context.job.steps[stepIndex]; + if (stepIndex !== undefined) { + context.step = context.job?.steps[stepIndex]; } return context; diff --git a/actions-languageservice/src/context-providers/env.ts b/actions-languageservice/src/context-providers/env.ts index 8aaeaf4..5aa22cb 100644 --- a/actions-languageservice/src/context-providers/env.ts +++ b/actions-languageservice/src/context-providers/env.ts @@ -1,6 +1,5 @@ import {data, DescriptionDictionary} from "@github/actions-expressions"; import {isScalar, isString} from "@github/actions-workflow-parser"; -import {isJob} from "@github/actions-workflow-parser/model/type-guards"; import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token"; import {WorkflowContext} from "../context/workflow-context"; @@ -13,9 +12,8 @@ export function getEnvContext(workflowContext: WorkflowContext): DescriptionDict } //job env - const job = workflowContext.job; - if (job && isJob(job) && job.env) { - envContext(job.env, d); + if (workflowContext.job && workflowContext.job.env) { + envContext(workflowContext.job.env, d); } //workflow env diff --git a/actions-languageservice/src/context-providers/job.ts b/actions-languageservice/src/context-providers/job.ts index 5adf7e4..6c37ab6 100644 --- a/actions-languageservice/src/context-providers/job.ts +++ b/actions-languageservice/src/context-providers/job.ts @@ -1,6 +1,5 @@ import {data, DescriptionDictionary} from "@github/actions-expressions"; import {isMapping, isSequence} from "@github/actions-workflow-parser"; -import {isReusableWorkflowJob} from "@github/actions-workflow-parser/model/type-guards"; import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token"; import {WorkflowContext} from "../context/workflow-context"; @@ -8,7 +7,7 @@ export function getJobContext(workflowContext: WorkflowContext): DescriptionDict // https://docs.github.com/en/actions/learn-github-actions/contexts#job-context const jobContext = new DescriptionDictionary(); const job = workflowContext.job; - if (!job || isReusableWorkflowJob(job)) { + if (!job) { return jobContext; } diff --git a/actions-languageservice/src/context-providers/needs.ts b/actions-languageservice/src/context-providers/needs.ts index 455bfd5..e261b0d 100644 --- a/actions-languageservice/src/context-providers/needs.ts +++ b/actions-languageservice/src/context-providers/needs.ts @@ -25,14 +25,15 @@ function needsJobContext(job?: WorkflowJob): DescriptionDictionary { if (job && isJob(job)) { d.add("outputs", jobOutputs(job)); } + // Can be "success", "failure", "cancelled", or "skipped" d.add("result", new data.Null()); return d; } -function jobOutputs(job: Job): DescriptionDictionary { +function jobOutputs(job?: Job): DescriptionDictionary { const d = new DescriptionDictionary(); - if (!job.outputs) { + if (!job?.outputs) { return d; } diff --git a/actions-languageservice/src/context-providers/steps.ts b/actions-languageservice/src/context-providers/steps.ts index fb2d76d..b07b18e 100644 --- a/actions-languageservice/src/context-providers/steps.ts +++ b/actions-languageservice/src/context-providers/steps.ts @@ -1,12 +1,11 @@ import {data, DescriptionDictionary} from "@github/actions-expressions"; -import {isReusableWorkflowJob} from "@github/actions-workflow-parser/model/type-guards"; import {Step} from "@github/actions-workflow-parser/model/workflow-template"; import {WorkflowContext} from "../context/workflow-context"; import {getDescription} from "./descriptions"; export function getStepsContext(workflowContext: WorkflowContext): DescriptionDictionary { const d = new DescriptionDictionary(); - if (!workflowContext.job || isReusableWorkflowJob(workflowContext.job)) { + if (!workflowContext.job?.steps) { return d; } diff --git a/actions-languageservice/src/context/workflow-context.ts b/actions-languageservice/src/context/workflow-context.ts index 2b77a24..c4c20af 100644 --- a/actions-languageservice/src/context/workflow-context.ts +++ b/actions-languageservice/src/context/workflow-context.ts @@ -1,6 +1,6 @@ import {isMapping, isSequence, WorkflowTemplate} from "@github/actions-workflow-parser"; import {isJob, isReusableWorkflowJob} from "@github/actions-workflow-parser/model/type-guards"; -import {Step, WorkflowJob} from "@github/actions-workflow-parser/model/workflow-template"; +import {Step, Job, ReusableWorkflowJob} 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"; @@ -11,8 +11,11 @@ export interface WorkflowContext { template: WorkflowTemplate | undefined; - /** If the context is for a position within a job, this will be the job */ - job?: WorkflowJob; + /** If the context is for a position within a regular job, this will be the job */ + job?: Job; + + /** If the context is for a position within a reusable workflow job, this will be the reusable workflow job */ + reusableWorkflowJob?: ReusableWorkflowJob; /** If the context is for a position within a step, this will be the step */ step?: Step; @@ -36,7 +39,15 @@ export function getWorkflowContext( switch (token.definition?.key) { case "job": { const jobID = (token as StringToken).value; - context.job = template.jobs.find(job => job.id.value === jobID); + const job = template.jobs.find(job => job.id.value === jobID); + if (!job) { + break; + } + if (isJob(job)) { + context.job = job; + } else if (isReusableWorkflowJob(job)) { + context.reusableWorkflowJob = job; + } break; } case "steps": { From 2e953b26ea17a7e692b511fd6af064e27ded3d3f Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Fri, 3 Feb 2023 17:59:14 -0500 Subject: [PATCH 8/8] Support reusable workflow jobs in the test context --- .../src/test-utils/workflow-context.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/actions-languageserver/src/test-utils/workflow-context.ts b/actions-languageserver/src/test-utils/workflow-context.ts index aef5cfe..88eb740 100644 --- a/actions-languageserver/src/test-utils/workflow-context.ts +++ b/actions-languageserver/src/test-utils/workflow-context.ts @@ -1,5 +1,6 @@ import {WorkflowContext} from "@github/actions-languageservice/context/workflow-context"; import {convertWorkflowTemplate, parseWorkflow, TraceWriter} from "@github/actions-workflow-parser"; +import {isJob} from "@github/actions-workflow-parser/model/type-guards"; const nullTrace: TraceWriter = { info: x => {}, @@ -16,7 +17,14 @@ export function createWorkflowContext(workflow: string, job?: string, stepIndex? const context: WorkflowContext = {uri: "test.yaml", template}; if (job) { - context.job = template.jobs.find(j => j.id.value === job); + const workflowJob = template.jobs.find(j => j.id.value === job); + if (workflowJob) { + if (isJob(workflowJob)) { + context.job = workflowJob; + } else { + context.reusableWorkflowJob = workflowJob; + } + } } if (stepIndex !== undefined) {