diff --git a/actions-languageserver/package.json b/actions-languageserver/package.json index 70178cc..91a5361 100644 --- a/actions-languageserver/package.json +++ b/actions-languageserver/package.json @@ -1,6 +1,6 @@ { "name": "@github/actions-languageserver", - "version": "0.1.48", + "version": "0.1.58", "description": "Language server for GitHub Actions", "license": "MIT", "type": "module", @@ -38,10 +38,12 @@ "prettier-fix": "prettier --write ." }, "dependencies": { - "@github/actions-languageservice": "^0.1.48", + "@github/actions-languageservice": "^0.1.58", + "@github/actions-workflow-parser": "*", "@octokit/rest": "^19.0.5", "vscode-languageserver": "^8.0.2", - "vscode-languageserver-textdocument": "^1.0.7" + "vscode-languageserver-textdocument": "^1.0.7", + "yaml": "^2.1.3" }, "engines": { "node": ">= 16" diff --git a/actions-languageserver/src/connection.ts b/actions-languageserver/src/connection.ts index 627cd90..c610947 100644 --- a/actions-languageserver/src/connection.ts +++ b/actions-languageserver/src/connection.ts @@ -1,6 +1,7 @@ -import { hover } from "@github/actions-languageservice/hover"; -import { registerLogger, setLogLevel } from "@github/actions-languageservice/log"; -import { validate } from "@github/actions-languageservice/validate"; +import {hover} from "@github/actions-languageservice/hover"; +import {registerLogger, setLogLevel} from "@github/actions-languageservice/log"; +import {validate, ValidationConfig} from "@github/actions-languageservice/validate"; +import {Octokit} from "@octokit/rest"; import { CompletionItem, Connection, @@ -12,12 +13,13 @@ import { TextDocuments, TextDocumentSyncKind } from "vscode-languageserver"; -import { TextDocument } from "vscode-languageserver-textdocument"; -import { contextProviders } from "./context-providers"; -import { InitializationOptions, RepositoryContext } from "./initializationOptions"; -import { onCompletion } from "./on-completion"; -import { TTLCache } from "./utils/cache"; -import { valueProviders } from "./value-providers"; +import {TextDocument} from "vscode-languageserver-textdocument"; +import {contextProviders} from "./context-providers"; +import {InitializationOptions, RepositoryContext} from "./initializationOptions"; +import {onCompletion} from "./on-completion"; +import {TTLCache} from "./utils/cache"; +import {valueProviders} from "./value-providers"; +import {getActionInputs} from "./value-providers/action-inputs"; export function initConnection(connection: Connection) { const documents: TextDocuments = new TextDocuments(TextDocument); @@ -82,11 +84,21 @@ export function initConnection(connection: Connection) { async function validateTextDocument(textDocument: TextDocument): Promise { const repoContext = repos.find(repo => textDocument.uri.startsWith(repo.workspaceUri)); - const result = await validate( - textDocument, - valueProviders(sessionToken, repoContext, cache), - contextProviders(sessionToken, repoContext, cache) - ); + + const config: ValidationConfig = { + valueProviderConfig: valueProviders(sessionToken, repoContext, cache), + contextProviderConfig: contextProviders(sessionToken, repoContext, cache), + getActionInputs: async action => { + if (sessionToken) { + const octokit = new Octokit({ + auth: sessionToken + }); + return await getActionInputs(octokit, cache, action); + } + return undefined; + } + }; + const result = await validate(textDocument, config); connection.sendDiagnostics({uri: textDocument.uri, diagnostics: result}); } diff --git a/actions-languageserver/src/value-providers.ts b/actions-languageserver/src/value-providers.ts index ff706a5..d5e0eba 100644 --- a/actions-languageserver/src/value-providers.ts +++ b/actions-languageserver/src/value-providers.ts @@ -4,6 +4,7 @@ import {ValueProviderKind} from "@github/actions-languageservice/value-providers import {Octokit} from "@octokit/rest"; import {RepositoryContext} from "./initializationOptions"; import {TTLCache} from "./utils/cache"; +import {getActionInputValues} from "./value-providers/action-inputs"; import {getEnvironments} from "./value-providers/job-environment"; import {getRunnerLabels} from "./value-providers/runs-on"; @@ -32,6 +33,10 @@ export function valueProviders( "runs-on": { kind: ValueProviderKind.SuggestedValues, get: (_: WorkflowContext) => getRunnerLabels(octokit, cache, repo.owner, repo.name) + }, + "step-with": { + kind: ValueProviderKind.AllowedValues, + get: (context: WorkflowContext) => getActionInputValues(octokit, cache, context) } }; } diff --git a/actions-languageserver/src/value-providers/action-inputs.ts b/actions-languageserver/src/value-providers/action-inputs.ts new file mode 100644 index 0000000..63d3e94 --- /dev/null +++ b/actions-languageserver/src/value-providers/action-inputs.ts @@ -0,0 +1,108 @@ +import { + actionIdentifier, + 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 {TTLCache} from "../utils/cache"; + +export async function getActionInputs( + client: Octokit, + cache: TTLCache, + action: ActionReference +): Promise { + const inputs = await cache.get(`${actionIdentifier(action)}/action-inputs`, undefined, () => + fetchActionInputs(client, action) + ); + + return inputs; +} + +export async function getActionInputValues( + client: Octokit, + cache: TTLCache, + context: WorkflowContext +): Promise { + if (!context.step || !isActionStep(context.step)) { + return []; + } + + const action = parseActionReference(context.step.uses.value); + if (!action) { + return []; + } + const inputs = await getActionInputs(client, cache, action); + if (!inputs) { + return []; + } + + return Object.entries(inputs).map(([inputName, input]) => { + return { + label: inputName, + description: input.description, + deprecated: input.deprecationMessage !== undefined + }; + }); +} + +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-languageservice/package.json b/actions-languageservice/package.json index 1d67ac5..d562436 100644 --- a/actions-languageservice/package.json +++ b/actions-languageservice/package.json @@ -1,6 +1,6 @@ { "name": "@github/actions-languageservice", - "version": "0.1.48", + "version": "0.1.58", "description": "Language service for GitHub Actions", "license": "MIT", "type": "module", @@ -38,6 +38,7 @@ "prettier-fix": "prettier --write ." }, "dependencies": { + "@github/actions-expressions": "*", "@github/actions-workflow-parser": "*", "vscode-languageserver-textdocument": "^1.0.7", "vscode-languageserver-types": "^3.17.2", diff --git a/actions-languageservice/src/action.test.ts b/actions-languageservice/src/action.test.ts new file mode 100644 index 0000000..f06de9b --- /dev/null +++ b/actions-languageservice/src/action.test.ts @@ -0,0 +1,101 @@ +import {actionIdentifier, parseActionReference as parse} from "./action"; + +describe("parseActionReference", () => { + it("basic action", () => { + expect(parse("actions/checkout@v2")).toEqual({ + owner: "actions", + name: "checkout", + ref: "v2" + }); + }); + + it("action with reference to branch", () => { + expect(parse("actions/checkout@main")).toEqual({ + owner: "actions", + name: "checkout", + ref: "main" + }); + }); + + it("action with reference to branch with slashes", () => { + expect(parse("actions/checkout@features/a")).toEqual({ + owner: "actions", + name: "checkout", + ref: "features/a" + }); + }); + + it("action with reference to commit sha", () => { + expect(parse("actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b")).toEqual({ + owner: "actions", + name: "checkout", + ref: "755da8c3cf115ac066823e79a1e1788f8940201b" + }); + }); + + it("valid action with path", () => { + expect(parse("actions/checkout/path/to/action@v2")).toEqual({ + owner: "actions", + name: "checkout", + ref: "v2", + path: "path/to/action" + }); + }); + + it("valid action with path and trailing slash", () => { + expect(parse("actions/checkout/path/to/action/@v2")).toEqual({ + owner: "actions", + name: "checkout", + ref: "v2", + path: "path/to/action" + }); + }); + + it("local action", () => { + expect(parse("./")).toBeUndefined(); + }); + + it("local action with path", () => { + expect(parse("./directory/")).toBeUndefined(); + }); + + it("Docker Hub action", () => { + expect(parse("docker://alpine:3.8")).toBeUndefined(); + }); + + it("GitHub Packages Container action", () => { + expect(parse("docker://ghcr.io/OWNER/IMAGE_NAME")).toBeUndefined(); + }); + + it("action with backslashes", () => { + expect(parse("actions\\checkout\\path\\to\\action@v2")).toEqual({ + owner: "actions", + name: "checkout", + ref: "v2", + path: "path/to/action" + }); + }); +}); + +describe("actionIdentifier", () => { + it("basic action", () => { + expect( + actionIdentifier({ + owner: "actions", + name: "checkout", + ref: "v2" + }) + ).toEqual("actions/checkout/v2"); + }); + + it("action with path", () => { + expect( + actionIdentifier({ + owner: "actions", + name: "checkout", + ref: "v2", + path: "path/to/action" + }) + ).toEqual("actions/checkout/v2/path/to/action"); + }); +}); diff --git a/actions-languageservice/src/action.ts b/actions-languageservice/src/action.ts new file mode 100644 index 0000000..2f92b5d --- /dev/null +++ b/actions-languageservice/src/action.ts @@ -0,0 +1,51 @@ +// https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#inputs +export type ActionInput = { + description: string; + required?: boolean; + default?: string; + deprecationMessage?: string; +}; + +export type ActionInputs = Record; + +export type ActionReference = { + owner: string; + name: string; + ref: string; + path?: string; +}; + +// https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsuses +export function parseActionReference(uses: string): ActionReference | undefined { + if (!uses || uses.startsWith("docker://") || uses.startsWith("./") || uses.startsWith(".\\")) { + return undefined; + } + + const [action, ref] = uses.split("@"); + const [owner, name, ...pathSegments] = action.split(/[\\/]/).filter(s => s.length > 0); + if (!owner || !name) { + return undefined; + } + + if (pathSegments.length === 0) { + return { + owner, + name, + ref + }; + } + + return { + owner, + name, + ref, + path: pathSegments.join("/") + }; +} + +export function actionIdentifier(ref: ActionReference): string { + if (ref.path) { + return `${ref.owner}/${ref.name}/${ref.ref}/${ref.path}`; + } + return `${ref.owner}/${ref.name}/${ref.ref}`; +} diff --git a/actions-languageservice/src/complete.expressions.test.ts b/actions-languageservice/src/complete.expressions.test.ts index a32c9dd..6b5e41b 100644 --- a/actions-languageservice/src/complete.expressions.test.ts +++ b/actions-languageservice/src/complete.expressions.test.ts @@ -1,4 +1,5 @@ import {data} from "@github/actions-expressions"; +import {CompletionItemKind} from "vscode-languageserver-types"; import {complete, getExpressionInput} from "./complete"; import {ContextProviderConfig} from "./context-providers/config"; import {registerLogger} from "./log"; @@ -30,13 +31,17 @@ describe("expressions", () => { expect(test("${{ gh |")).toBe(" gh "); expect(test("${{ gh |}}")).toBe(" gh "); + expect(test("${{ vars| == 'test' }}")).toBe(" vars"); + expect(test("${{ fromJso|('test').bar == 'test' }}")).toBe(" fromJso"); + expect(test("${{ github.| == 'test' }}")).toBe(" github."); + expect(test("test ${{ github.| == 'test' }}")).toBe(" github."); expect(test("${{ vars }} ${{ gh |}}")).toBe(" gh "); }); describe("top-level auto-complete", () => { it("single region", async () => { const input = "run-name: ${{ | }}"; - const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig); + const result = await complete(...getPositionFromCursor(input), undefined, undefined); expect(result.map(x => x.label)).toEqual([ "github", @@ -70,6 +75,42 @@ describe("expressions", () => { ]); }); + it("single region with existing condition", async () => { + const input = "run-name: ${{ g| == 'test' }}"; + const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig); + + expect(result.map(x => x.label)).toEqual([ + "github", + "inputs", + "vars", + "contains", + "endsWith", + "format", + "fromJson", + "join", + "startsWith", + "toJson" + ]); + }); + + it("multiple regions with partial function", async () => { + const input = "run-name: Run a ${{ inputs.test }} one-line script ${{ from|('test') == inputs.name }}"; + const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig); + + expect(result.map(x => x.label)).toEqual([ + "github", + "inputs", + "vars", + "contains", + "endsWith", + "format", + "fromJson", + "join", + "startsWith", + "toJson" + ]); + }); + it("multiple regions", async () => { const input = "run-name: test-${{ github }}-${{ | }}"; const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig); @@ -320,6 +361,23 @@ jobs: expect(result).toEqual([]); }); + it("github context includes expected keys", async () => { + const input = ` +on: push + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: echo \${{ github.| }} + `; + + const result = await complete(...getPositionFromCursor(input), undefined, undefined); + + expect(result.map(x => x.label)).toContain("actor"); + }); + describe("steps context", () => { it("includes defined step IDs", async () => { const input = ` @@ -615,4 +673,53 @@ jobs: expect(result.map(x => x.label)).toEqual(["color"]); }); }); + + it("context completion items include kind and insert text", async () => { + const input = ` + on: push + + jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: echo \${{ | }}.txt + `; + + const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig); + + // Built-in function + const toJSON = result.find(x => x.label === "toJson"); + expect(toJSON).toBeDefined(); + expect(toJSON!.kind).toBe(CompletionItemKind.Function); + expect(toJSON!.insertText).toBe("toJson()"); + + // Function from context + const hashFiles = result.find(x => x.label === "hashFiles"); + expect(hashFiles).toBeDefined(); + expect(hashFiles!.kind).toBe(CompletionItemKind.Function); + expect(hashFiles!.insertText).toBe("hashFiles()"); + + // Not a function + const github = result.find(x => x.label === "github"); + expect(github).toBeDefined(); + expect(github!.kind).toBe(CompletionItemKind.Variable); + expect(github!.insertText).toBeUndefined(); + }); + + it("function parentheses are not inserted when parentheses already exist", async () => { + const input = ` + on: push + + jobs: + test: + runs-on: ubuntu-latest + steps: + - run: echo \${{ toJS|(github.event) }} + `; + + const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig); + + expect(result.find(x => x.label === "toJson")!.insertText).toBe("toJson"); + }); }); diff --git a/actions-languageservice/src/complete.test.ts b/actions-languageservice/src/complete.test.ts index ef59c0e..b9c20ca 100644 --- a/actions-languageservice/src/complete.test.ts +++ b/actions-languageservice/src/complete.test.ts @@ -199,6 +199,24 @@ jobs: expect(result[0].label).toEqual("my-custom-label"); }); + it("custom value providers for sequences", async () => { + const input = "on: push\njobs:\n build:\n runs-on: [m|]"; + + const config: ValueProviderConfig = { + "runs-on": { + kind: ValueProviderKind.SuggestedValues, + get: async (_: WorkflowContext) => { + return [{label: "my-custom-label"}]; + } + } + }; + const result = await complete(...getPositionFromCursor(input), config); + + expect(result).not.toBeUndefined(); + expect(result.length).toEqual(1); + expect(result[0].label).toEqual("my-custom-label"); + }); + it("does not show parent mapping sibling keys", async () => { const input = `on: push jobs: diff --git a/actions-languageservice/src/complete.ts b/actions-languageservice/src/complete.ts index bce981a..0477b19 100644 --- a/actions-languageservice/src/complete.ts +++ b/actions-languageservice/src/complete.ts @@ -1,20 +1,20 @@ import {complete as completeExpression} from "@github/actions-expressions"; +import {CompletionItem as ExpressionCompletionItem} from "@github/actions-expressions/completion"; import {convertWorkflowTemplate, isSequence, isString, parseWorkflow} from "@github/actions-workflow-parser"; import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert"; import {DefinitionType} from "@github/actions-workflow-parser/templates/schema/definition-type"; import {StringDefinition} from "@github/actions-workflow-parser/templates/schema/string-definition"; -import {CLOSE_EXPRESSION, OPEN_EXPRESSION} from "@github/actions-workflow-parser/templates/template-constants"; +import {OPEN_EXPRESSION} from "@github/actions-workflow-parser/templates/template-constants"; import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/index"; import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token"; import {TokenType} from "@github/actions-workflow-parser/templates/tokens/types"; import {File} from "@github/actions-workflow-parser/workflows/file"; import {Position, TextDocument} from "vscode-languageserver-textdocument"; -import {CompletionItem, Range, TextEdit} from "vscode-languageserver-types"; +import {CompletionItem, CompletionItemKind, CompletionItemTag, Range, TextEdit} from "vscode-languageserver-types"; import {ContextProviderConfig} from "./context-providers/config"; import {getContext, Mode} from "./context-providers/default"; import {getWorkflowContext, WorkflowContext} from "./context/workflow-context"; import {nullTrace} from "./nulltrace"; -import {getAllowedContext} from "./utils/allowed-context"; import {findToken} from "./utils/find-token"; import {mapRange} from "./utils/range"; import {transform} from "./utils/transform"; @@ -29,14 +29,7 @@ export function getExpressionInput(input: string, pos: number): string { return input; } - // Find end marker after the cursor position - let endPos = input.indexOf(CLOSE_EXPRESSION, pos); - if (endPos === -1) { - // Assume an unfinished expression like "${{ someinput.|" - endPos = input.length; - } - - return input.substring(startPos + OPEN_EXPRESSION.length, endPos); + return input.substring(startPos + OPEN_EXPRESSION.length, pos); } export async function complete( @@ -85,17 +78,19 @@ export async function complete( if (token.range!.start[0] !== token.range!.end[0]) { const lines = currentInput.split("\n"); const linesBeforeCusor = lines.slice(0, lineDiff); - relCharPos = linesBeforeCusor.join("\n").length + newPos.character; + relCharPos = linesBeforeCusor.join("\n").length + 1 + newPos.character; } else { - relCharPos = newPos.character - token.range!.start[1]; + relCharPos = newPos.character - token.range!.start[1] + 1; } const expressionInput = (getExpressionInput(currentInput, relCharPos) || "").trim(); - const allowedContext = getAllowedContext(token, parent); + const allowedContext = token.definitionInfo?.allowedContext || []; const context = await getContext(allowedContext, contextProviderConfig, workflowContext, Mode.Completion); - return completeExpression(expressionInput, context, []); + return completeExpression(expressionInput, context, []).map(item => + mapExpressionCompletionItem(item, currentInput[relCharPos]) + ); } } @@ -106,11 +101,12 @@ export async function complete( } return values.map(value => { - const item = CompletionItem.create(value.label); - item.detail = value.description; - if (replaceRange) { - item.textEdit = TextEdit.replace(replaceRange, value.label); - } + const item: CompletionItem = { + label: value.label, + detail: value.description, + tags: value.deprecated ? [CompletionItemTag.Deprecated] : undefined, + textEdit: replaceRange ? TextEdit.replace(replaceRange, value.label) : undefined + }; return item; }); @@ -129,21 +125,22 @@ async function getValues( const existingValues = getExistingValues(token, parent); - if (keyToken?.definition?.key) { - const customValues = await valueProviderConfig?.[keyToken.definition.key]?.get(workflowContext); + // Use the value providers from the parent if the current key is null + const valueProviderToken = keyToken || parent; + const customValueProvider = + valueProviderToken?.definition?.key && valueProviderConfig?.[valueProviderToken.definition.key]; + if (customValueProvider) { + const customValues = await customValueProvider.get(workflowContext); if (customValues) { return filterAndSortCompletionOptions(customValues, existingValues); } } - // Use the value provider from the parent if we don't have a value provider for the current key - const valueProvider = - (keyToken?.definition?.key && defaultValueProviders[keyToken.definition.key]) || - (parent.definition?.key && defaultValueProviders[parent.definition.key]); - - if (valueProvider) { - const values = await valueProvider.get(workflowContext); + const defaultValueProvider = + valueProviderToken?.definition?.key && defaultValueProviders[valueProviderToken.definition.key]; + if (defaultValueProvider) { + const values = await defaultValueProvider.get(workflowContext); return filterAndSortCompletionOptions(values, existingValues); } @@ -198,3 +195,17 @@ function filterAndSortCompletionOptions(options: Value[], existingValues?: Set a.label.localeCompare(b.label)); return options; } + +function mapExpressionCompletionItem(item: ExpressionCompletionItem, charAfterPos: string): CompletionItem { + let insertText: string | undefined; + // Insert parentheses if the cursor is after a function + // and the function does not have any parantheses already + if (item.function) { + insertText = charAfterPos === "(" ? item.label : item.label + "()"; + } + return { + label: item.label, + insertText: insertText, + kind: item.function ? CompletionItemKind.Function : CompletionItemKind.Variable + }; +} diff --git a/actions-languageservice/src/context-providers/default.ts b/actions-languageservice/src/context-providers/default.ts index 00c16fd..a586cdf 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 {getGithubContext} from "./github"; import {getInputsContext} from "./inputs"; import {getJobContext} from "./job"; import {getMatrixContext} from "./matrix"; @@ -45,6 +46,18 @@ export async function getContext( function getDefaultContext(name: string, workflowContext: WorkflowContext, mode: Mode): ContextValue | undefined { switch (name) { + case "github": + return getGithubContext(); + + case "inputs": + return getInputsContext(workflowContext); + + case "matrix": + return getMatrixContext(workflowContext, mode); + + case "needs": + return getNeedsContext(workflowContext); + case "runner": return objectToDictionary({ os: "Linux", @@ -54,12 +67,6 @@ function getDefaultContext(name: string, workflowContext: WorkflowContext, mode: temp: "/home/runner/work/_temp" }); - case "needs": - return getNeedsContext(workflowContext); - - case "inputs": - return getInputsContext(workflowContext); - case "secrets": return objectToDictionary({GITHUB_TOKEN: "***"}); diff --git a/actions-languageservice/src/context-providers/github.ts b/actions-languageservice/src/context-providers/github.ts new file mode 100644 index 0000000..0422bb9 --- /dev/null +++ b/actions-languageservice/src/context-providers/github.ts @@ -0,0 +1,47 @@ +import {data} from "@github/actions-expressions"; + +export function getGithubContext(): data.Dictionary { + // https://docs.github.com/en/actions/learn-github-actions/contexts#github-context + const keys = [ + "action", + "action_path", + "action_ref", + "action_repository", + "action_status", + "actor", + "api_url", + "base_ref", + "env", + "event", + "event_name", + "event_path", + "graphql_url", + "head_ref", + "job", + "ref", + "ref_name", + "ref_protected", + "ref_type", + "path", + "repository", + "repository_owner", + "repositoryUrl", + "retention_days", + "run_id", + "run_number", + "run_attempt", + "secret_source", + "server_url", + "sha", + "token", + "triggering_actor", + "workflow", + "workspace" + ]; + + return new data.Dictionary( + ...keys.map(key => { + return {key, value: new data.Null()}; + }) + ); +} diff --git a/actions-languageservice/src/utils/allowed-context.ts b/actions-languageservice/src/utils/allowed-context.ts deleted file mode 100644 index 36a5d91..0000000 --- a/actions-languageservice/src/utils/allowed-context.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token"; - -export function getAllowedContext(token: TemplateToken, parent: TemplateToken | null | undefined): string[] { - // Workaround for https://github.com/github/c2c-actions-experience/issues/6876 - // Context is inherited from the parent - const allowedContext = new Set(); - for (const t of [token, parent]) { - if (t?.definition?.readerContext) { - for (const context of t.definition.readerContext) { - allowedContext.add(context); - } - } - } - return Array.from(allowedContext); -} diff --git a/actions-languageservice/src/validate-action.ts b/actions-languageservice/src/validate-action.ts new file mode 100644 index 0000000..dbd404a --- /dev/null +++ b/actions-languageservice/src/validate-action.ts @@ -0,0 +1,82 @@ +import {isMapping} from "@github/actions-workflow-parser"; +import {isActionStep} from "@github/actions-workflow-parser/model/type-guards"; +import {Step} from "@github/actions-workflow-parser/model/workflow-template"; +import {ScalarToken} from "@github/actions-workflow-parser/templates/tokens/scalar-token"; +import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token"; +import {Diagnostic, DiagnosticSeverity} from "vscode-languageserver-types"; +import {parseActionReference} from "./action"; +import {mapRange} from "./utils/range"; +import {ValidationConfig} from "./validate"; + +export async function validateAction( + diagnostics: Diagnostic[], + stepToken: TemplateToken, + step: Step | undefined, + config: ValidationConfig | undefined +): Promise { + if (!isMapping(stepToken) || !step || !isActionStep(step) || !config?.getActionInputs) { + return; + } + + const action = parseActionReference(step.uses.value); + if (!action) { + return; + } + + const actionInputs = await config.getActionInputs(action); + if (actionInputs === undefined) { + return; + } + + let withKey: ScalarToken | undefined; + let withToken: TemplateToken | undefined; + for (const {key, value} of stepToken) { + if (key.toString() === "with") { + withKey = key; + withToken = value; + break; + } + } + + const stepInputs = new Map(); + if (withToken && isMapping(withToken)) { + for (const {key} of withToken) { + stepInputs.set(key.toString(), key); + } + } + + for (const [input, inputToken] of stepInputs) { + if (!actionInputs[input]) { + diagnostics.push({ + severity: DiagnosticSeverity.Error, + range: mapRange(inputToken.range), + message: `Invalid action input '${input}'` + }); + } + + const deprecationMessage = actionInputs[input]?.deprecationMessage; + if (deprecationMessage) { + diagnostics.push({ + severity: DiagnosticSeverity.Warning, + range: mapRange(inputToken.range), + message: deprecationMessage + }); + } + } + + const missingRequiredInputs = Object.entries(actionInputs).filter( + ([inputName, input]) => input.required && !stepInputs.has(inputName) + ); + + if (missingRequiredInputs.length > 0) { + const message = + missingRequiredInputs.length === 1 + ? `Missing required input \`${missingRequiredInputs[0][0]}\`` + : `Missing required inputs: ${missingRequiredInputs.map(input => `\`${input[0]}\``).join(", ")}`; + diagnostics.push({ + severity: DiagnosticSeverity.Error, + range: mapRange((withKey || stepToken).range), // Highlight the whole step if we don't have a with key + message: message + }); + } +} diff --git a/actions-languageservice/src/validate.actions.test.ts b/actions-languageservice/src/validate.actions.test.ts new file mode 100644 index 0000000..9b499bf --- /dev/null +++ b/actions-languageservice/src/validate.actions.test.ts @@ -0,0 +1,259 @@ +import {Diagnostic, DiagnosticSeverity} from "vscode-languageserver-types"; +import {ActionInput, ActionReference} from "./action"; +import {registerLogger} from "./log"; +import {createDocument} from "./test-utils/document"; +import {TestLogger} from "./test-utils/logger"; +import {validate, ValidationConfig} from "./validate"; +import {ValueProviderKind} from "./value-providers/config"; + +registerLogger(new TestLogger()); + +const validationConfig: ValidationConfig = { + getActionInputs: async (ref: ActionReference) => { + let inputs: Record = {}; + switch (ref.owner + "/" + ref.name + "@" + ref.ref) { + case "actions/checkout@v3": + inputs = { + repository: { + description: "Repository name with owner", + default: "${{ github.repository }}" + } + }; + break; + case "actions/setup-node@v1": + inputs = { + version: { + description: "Deprecated. Use node-version instead. Will not be supported after October 1, 2019", + deprecationMessage: + "The version property will not be supported after October 1, 2019. Use node-version instead" + } + }; + break; + case "actions/cache@v1": + inputs = { + path: { + description: "A directory to store and save the cache", + required: true + }, + key: { + description: "An explicit key for restoring and saving the cache", + required: true + }, + "restore-keys": { + description: "An ordered list of keys to use for restoring the cache if no cache hit occurred for key", + required: false + } + }; + break; + } + return inputs; + } +}; +describe("validate action steps", () => { + it("valid action reference", async () => { + const input = ` +on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 +`; + const result = await validate(createDocument("wf.yaml", input), validationConfig); + + expect(result).toEqual([]); + }); + + it("invalid input", async () => { + const input = ` +on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + notanoption: true +`; + const result = await validate(createDocument("wf.yaml", input), validationConfig); + + expect(result).toEqual([ + { + message: "Invalid action input 'notanoption'", + range: { + end: { + character: 19, + line: 8 + }, + start: { + character: 8, + line: 8 + } + }, + severity: DiagnosticSeverity.Error + } + ]); + }); + + it("deprecated input", async () => { + const input = ` +on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-node@v1 + with: + version: 10 +`; + const result = await validate(createDocument("wf.yaml", input), validationConfig); + + expect(result).toEqual([ + { + message: "The version property will not be supported after October 1, 2019. Use node-version instead", + range: { + end: { + character: 15, + line: 8 + }, + start: { + character: 8, + line: 8 + } + }, + severity: DiagnosticSeverity.Warning + } + ]); + }); + + it("missing required input", async () => { + const input = ` +on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/cache@v1 + with: + key: \${{ runner.os }}-node-\${{ hashFiles('**/package-lock.json') }} +`; + const result = await validate(createDocument("wf.yaml", input), validationConfig); + + expect(result).toEqual([ + { + message: "Missing required input `path`", + range: { + end: { + character: 10, + line: 7 + }, + start: { + character: 6, + line: 7 + } + }, + severity: DiagnosticSeverity.Error + } + ]); + }); + + it("multiple missing required inputs", async () => { + const input = ` +on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/cache@v1 + with: + restore-keys: \${{ runner.os }}-node- +`; + const result = await validate(createDocument("wf.yaml", input), validationConfig); + + expect(result).toEqual([ + { + message: "Missing required inputs: `path`, `key`", + range: { + end: { + character: 10, + line: 7 + }, + start: { + character: 6, + line: 7 + } + }, + severity: DiagnosticSeverity.Error + } + ]); + }); + + it("missing required inputs without a `with` key", async () => { + const input = ` +on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/cache@v1 +`; + const result = await validate(createDocument("wf.yaml", input), validationConfig); + + expect(result).toEqual([ + { + message: "Missing required inputs: `path`, `key`", + range: { + end: { + character: 0, + line: 7 + }, + start: { + character: 6, + line: 6 + } + }, + severity: DiagnosticSeverity.Error + } + ]); + }); + + it("skips extra validation from custom value provider", async () => { + const input = ` +on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + notanoption: true +`; + const config = validationConfig; + config.valueProviderConfig = { + "step-with": { + kind: ValueProviderKind.AllowedValues, + get: async () => { + return [{label: "repository", description: "Repository name with owner."}]; + } + } + }; + const result = await validate(createDocument("wf.yaml", input), config); + + expect(result).toEqual([ + { + message: "Invalid action input 'notanoption'", + range: { + end: { + character: 19, + line: 8 + }, + start: { + character: 8, + line: 8 + } + }, + severity: DiagnosticSeverity.Error + } + ]); + }); +}); diff --git a/actions-languageservice/src/validate.expressions.test.ts b/actions-languageservice/src/validate.expressions.test.ts index 8e7f6fe..6cb674e 100644 --- a/actions-languageservice/src/validate.expressions.test.ts +++ b/actions-languageservice/src/validate.expressions.test.ts @@ -367,7 +367,7 @@ jobs: }); }); - describe("multi-line strings", () => { + describe("multi-line strings warnings", () => { it("indented |", async () => { const input = `on: push jobs: @@ -489,6 +489,124 @@ jobs: }); }); + describe("multi-line strings errors", () => { + it("indented |", async () => { + const input = `on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - run: | + first line + test \${{ fromJSON2('') }} + test2`; + const result = await validate(createDocument("wf.yaml", input)); + + expect(result).toEqual([ + { + message: "Unrecognized function: 'fromJSON2'", + range: { + end: { + character: 35, + line: 7 + }, + start: { + character: 15, + line: 7 + } + } + } + ]); + }); + + it("indented |+", async () => { + const input = `on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - run: |+ + first line + test \${{ fromJSON2('') }} + test2`; + const result = await validate(createDocument("wf.yaml", input)); + + expect(result).toEqual([ + { + message: "Unrecognized function: 'fromJSON2'", + range: { + end: { + character: 35, + line: 7 + }, + start: { + character: 15, + line: 7 + } + } + } + ]); + }); + + it("indented >", async () => { + const input = `on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - run: > + first line + test \${{ fromJSON2('') }} + test2`; + const result = await validate(createDocument("wf.yaml", input)); + + expect(result).toEqual([ + { + message: "Unrecognized function: 'fromJSON2'", + range: { + end: { + character: 35, + line: 7 + }, + start: { + character: 15, + line: 7 + } + } + } + ]); + }); + + it("indented >+", async () => { + const input = `on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - run: >+ + first line + test \${{ fromJSON2('') }} + test2`; + const result = await validate(createDocument("wf.yaml", input)); + + expect(result).toEqual([ + { + message: "Unrecognized function: 'fromJSON2'", + range: { + end: { + character: 35, + line: 7 + }, + start: { + character: 15, + line: 7 + } + } + } + ]); + }); + }); + describe("matrix context", () => { it("reference within a matrix job", async () => { const input = ` diff --git a/actions-languageservice/src/validate.test.ts b/actions-languageservice/src/validate.test.ts index 44beb3a..eaaa38e 100644 --- a/actions-languageservice/src/validate.test.ts +++ b/actions-languageservice/src/validate.test.ts @@ -70,7 +70,7 @@ jobs: steps: - run: echo` ), - defaultValueProviders + {valueProviderConfig: defaultValueProviders} ); expect(result.length).toBe(1); @@ -103,7 +103,7 @@ jobs: steps: - run: echo` ), - defaultValueProviders + {valueProviderConfig: defaultValueProviders} ); expect(result.length).toBe(1); @@ -139,7 +139,7 @@ jobs: steps: - run: echo` ), - defaultValueProviders + {valueProviderConfig: defaultValueProviders} ); expect(result[0]).toEqual({ @@ -168,7 +168,7 @@ jobs: runs-on: - ubuntu-latest` ), - defaultValueProviders + {valueProviderConfig: defaultValueProviders} ); expect(result.length).toBe(1); diff --git a/actions-languageservice/src/validate.ts b/actions-languageservice/src/validate.ts index 8d2938e..d5e9884 100644 --- a/actions-languageservice/src/validate.ts +++ b/actions-languageservice/src/validate.ts @@ -16,6 +16,7 @@ import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/te import {File} from "@github/actions-workflow-parser/workflows/file"; import {TextDocument} from "vscode-languageserver-textdocument"; import {Diagnostic, DiagnosticSeverity, URI} from "vscode-languageserver-types"; +import {ActionInputs, ActionReference} from "./action"; import {ContextProviderConfig} from "./context-providers/config"; import {getContext, Mode} from "./context-providers/default"; @@ -23,12 +24,18 @@ import {getWorkflowContext, WorkflowContext} from "./context/workflow-context"; import {AccessError, wrapDictionary} from "./expression-validation/error-dictionary"; import {error} from "./log"; import {nullTrace} from "./nulltrace"; -import {getAllowedContext} from "./utils/allowed-context"; import {findToken} from "./utils/find-token"; import {mapRange} from "./utils/range"; +import {validateAction} from "./validate-action"; import {ValueProviderConfig, ValueProviderKind} from "./value-providers/config"; import {defaultValueProviders} from "./value-providers/default"; +export type ValidationConfig = { + valueProviderConfig?: ValueProviderConfig; + contextProviderConfig?: ContextProviderConfig; + getActionInputs?(action: ActionReference): Promise; +}; + /** * Validates a workflow file * @@ -37,9 +44,8 @@ import {defaultValueProviders} from "./value-providers/default"; */ export async function validate( textDocument: TextDocument, + config?: ValidationConfig // TODO: Support multiple files, context for API calls - valueProviderConfig?: ValueProviderConfig, - contextProviderConfig?: ContextProviderConfig ): Promise { const file: File = { name: textDocument.uri, @@ -55,14 +61,7 @@ export async function validate( const template = convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion); // Validate expressions and value providers - await additionalValidations( - diagnostics, - textDocument.uri, - template, - result.value, - valueProviderConfig, - contextProviderConfig - ); + await additionalValidations(diagnostics, textDocument.uri, template, result.value, config); } // For now map parser errors directly to diagnostics @@ -86,8 +85,7 @@ async function additionalValidations( documentUri: URI, template: WorkflowTemplate, root: TemplateToken, - valueProviderConfig: ValueProviderConfig | undefined, - contextProviderConfig: ContextProviderConfig | undefined + config?: ValidationConfig ) { for (const [parent, token, key] of TemplateToken.traverse(root)) { // If the token is a value in a pair, use the key definition for validation @@ -95,26 +93,33 @@ async function additionalValidations( const validationToken = key || parent || token; const validationDefinition = validationToken.definition; - const allowedContext = getAllowedContext(validationToken, parent); - // If this is an expression, validate it if (isBasicExpression(token)) { await validateExpression( diagnostics, token, - allowedContext, - contextProviderConfig, + validationToken.definitionInfo?.allowedContext || [], + config?.contextProviderConfig, getProviderContext(documentUri, template, root, token) ); } + if (token.definition?.key === "regular-step") { + const context = getProviderContext(documentUri, template, root, token); + await validateAction(diagnostics, token, context.step, config); + } + // Allowed values coming from the schema have already been validated. Only check if // a value provider is defined for a token and if it is, validate the values match. - if (valueProviderConfig && token.range && validationDefinition) { + if (config?.valueProviderConfig && token.range && validationDefinition) { const defKey = validationDefinition.key; + if (defKey === "step-with") { + // Action inputs should be validated already in validateAction + continue; + } // Try a custom value provider first - let valueProvider = valueProviderConfig[defKey]; + let valueProvider = config.valueProviderConfig[defKey]; if (!valueProvider) { // fall back to default valueProvider = defaultValueProviders[defKey]; @@ -209,8 +214,6 @@ async function validateExpression( severity: DiagnosticSeverity.Warning, range: mapRange(expression.range) }); - } else { - throw e; } } } diff --git a/actions-languageservice/src/value-providers/config.ts b/actions-languageservice/src/value-providers/config.ts index 34b7337..725ad4c 100644 --- a/actions-languageservice/src/value-providers/config.ts +++ b/actions-languageservice/src/value-providers/config.ts @@ -3,6 +3,7 @@ import {WorkflowContext} from "../context/workflow-context"; export interface Value { label: string; description?: string; + deprecated?: boolean; } export enum ValueProviderKind { diff --git a/browser-playground/package.json b/browser-playground/package.json index 175a156..f87528e 100644 --- a/browser-playground/package.json +++ b/browser-playground/package.json @@ -1,12 +1,12 @@ { "name": "browser-playground", - "version": "0.1.48", + "version": "0.1.58", "description": "", "private": true, "main": "index.js", "type": "module", "dependencies": { - "@github/actions-languageserver": "^0.1.48", + "@github/actions-languageserver": "^0.1.58", "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 316b5a0..15cc41f 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", "useWorkspaces": true, - "version": "0.1.48" + "version": "0.1.58" } diff --git a/package-lock.json b/package-lock.json index 5106819..fd48861 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,13 +16,15 @@ }, "actions-languageserver": { "name": "@github/actions-languageserver", - "version": "0.1.48", + "version": "0.1.58", "license": "MIT", "dependencies": { - "@github/actions-languageservice": "^0.1.48", + "@github/actions-languageservice": "^0.1.58", + "@github/actions-workflow-parser": "*", "@octokit/rest": "^19.0.5", "vscode-languageserver": "^8.0.2", - "vscode-languageserver-textdocument": "^1.0.7" + "vscode-languageserver-textdocument": "^1.0.7", + "yaml": "^2.1.3" }, "devDependencies": { "@types/jest": "^29.0.3", @@ -38,9 +40,10 @@ }, "actions-languageservice": { "name": "@github/actions-languageservice", - "version": "0.1.48", + "version": "0.1.58", "license": "MIT", "dependencies": { + "@github/actions-expressions": "*", "@github/actions-workflow-parser": "*", "vscode-languageserver-textdocument": "^1.0.7", "vscode-languageserver-types": "^3.17.2", @@ -59,10 +62,10 @@ } }, "browser-playground": { - "version": "0.1.48", + "version": "0.1.58", "license": "MIT", "dependencies": { - "@github/actions-languageserver": "^0.1.48", + "@github/actions-languageserver": "^0.1.58", "monaco-editor-webpack-plugin": "^7.0.1", "monaco-editor-workers": "^0.34.2", "monaco-languageclient": "^4.0.3", @@ -681,9 +684,9 @@ "dev": true }, "node_modules/@github/actions-expressions": { - "version": "0.0.7", - "resolved": "https://npm.pkg.github.com/download/@github/actions-expressions/0.0.7/deed2be11b9f74791730303b6e33f9aca701d6a7", - "integrity": "sha512-PG8dQUafOckk/ny5O1wcUcfml5IZM8REoi7WBBY1HfpnRR7mL7gqTczQEmQWNTpoHb9f1VQK47y2hBZTOg9Y7g==", + "version": "0.0.8", + "resolved": "https://npm.pkg.github.com/download/@github/actions-expressions/0.0.8/055f455fcb0ed907f52c349b9385728aeed38f9a", + "integrity": "sha512-SPuGfnjgKAbMzJNCk4sZPAK2bxHlRXDJCk6vrwIi9VJL16Eh1YyiyM4fU9kV5EXEE5AV9aPFlZAPXu6t0mmBNw==", "license": "MIT", "engines": { "node": ">= 16" @@ -698,9 +701,9 @@ "link": true }, "node_modules/@github/actions-workflow-parser": { - "version": "0.0.31", - "resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.31/ebc46956b91ed1a8c45efd41a3256ae818722557", - "integrity": "sha512-3k+MBWG7Gn86aHeLdJTiYUTGm53npEelzleCbq/8Ir51xVkRINsyNe4HuGJizXxZ2WoTxAkLeZ6qpO1oCucSIg==", + "version": "0.0.35", + "resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.35/0e88d541486f7c8772c5640fc13fac630cfaeaa8", + "integrity": "sha512-OuNEuqUH4AOfWd2xb8VRl7KwJrtiZu1ic0b27lmadwvK8qgRhPHxvS+QICEc+JtFbbVWpAA2ymh8I1U35639mA==", "license": "MIT", "dependencies": { "@github/actions-expressions": "*", @@ -13498,14 +13501,15 @@ "dev": true }, "@github/actions-expressions": { - "version": "0.0.7", - "resolved": "https://npm.pkg.github.com/download/@github/actions-expressions/0.0.7/deed2be11b9f74791730303b6e33f9aca701d6a7", - "integrity": "sha512-PG8dQUafOckk/ny5O1wcUcfml5IZM8REoi7WBBY1HfpnRR7mL7gqTczQEmQWNTpoHb9f1VQK47y2hBZTOg9Y7g==" + "version": "0.0.8", + "resolved": "https://npm.pkg.github.com/download/@github/actions-expressions/0.0.8/055f455fcb0ed907f52c349b9385728aeed38f9a", + "integrity": "sha512-SPuGfnjgKAbMzJNCk4sZPAK2bxHlRXDJCk6vrwIi9VJL16Eh1YyiyM4fU9kV5EXEE5AV9aPFlZAPXu6t0mmBNw==" }, "@github/actions-languageserver": { "version": "file:actions-languageserver", "requires": { - "@github/actions-languageservice": "^0.1.48", + "@github/actions-languageservice": "^0.1.58", + "@github/actions-workflow-parser": "*", "@octokit/rest": "^19.0.5", "@types/jest": "^29.0.3", "jest": "^29.0.3", @@ -13514,12 +13518,14 @@ "ts-jest": "^29.0.3", "typescript": "^4.8.4", "vscode-languageserver": "^8.0.2", - "vscode-languageserver-textdocument": "^1.0.7" + "vscode-languageserver-textdocument": "^1.0.7", + "yaml": "^2.1.3" } }, "@github/actions-languageservice": { "version": "file:actions-languageservice", "requires": { + "@github/actions-expressions": "*", "@github/actions-workflow-parser": "*", "@types/jest": "^29.0.3", "jest": "^29.0.3", @@ -13533,9 +13539,9 @@ } }, "@github/actions-workflow-parser": { - "version": "0.0.31", - "resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.31/ebc46956b91ed1a8c45efd41a3256ae818722557", - "integrity": "sha512-3k+MBWG7Gn86aHeLdJTiYUTGm53npEelzleCbq/8Ir51xVkRINsyNe4HuGJizXxZ2WoTxAkLeZ6qpO1oCucSIg==", + "version": "0.0.35", + "resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.35/0e88d541486f7c8772c5640fc13fac630cfaeaa8", + "integrity": "sha512-OuNEuqUH4AOfWd2xb8VRl7KwJrtiZu1ic0b27lmadwvK8qgRhPHxvS+QICEc+JtFbbVWpAA2ymh8I1U35639mA==", "requires": { "@github/actions-expressions": "*", "yaml": "^2.0.0-8" @@ -16288,7 +16294,7 @@ "browser-playground": { "version": "file:browser-playground", "requires": { - "@github/actions-languageserver": "^0.1.48", + "@github/actions-languageserver": "^0.1.58", "css-loader": "^6.7.2", "monaco-editor-webpack-plugin": "^7.0.1", "monaco-editor-workers": "^0.34.2",