diff --git a/actions-languageserver/package.json b/actions-languageserver/package.json index 5ba7a02..b475153 100644 --- a/actions-languageserver/package.json +++ b/actions-languageserver/package.json @@ -1,6 +1,6 @@ { "name": "@github/actions-languageserver", - "version": "0.1.61", + "version": "0.1.65", "description": "Language server for GitHub Actions", "license": "MIT", "type": "module", @@ -38,7 +38,7 @@ "prettier-fix": "prettier --write ." }, "dependencies": { - "@github/actions-languageservice": "^0.1.61", + "@github/actions-languageservice": "^0.1.65", "@github/actions-workflow-parser": "*", "@octokit/rest": "^19.0.5", "vscode-languageserver": "^8.0.2", @@ -54,6 +54,7 @@ "devDependencies": { "@types/jest": "^29.0.3", "jest": "^29.0.3", + "nock": "^13.2.9", "prettier": "^2.7.1", "rimraf": "^3.0.2", "ts-jest": "^29.0.3", diff --git a/actions-languageserver/src/context-providers.ts b/actions-languageserver/src/context-providers.ts index 20301c7..edc8cfa 100644 --- a/actions-languageserver/src/context-providers.ts +++ b/actions-languageserver/src/context-providers.ts @@ -1,7 +1,9 @@ import {data} from "@github/actions-expressions"; import {ContextProviderConfig} from "@github/actions-languageservice/."; +import {WorkflowContext} from "@github/actions-languageservice/context/workflow-context"; import {Octokit} from "@octokit/rest"; import {getSecrets} from "./context-providers/secrets"; +import {getStepsContext} from "./context-providers/steps"; import {RepositoryContext} from "./initializationOptions"; import {TTLCache} from "./utils/cache"; @@ -18,14 +20,22 @@ export function contextProviders( auth: sessionToken }); - const getContext = async (name: string, defaultContext: data.Dictionary | undefined) => { + const getContext = async ( + name: string, + defaultContext: data.Dictionary | undefined, + workflowContext: WorkflowContext + ) => { switch (name) { - case "secrets": + case "secrets": { const secrets = await getSecrets(octokit, cache, repo.owner, repo.name); defaultContext = defaultContext || new data.Dictionary(); secrets.forEach(secret => defaultContext!.add(secret.value, new data.StringData("***"))); return defaultContext; + } + case "steps": { + return await getStepsContext(octokit, cache, defaultContext, workflowContext); + } } }; diff --git a/actions-languageserver/src/context-providers/action-outputs.ts b/actions-languageserver/src/context-providers/action-outputs.ts new file mode 100644 index 0000000..de5e919 --- /dev/null +++ b/actions-languageserver/src/context-providers/action-outputs.ts @@ -0,0 +1,12 @@ +import {ActionReference, ActionOutputs} from "@github/actions-languageservice/action"; +import {Octokit} from "@octokit/rest"; +import {fetchActionMetadata} from "../utils/action-metadata"; +import {TTLCache} from "../utils/cache"; + +export async function getActionOutputs( + octokit: Octokit, + cache: TTLCache, + action: ActionReference +): Promise { + return (await fetchActionMetadata(octokit, cache, action))?.outputs; +} diff --git a/actions-languageserver/src/context-providers/steps.test.ts b/actions-languageserver/src/context-providers/steps.test.ts new file mode 100644 index 0000000..9c883d9 --- /dev/null +++ b/actions-languageserver/src/context-providers/steps.test.ts @@ -0,0 +1,98 @@ +import {data} from "@github/actions-expressions"; +import {getStepsContext as getDefaultStepsContext} from "@github/actions-languageservice/context-providers/steps"; +import {Octokit} from "@octokit/rest"; +import nock from "nock"; +import {createWorkflowContext} from "../test-utils/workflow-context"; +import {TTLCache} from "../utils/cache"; +import {getStepsContext} from "./steps"; + +const workflow = ` +name: Caching Primes + +on: push + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Cache Primes + id: cache-primes + uses: actions/cache@v3 + with: + path: prime-numbers + key: \${{ runner.os }}-primes + + - name: Generate Prime Numbers + if: steps.cache-primes.outputs.cache-hit != 'true' + run: /generate-primes.sh -d prime-numbers + + - name: Use Prime Numbers + run: /primes.sh -d prime-numbers +`; + +// https://api.github.com/repos/actions/cache/contents/action.yml?ref=v3 +const actionMetadata = { + name: "action.yml", + path: "action.yml", + sha: "3e158e3ee33f7e597ccf45c369a6e830dc8d43e3", + size: 946, + url: "https://api.github.com/repos/actions/cache/contents/action.yml?ref=v3", + html_url: "https://github.com/actions/cache/blob/v3/action.yml", + git_url: "https://api.github.com/repos/actions/cache/git/blobs/3e158e3ee33f7e597ccf45c369a6e830dc8d43e3", + download_url: "https://raw.githubusercontent.com/actions/cache/v3/action.yml", + type: "file", + content: + "bmFtZTogJ0NhY2hlJwpkZXNjcmlwdGlvbjogJ0NhY2hlIGFydGlmYWN0cyBs\naWtlIGRlcGVuZGVuY2llcyBhbmQgYnVpbGQgb3V0cHV0cyB0byBpbXByb3Zl\nIHdvcmtmbG93IGV4ZWN1dGlvbiB0aW1lJwphdXRob3I6ICdHaXRIdWInCmlu\ncHV0czoKICBwYXRoOgogICAgZGVzY3JpcHRpb246ICdBIGxpc3Qgb2YgZmls\nZXMsIGRpcmVjdG9yaWVzLCBhbmQgd2lsZGNhcmQgcGF0dGVybnMgdG8gY2Fj\naGUgYW5kIHJlc3RvcmUnCiAgICByZXF1aXJlZDogdHJ1ZQogIGtleToKICAg\nIGRlc2NyaXB0aW9uOiAnQW4gZXhwbGljaXQga2V5IGZvciByZXN0b3Jpbmcg\nYW5kIHNhdmluZyB0aGUgY2FjaGUnCiAgICByZXF1aXJlZDogdHJ1ZQogIHJl\nc3RvcmUta2V5czoKICAgIGRlc2NyaXB0aW9uOiAnQW4gb3JkZXJlZCBsaXN0\nIG9mIGtleXMgdG8gdXNlIGZvciByZXN0b3Jpbmcgc3RhbGUgY2FjaGUgaWYg\nbm8gY2FjaGUgaGl0IG9jY3VycmVkIGZvciBrZXkuIE5vdGUgYGNhY2hlLWhp\ndGAgcmV0dXJucyBmYWxzZSBpbiB0aGlzIGNhc2UuJwogICAgcmVxdWlyZWQ6\nIGZhbHNlCiAgdXBsb2FkLWNodW5rLXNpemU6CiAgICBkZXNjcmlwdGlvbjog\nJ1RoZSBjaHVuayBzaXplIHVzZWQgdG8gc3BsaXQgdXAgbGFyZ2UgZmlsZXMg\nZHVyaW5nIHVwbG9hZCwgaW4gYnl0ZXMnCiAgICByZXF1aXJlZDogZmFsc2UK\nb3V0cHV0czoKICBjYWNoZS1oaXQ6CiAgICBkZXNjcmlwdGlvbjogJ0EgYm9v\nbGVhbiB2YWx1ZSB0byBpbmRpY2F0ZSBhbiBleGFjdCBtYXRjaCB3YXMgZm91\nbmQgZm9yIHRoZSBwcmltYXJ5IGtleScKcnVuczoKICB1c2luZzogJ25vZGUx\nNicKICBtYWluOiAnZGlzdC9yZXN0b3JlL2luZGV4LmpzJwogIHBvc3Q6ICdk\naXN0L3NhdmUvaW5kZXguanMnCiAgcG9zdC1pZjogJ3N1Y2Nlc3MoKScKYnJh\nbmRpbmc6CiAgaWNvbjogJ2FyY2hpdmUnCiAgY29sb3I6ICdncmF5LWRhcmsn\nCg==\n", + encoding: "base64", + _links: { + self: "https://api.github.com/repos/actions/cache/contents/action.yml?ref=v3", + git: "https://api.github.com/repos/actions/cache/git/blobs/3e158e3ee33f7e597ccf45c369a6e830dc8d43e3", + html: "https://github.com/actions/cache/blob/v3/action.yml" + } +}; + +beforeEach(() => { + nock.disableNetConnect(); +}); + +afterEach(() => { + nock.cleanAll(); +}); + +it("returns default context when job is undefined", async () => { + const workflowContext = createWorkflowContext(workflow, undefined); + const defaultContext = getDefaultStepsContext(workflowContext); + + const stepsContext = await getStepsContext(new Octokit(), new TTLCache(), defaultContext, workflowContext); + expect(stepsContext).toEqual(defaultContext); +}); + +it("adds action outputs", async () => { + nock("https://api.github.com").get("/repos/actions/cache/contents/action.yml?ref=v3").reply(200, actionMetadata); + + const workflowContext = createWorkflowContext(workflow, "build"); + const defaultContext = getDefaultStepsContext(workflowContext); + + const stepsContext = await getStepsContext(new Octokit(), new TTLCache(), defaultContext, workflowContext); + expect(stepsContext).toBeDefined(); + + expect(stepsContext).toEqual( + new data.Dictionary({ + key: "cache-primes", + value: new data.Dictionary( + { + key: "outputs", + value: new data.Dictionary({ + key: "cache-hit", + value: new data.StringData("A boolean value to indicate an exact match was found for the primary key") + }) + }, + {key: "conclusion", value: new data.Null()}, + {key: "outcome", value: new data.Null()} + ) + }) + ); +}); diff --git a/actions-languageserver/src/context-providers/steps.ts b/actions-languageserver/src/context-providers/steps.ts new file mode 100644 index 0000000..85cba47 --- /dev/null +++ b/actions-languageserver/src/context-providers/steps.ts @@ -0,0 +1,75 @@ +import {data} from "@github/actions-expressions"; +import {isDictionary} from "@github/actions-expressions/data/dictionary"; +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 {Octokit} from "@octokit/rest"; +import {TTLCache} from "../utils/cache"; +import {getActionOutputs} from "./action-outputs"; + +export async function getStepsContext( + octokit: Octokit, + cache: TTLCache, + defaultContext: data.Dictionary | undefined, + workflowContext: WorkflowContext +): Promise { + if (!defaultContext || !workflowContext.job) { + return defaultContext; + } + + // The default context includes the set of valid + // step ids that can be used in expressions + const contextSteps = new Set(); + for (const {key} of defaultContext.pairs()) { + contextSteps.add(key); + } + + // Copy the default context for each step + // If the step is an action, add the action outputs to the context + const stepsContext = new data.Dictionary(); + for (const step of workflowContext.job.steps) { + if (!contextSteps.has(step.id)) { + continue; + } + + const defaultStepContext = defaultContext.get(step.id); + if (!defaultStepContext) { + stepsContext.add(step.id, new data.Null()); + continue; + } + + if (!isActionStep(step) || !isDictionary(defaultStepContext)) { + stepsContext.add(step.id, defaultStepContext); + continue; + } + + const action = parseActionReference(step.uses.value); + if (!action) { + stepsContext.add(step.id, defaultStepContext); + continue; + } + + const stepContext = new data.Dictionary(); + for (const {key, value} of defaultStepContext.pairs()) { + switch (key) { + case "outputs": + const outputs = await getActionOutputs(octokit, cache, action); + if (!outputs) { + stepContext.add(key, value); + continue; + } + const outputsDict = new data.Dictionary(); + for (const [key, value] of Object.entries(outputs)) { + outputsDict.add(key, new data.StringData(value.description)); + } + stepContext.add("outputs", outputsDict); + break; + default: + stepContext.add(key, value); + } + } + stepsContext.add(step.id, stepContext); + } + + return stepsContext; +} diff --git a/actions-languageserver/src/test-utils/workflow-context.ts b/actions-languageserver/src/test-utils/workflow-context.ts new file mode 100644 index 0000000..63841a6 --- /dev/null +++ b/actions-languageserver/src/test-utils/workflow-context.ts @@ -0,0 +1,27 @@ +import {WorkflowContext} from "@github/actions-languageservice/context/workflow-context"; +import {convertWorkflowTemplate, parseWorkflow, TraceWriter} from "@github/actions-workflow-parser"; + +const nullTrace: TraceWriter = { + info: x => {}, + verbose: x => {}, + error: x => {} +}; + +export function createWorkflowContext(workflow: string, job?: string, stepIndex?: number): WorkflowContext { + const parsed = parseWorkflow("test.yaml", [{name: "test.yaml", content: workflow}], nullTrace); + if (!parsed.value) { + throw new Error("Failed to parse workflow"); + } + const template = convertWorkflowTemplate(parsed.context, parsed.value); + const context: WorkflowContext = {uri: "test.yaml", template}; + + if (job) { + context.job = template.jobs.find(j => j.id.value === job); + } + + if (stepIndex) { + context.step = context.job?.steps[stepIndex]; + } + + return context; +} diff --git a/actions-languageserver/src/utils/action-metadata.ts b/actions-languageserver/src/utils/action-metadata.ts new file mode 100644 index 0000000..708911f --- /dev/null +++ b/actions-languageserver/src/utils/action-metadata.ts @@ -0,0 +1,68 @@ +import {ActionReference, ActionInputs, ActionOutputs, actionIdentifier} from "@github/actions-languageservice/action"; +import {Octokit, RestEndpointMethodTypes} from "@octokit/rest"; +import {parse} from "yaml"; +import {TTLCache} from "./cache"; + +export type ActionMetadata = { + inputs?: ActionInputs; + outputs?: ActionOutputs; +}; + +export async function fetchActionMetadata( + client: Octokit, + cache: TTLCache, + action: ActionReference +): Promise { + const metadata = await cache.get(`${actionIdentifier(action)}/action-metadata`, undefined, () => + getActionMetadata(client, action) + ); + if (!metadata) { + return undefined; + } + + return parseActionMetadata(metadata); +} + +async function getActionMetadata(client: Octokit, action: ActionReference): Promise { + let resp: RestEndpointMethodTypes["repos"]["getContent"]["response"]; + try { + resp = await client.repos.getContent({ + owner: action.owner, + repo: action.name, + ref: action.ref, + path: action.path ? `${action.path}/action.yml` : "action.yml" + }); + } catch (e: any) { + // If action.yml doesn't exist, try action.yaml + if (e.status === 404) { + resp = await client.repos.getContent({ + owner: action.owner, + repo: action.name, + ref: action.ref, + path: action.path ? `${action.path}/action.yaml` : "action.yaml" + }); + } else { + throw e; + } + } + + // https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28 + // Ignore directories (array of files) and non-file content + if (resp.data === undefined || Array.isArray(resp.data) || resp.data.type !== "file") { + return undefined; + } + + if (resp.data.content === undefined) { + return undefined; + } + + const text = Buffer.from(resp.data.content, "base64").toString("utf8"); + // Remove any null bytes + return text.replace(/\0/g, ""); +} + +// https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions +async function parseActionMetadata(content: string): Promise { + const metadata: ActionMetadata = parse(content); + return metadata; +} diff --git a/actions-languageserver/src/value-providers/action-inputs.ts b/actions-languageserver/src/value-providers/action-inputs.ts index 63d3e94..b9383f0 100644 --- a/actions-languageserver/src/value-providers/action-inputs.ts +++ b/actions-languageserver/src/value-providers/action-inputs.ts @@ -1,14 +1,9 @@ -import { - actionIdentifier, - ActionInputs, - ActionReference, - parseActionReference -} from "@github/actions-languageservice/action"; +import {ActionInputs, ActionReference, parseActionReference} from "@github/actions-languageservice/action"; import {WorkflowContext} from "@github/actions-languageservice/context/workflow-context"; import {Value} from "@github/actions-languageservice/value-providers/config"; import {isActionStep} from "@github/actions-workflow-parser/model/type-guards"; -import {Octokit, RestEndpointMethodTypes} from "@octokit/rest"; -import {parse} from "yaml"; +import {Octokit} from "@octokit/rest"; +import {fetchActionMetadata} from "../utils/action-metadata"; import {TTLCache} from "../utils/cache"; export async function getActionInputs( @@ -16,11 +11,7 @@ export async function getActionInputs( cache: TTLCache, action: ActionReference ): Promise { - const inputs = await cache.get(`${actionIdentifier(action)}/action-inputs`, undefined, () => - fetchActionInputs(client, action) - ); - - return inputs; + return (await fetchActionMetadata(client, cache, action))?.inputs; } export async function getActionInputValues( @@ -49,60 +40,3 @@ export async function getActionInputValues( }; }); } - -async function fetchActionInputs(client: Octokit, action: ActionReference): Promise { - const metadata = await getActionMetadata(client, action); - if (!metadata) { - return undefined; - } - - return parseActionMetadata(metadata); -} - -async function getActionMetadata(client: Octokit, action: ActionReference): Promise { - let resp: RestEndpointMethodTypes["repos"]["getContent"]["response"]; - try { - resp = await client.repos.getContent({ - owner: action.owner, - repo: action.name, - ref: action.ref, - path: action.path ? `${action.path}/action.yml` : "action.yml" - }); - } catch (e: any) { - // If action.yml doesn't exist, try action.yaml - if (e.status === 404) { - resp = await client.repos.getContent({ - owner: action.owner, - repo: action.name, - ref: action.ref, - path: action.path ? `${action.path}/action.yaml` : "action.yaml" - }); - } else { - throw e; - } - } - - // https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28 - // Ignore directories (array of files) and non-file content - if (resp.data === undefined || Array.isArray(resp.data) || resp.data.type !== "file") { - return undefined; - } - - if (resp.data.content === undefined) { - return undefined; - } - - const text = Buffer.from(resp.data.content, "base64").toString("utf8"); - // Remove any null bytes - return text.replace(/\0/g, ""); -} - -type ActionMetadata = { - inputs?: ActionInputs; -}; - -// https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions -async function parseActionMetadata(content: string): Promise { - const metadata: ActionMetadata = parse(content); - return metadata.inputs ?? {}; -} diff --git a/actions-languageserver/tsconfig.build.json b/actions-languageserver/tsconfig.build.json index 88bdfa8..61c74a3 100644 --- a/actions-languageserver/tsconfig.build.json +++ b/actions-languageserver/tsconfig.build.json @@ -1,5 +1,5 @@ { - "exclude": ["./src/**/*.test.ts"], + "exclude": ["./src/**/*.test.ts", "./src/test-utils"], "extends": "./tsconfig.json", "compilerOptions": { "declaration": true, diff --git a/actions-languageservice/package.json b/actions-languageservice/package.json index 3f7638b..f5f56a3 100644 --- a/actions-languageservice/package.json +++ b/actions-languageservice/package.json @@ -1,6 +1,6 @@ { "name": "@github/actions-languageservice", - "version": "0.1.61", + "version": "0.1.65", "description": "Language service for GitHub Actions", "license": "MIT", "type": "module", diff --git a/actions-languageservice/src/action.ts b/actions-languageservice/src/action.ts index 2f92b5d..4c35918 100644 --- a/actions-languageservice/src/action.ts +++ b/actions-languageservice/src/action.ts @@ -8,6 +8,14 @@ export type ActionInput = { export type ActionInputs = Record; +// https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#outputs-for-docker-container-and-javascript-actions +export type ActionOutput = { + description: string; + value?: string; +}; + +export type ActionOutputs = Record; + export type ActionReference = { owner: string; name: string; diff --git a/actions-languageservice/src/complete.expressions.test.ts b/actions-languageservice/src/complete.expressions.test.ts index 78e1166..86973b1 100644 --- a/actions-languageservice/src/complete.expressions.test.ts +++ b/actions-languageservice/src/complete.expressions.test.ts @@ -311,6 +311,35 @@ jobs: expect(result.map(x => x.label)).toEqual(["build_id"]); }); + it("env steps context only for current step and job", async () => { + const input = ` +on: push +env: + envwf: workflow_env +jobs: + a: + runs-on: ubuntu-latest + env: + envjoba: job_a_env + b: + runs-on: ubuntu-latest + env: + envjobb: job_b_env + steps: + - name: step a + run: echo "hello" + env: + envstepa: step_a_env + - name: step b + run: echo "hello \${{ env.| + env: + envstepb: step_b_env +`; + const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig); + + expect(result.map(x => x.label)).toEqual(["envjobb", "envstepb", "envwf"]); + }); + it("inputs", async () => { const input = ` on: @@ -409,7 +438,7 @@ jobs: 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"); + expect(result.map(x => x.label)).not.toContain("schedule"); }); it("includes cron schedules", async () => { @@ -428,7 +457,7 @@ jobs: const result = await complete(...getPositionFromCursor(input), undefined, undefined); - expect(result.map(x => x.label)).toContain("cron"); + expect(result.map(x => x.label)).toContain("schedule"); }); }); diff --git a/actions-languageservice/src/context-providers/config.ts b/actions-languageservice/src/context-providers/config.ts index 6b3b49f..497b94b 100644 --- a/actions-languageservice/src/context-providers/config.ts +++ b/actions-languageservice/src/context-providers/config.ts @@ -1,9 +1,14 @@ import {data} from "@github/actions-expressions"; import {Dictionary} from "@github/actions-expressions/data/dictionary"; import {ExpressionData, Pair} from "@github/actions-expressions/data/expressiondata"; +import {WorkflowContext} from "../context/workflow-context"; export type ContextProviderConfig = { - getContext: (name: string, defaultContext: data.Dictionary | undefined) => Promise; + getContext: ( + name: string, + defaultContext: data.Dictionary | undefined, + workflowContext: WorkflowContext + ) => Promise; }; /** diff --git a/actions-languageservice/src/context-providers/default.ts b/actions-languageservice/src/context-providers/default.ts index 4e8e62d..f554bb8 100644 --- a/actions-languageservice/src/context-providers/default.ts +++ b/actions-languageservice/src/context-providers/default.ts @@ -2,6 +2,7 @@ import {data} from "@github/actions-expressions"; import {Kind} from "@github/actions-expressions/data/expressiondata"; import {WorkflowContext} from "../context/workflow-context"; import {ContextProviderConfig} from "./config"; +import {getEnvContext} from "./env"; import {getGithubContext} from "./github"; import {getInputsContext} from "./inputs"; import {getJobContext} from "./job"; @@ -36,7 +37,7 @@ export async function getContext( continue; } - value = (await config?.getContext(contextName, value)) || value; + value = (await config?.getContext(contextName, value, workflowContext)) || value; context.add(contextName, value); } @@ -78,6 +79,9 @@ function getDefaultContext(name: string, workflowContext: WorkflowContext, mode: case "job": return getJobContext(workflowContext); + + case "env": + return getEnvContext(workflowContext); } return undefined; diff --git a/actions-languageservice/src/context-providers/env.ts b/actions-languageservice/src/context-providers/env.ts new file mode 100644 index 0000000..41eda1f --- /dev/null +++ b/actions-languageservice/src/context-providers/env.ts @@ -0,0 +1,37 @@ +import {data} from "@github/actions-expressions"; +import {isScalar, isString} from "@github/actions-workflow-parser"; +import {WorkflowContext} from "../context/workflow-context"; +import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token"; + +export function getEnvContext(workflowContext: WorkflowContext): data.Dictionary { + const d = new data.Dictionary(); + + //step env + if (workflowContext.step?.env) { + envContext(workflowContext.step.env, d); + } + + //job env + if (workflowContext.job && workflowContext.job.env) { + envContext(workflowContext.job.env, d); + } + + //workflow env + if (workflowContext.template && workflowContext.template.env) { + const wfEnv = workflowContext.template.env.assertMapping("workflow env"); + envContext(wfEnv, d); + } + + return d; +} + +function envContext(envMap: MappingToken, d: data.Dictionary) { + for (const env of envMap) { + if (!isString(env.key)) { + continue; + } + + const value = isScalar(env.value) ? new data.StringData(env.value.toDisplayString()) : new data.Null(); + d.add(env.key.value, value); + } +} diff --git a/actions-languageservice/src/context-providers/github.ts b/actions-languageservice/src/context-providers/github.ts index bd0917c..89fb100 100644 --- a/actions-languageservice/src/context-providers/github.ts +++ b/actions-languageservice/src/context-providers/github.ts @@ -70,7 +70,7 @@ function getEventContext(workflowContext: WorkflowContext): ExpressionData { 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)); + d.add("schedule", new data.StringData(default_cron)); } return d; diff --git a/actions-languageservice/src/utils/find-token.test.ts b/actions-languageservice/src/utils/find-token.test.ts index f79e561..c694311 100644 --- a/actions-languageservice/src/utils/find-token.test.ts +++ b/actions-languageservice/src/utils/find-token.test.ts @@ -139,7 +139,7 @@ describe("find-token", () => { testFindToken(`on: push jobs: build: - runs-on: [ubuntu-latest, self|`) + runs-on: [ubuntu-latest, self|]`) ).toEqual({ path: [ ["workflow-root-strict", TokenType.Mapping], diff --git a/actions-languageservice/src/validate.expressions.test.ts b/actions-languageservice/src/validate.expressions.test.ts index 5882796..a5b4944 100644 --- a/actions-languageservice/src/validate.expressions.test.ts +++ b/actions-languageservice/src/validate.expressions.test.ts @@ -350,6 +350,82 @@ jobs: }); }); + describe("env context", () => { + it("references env within scope", async () => { + const input = ` +on: push +jobs: + a: + runs-on: ubuntu-latest + steps: + - name: step a + env: + step_env: job_a_env + run: echo "hello \${{ env.step_env }} +`; + const result = await validate(createDocument("wf.yaml", input)); + + expect(result).toEqual([]); + }); + + it("inherits parent env", async () => { + const input = ` +on: push +env: + envwf: workflow_env +jobs: + a: + runs-on: ubuntu-latest + env: + envjoba: job_a_env + steps: + - name: step a + run: echo "hello \${{ env.envwf }} +`; + const result = await validate(createDocument("wf.yaml", input)); + + expect(result).toEqual([]); + }); + + it("references env outside of scope", async () => { + const input = ` +on: push +env: + envwf: workflow_env +jobs: + a: + runs-on: ubuntu-latest + env: + envjoba: job_a_env + steps: + - name: step a + run: echo "hello" + env: + envstepa: step_a_env + - name: step b + run: echo "hello \${{ env.envstepa }} +`; + const result = await validate(createDocument("wf.yaml", input)); + + expect(result).toEqual([ + { + message: "Context access might be invalid: envstepa", + range: { + end: { + character: 42, + line: 15 + }, + start: { + character: 23, + line: 15 + } + }, + severity: DiagnosticSeverity.Warning + } + ]); + }); + }); + describe("strategy context", () => { it("reference within a matrix job", async () => { const input = ` diff --git a/browser-playground/package.json b/browser-playground/package.json index 74c198e..54143a7 100644 --- a/browser-playground/package.json +++ b/browser-playground/package.json @@ -1,12 +1,12 @@ { "name": "browser-playground", - "version": "0.1.61", + "version": "0.1.65", "description": "", "private": true, "main": "index.js", "type": "module", "dependencies": { - "@github/actions-languageserver": "^0.1.61", + "@github/actions-languageserver": "^0.1.65", "monaco-editor-webpack-plugin": "^7.0.1", "monaco-editor-workers": "^0.34.2", "monaco-languageclient": "^4.0.3", diff --git a/lerna.json b/lerna.json index 57a90f9..7660264 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", "useWorkspaces": true, - "version": "0.1.61" + "version": "0.1.65" } diff --git a/package-lock.json b/package-lock.json index 3ff77de..4a3ac62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,10 +16,10 @@ }, "actions-languageserver": { "name": "@github/actions-languageserver", - "version": "0.1.61", + "version": "0.1.65", "license": "MIT", "dependencies": { - "@github/actions-languageservice": "^0.1.61", + "@github/actions-languageservice": "^0.1.65", "@github/actions-workflow-parser": "*", "@octokit/rest": "^19.0.5", "vscode-languageserver": "^8.0.2", @@ -29,6 +29,7 @@ "devDependencies": { "@types/jest": "^29.0.3", "jest": "^29.0.3", + "nock": "^13.2.9", "prettier": "^2.7.1", "rimraf": "^3.0.2", "ts-jest": "^29.0.3", @@ -40,7 +41,7 @@ }, "actions-languageservice": { "name": "@github/actions-languageservice", - "version": "0.1.61", + "version": "0.1.65", "license": "MIT", "dependencies": { "@github/actions-expressions": "*", @@ -62,10 +63,10 @@ } }, "browser-playground": { - "version": "0.1.61", + "version": "0.1.65", "license": "MIT", "dependencies": { - "@github/actions-languageserver": "^0.1.61", + "@github/actions-languageserver": "^0.1.65", "monaco-editor-webpack-plugin": "^7.0.1", "monaco-editor-workers": "^0.34.2", "monaco-languageclient": "^4.0.3", @@ -8814,6 +8815,21 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node_modules/nock": { + "version": "13.2.9", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.2.9.tgz", + "integrity": "sha512-1+XfJNYF1cjGB+TKMWi29eZ0b82QOvQs2YoLNzbpWGqFMtRQHTa57osqdGj4FrFPgkO4D4AZinzUJR9VvW3QUA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.21", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, "node_modules/node-addon-api": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", @@ -10271,6 +10287,15 @@ "read": "1" } }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -13508,11 +13533,12 @@ "@github/actions-languageserver": { "version": "file:actions-languageserver", "requires": { - "@github/actions-languageservice": "^0.1.61", + "@github/actions-languageservice": "^0.1.65", "@github/actions-workflow-parser": "*", "@octokit/rest": "^19.0.5", "@types/jest": "^29.0.3", "jest": "^29.0.3", + "nock": "^13.2.9", "prettier": "^2.7.1", "rimraf": "^3.0.2", "ts-jest": "^29.0.3", @@ -16294,7 +16320,7 @@ "browser-playground": { "version": "file:browser-playground", "requires": { - "@github/actions-languageserver": "^0.1.61", + "@github/actions-languageserver": "^0.1.65", "css-loader": "^6.7.2", "monaco-editor-webpack-plugin": "^7.0.1", "monaco-editor-workers": "^0.34.2", @@ -19949,6 +19975,18 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "nock": { + "version": "13.2.9", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.2.9.tgz", + "integrity": "sha512-1+XfJNYF1cjGB+TKMWi29eZ0b82QOvQs2YoLNzbpWGqFMtRQHTa57osqdGj4FrFPgkO4D4AZinzUJR9VvW3QUA==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.21", + "propagate": "^2.0.0" + } + }, "node-addon-api": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", @@ -21020,6 +21058,12 @@ "read": "1" } }, + "propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true + }, "proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",