From 48c728771e38751736fea03d50a5bf9ff484a296 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Mon, 23 Jan 2023 16:02:13 -0500 Subject: [PATCH 1/3] Show action input descriptions on hover --- actions-languageserver/src/connection.ts | 5 +- .../src/description-provider.ts | 26 +++++++ .../src/description-providers/action-input.ts | 35 +++++++++ actions-languageservice/src/hover.test.ts | 55 +++++++++++++- actions-languageservice/src/hover.ts | 74 ++++++++++++------- .../src/value-providers/config.ts | 2 +- 6 files changed, 166 insertions(+), 31 deletions(-) create mode 100644 actions-languageserver/src/description-provider.ts create mode 100644 actions-languageserver/src/description-providers/action-input.ts diff --git a/actions-languageserver/src/connection.ts b/actions-languageserver/src/connection.ts index af1bcad..d1650bc 100644 --- a/actions-languageserver/src/connection.ts +++ b/actions-languageserver/src/connection.ts @@ -23,6 +23,7 @@ import {TTLCache} from "./utils/cache"; import {valueProviders} from "./value-providers"; import {getActionInputs} from "./value-providers/action-inputs"; import {Commands} from "./commands"; +import {descriptionProvider} from "./description-provider"; export function initConnection(connection: Connection) { const documents: TextDocuments = new TextDocuments(TextDocument); @@ -120,7 +121,9 @@ export function initConnection(connection: Connection) { }); connection.onHover(async ({position, textDocument}: HoverParams): Promise => { - return hover(documents.get(textDocument.uri)!, position); + return hover(documents.get(textDocument.uri)!, position, { + descriptionProvider: descriptionProvider(sessionToken, cache) + }); }); connection.onRequest("workspace/executeCommand", (params: ExecuteCommandParams) => { diff --git a/actions-languageserver/src/description-provider.ts b/actions-languageserver/src/description-provider.ts new file mode 100644 index 0000000..bd724ed --- /dev/null +++ b/actions-languageserver/src/description-provider.ts @@ -0,0 +1,26 @@ +import {DescriptionProvider} from "@github/actions-languageservice/hover"; +import {Octokit} from "@octokit/rest"; +import {TTLCache} from "./utils/cache"; +import {getActionInputDescription} from "./description-providers/action-input"; + +export function descriptionProvider(sessionToken: string | undefined, cache: TTLCache): DescriptionProvider { + const octokit = + sessionToken && + new Octokit({ + auth: sessionToken + }); + + const getDescription: DescriptionProvider["getDescription"] = async (context, token, path) => { + if (!octokit) { + return undefined; + } + const parent = path[path.length - 1]; + if (context.step && parent.definition?.key === "step-with") { + return await getActionInputDescription(octokit, cache, context.step, token); + } + }; + + return { + getDescription + }; +} diff --git a/actions-languageserver/src/description-providers/action-input.ts b/actions-languageserver/src/description-providers/action-input.ts new file mode 100644 index 0000000..1f26abf --- /dev/null +++ b/actions-languageserver/src/description-providers/action-input.ts @@ -0,0 +1,35 @@ +import {parseActionReference} from "@github/actions-languageservice/action"; +import {isString} 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 {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token"; +import {Octokit} from "@octokit/rest"; +import {fetchActionMetadata} from "../utils/action-metadata"; +import {TTLCache} from "../utils/cache"; + +export async function getActionInputDescription( + client: Octokit, + cache: TTLCache, + step: Step, + token: TemplateToken +): Promise { + if (!isActionStep(step)) { + return undefined; + } + const action = parseActionReference(step.uses.value); + if (!action) { + return undefined; + } + + const inputName = isString(token) && token.value; + if (!inputName) { + return undefined; + } + + const metadata = await fetchActionMetadata(client, cache, action); + if (!metadata?.inputs) { + return undefined; + } + + return metadata.inputs[inputName]?.description; +} diff --git a/actions-languageservice/src/hover.test.ts b/actions-languageservice/src/hover.test.ts index 1fce7e9..8581a27 100644 --- a/actions-languageservice/src/hover.test.ts +++ b/actions-languageservice/src/hover.test.ts @@ -1,6 +1,25 @@ -import {hover} from "./hover"; +import {isString} from "@github/actions-workflow-parser/."; +import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token"; +import {DescriptionProvider, hover, HoverConfig} from "./hover"; import {getPositionFromCursor} from "./test-utils/cursor-position"; +function testHoverConfig(tokenValue: string, tokenKey: string, description?: string) { + return { + descriptionProvider: { + getDescription: async (_, token, __) => { + if (!isString(token)) { + throw new Error("Test provider only supports string tokens"); + } + + expect((token as StringToken).value).toEqual(tokenValue); + expect(token.definition!.key).toEqual(tokenKey); + + return description; + } + } satisfies DescriptionProvider + } satisfies HoverConfig +} + describe("hover", () => { it("on a key", async () => { const input = `o|n: push @@ -77,3 +96,37 @@ jobs: expect(result?.contents).toEqual("Runs your workflow when you push a commit or tag."); }); }); + +describe("hover with description provider", () => { + it("uses the description provider", async () => { + const input = ` +on: push +jobs: + build: + runs-on: [self-hosted] + steps: + - uses: actions/checkout@v2 + with: + ref|: main +`; + + const result = await hover(...getPositionFromCursor(input), testHoverConfig("ref", "string", "The branch, tag or SHA to checkout.")); + expect(result).not.toBeUndefined(); + expect(result?.contents).toEqual("The branch, tag or SHA to checkout."); + }); + + it("falls back to the token description", async () => { + const input = ` +on: push +jobs: + build: + runs-on: [self-hosted] + steps: + - uses|: actions/checkout@v2 +`; + + const result = await hover(...getPositionFromCursor(input), testHoverConfig("uses", "non-empty-string", undefined)); + expect(result).not.toBeUndefined(); + expect(result?.contents).toEqual("Selects an action to run as part of a step in your job. An action is a reusable unit of code. You can use an action defined in the same repository as the workflow, a public repository, or in a published Docker container image."); + }); +}); diff --git a/actions-languageservice/src/hover.ts b/actions-languageservice/src/hover.ts index 7d39495..4d5ebdd 100644 --- a/actions-languageservice/src/hover.ts +++ b/actions-languageservice/src/hover.ts @@ -1,49 +1,67 @@ -import {parseWorkflow} from "@github/actions-workflow-parser"; +import {convertWorkflowTemplate, parseWorkflow, ParseWorkflowResult} from "@github/actions-workflow-parser"; +import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert"; import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token"; import {File} from "@github/actions-workflow-parser/workflows/file"; import {Position, TextDocument} from "vscode-languageserver-textdocument"; import {Hover} from "vscode-languageserver-types"; +import {getWorkflowContext, WorkflowContext} from "./context/workflow-context"; import {info} from "./log"; import {nullTrace} from "./nulltrace"; import {findToken} from "./utils/find-token"; import {mapRange} from "./utils/range"; +export type DescriptionProvider = { + getDescription(context: WorkflowContext, token: TemplateToken, path: TemplateToken[]): Promise; +}; + +export type HoverConfig = { + descriptionProvider?: DescriptionProvider; +}; + // Render value description and Context when hovering over a key in a MappingToken -export async function hover(document: TextDocument, position: Position): Promise { +export async function hover(document: TextDocument, position: Position, config?: HoverConfig): Promise { const file: File = { name: document.uri, content: document.getText() }; const result = parseWorkflow(file.name, [file], nullTrace); - const {token} = findToken(position, result.value); - - if (result.value && token) { - return getHover(token); + const {token, path} = findToken(position, result.value); + if (!token?.definition) { + return null; } - return null; + + info(`Calculating hover for token with definition ${token.definition.key}`); + + let description = await getDescription(document, config, result, token, path); + + if (token.definition.evaluatorContext.length > 0) { + // Only add padding if there is a description + description += `${description.length > 0 ? `\n\n` : ""}**Context:** ${token.definition.evaluatorContext.join( + ", " + )}`; + } + + return { + contents: description, + range: mapRange(token.range) + } satisfies Hover; } -function getHover(token: TemplateToken): Hover | null { - if (token.definition) { - info(`Calculating hover for token with definition ${token.definition.key}`); - - let description = ""; - if (token.description) { - description = token.description; - } - - if (token.definition.evaluatorContext.length > 0) { - // Only add padding if there is a description - description += `${description.length > 0 ? `\n\n` : ""}**Context:** ${token.definition.evaluatorContext.join( - ", " - )}`; - } - - return { - contents: description, - range: mapRange(token.range) - } as Hover; +async function getDescription( + document: TextDocument, + config: HoverConfig | undefined, + result: ParseWorkflowResult | undefined, + token: TemplateToken, + path: TemplateToken[] +) { + const defaultDescription = token.description || ""; + if (!result?.value || !config?.descriptionProvider) { + return defaultDescription; } - return null; + + const template = convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion); + const workflowContext = getWorkflowContext(document.uri, template, path); + const description = await config.descriptionProvider.getDescription(workflowContext, token, path); + return description || defaultDescription; } diff --git a/actions-languageservice/src/value-providers/config.ts b/actions-languageservice/src/value-providers/config.ts index 916a569..40b88df 100644 --- a/actions-languageservice/src/value-providers/config.ts +++ b/actions-languageservice/src/value-providers/config.ts @@ -4,7 +4,7 @@ export interface Value { /** Label of this value */ label: string; - /** Optional description to show when auto-completing or hovering */ + /** Optional description to show when auto-completing */ description?: string; /** Whether this value is deprecated */ From 4a9fe401aff099130855409aca441c3f4876d4cf Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Mon, 23 Jan 2023 19:39:28 -0500 Subject: [PATCH 2/3] Include deprecated and required input status --- .../src/description-providers/action-input.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/actions-languageserver/src/description-providers/action-input.ts b/actions-languageserver/src/description-providers/action-input.ts index 1f26abf..a6b1385 100644 --- a/actions-languageserver/src/description-providers/action-input.ts +++ b/actions-languageserver/src/description-providers/action-input.ts @@ -31,5 +31,23 @@ export async function getActionInputDescription( return undefined; } - return metadata.inputs[inputName]?.description; + const input = metadata.inputs[inputName]; + if (!input) { + return undefined; + } + + let description = input.description; + + const deprecated = input.deprecationMessage !== undefined; + + if (deprecated) { + // Validation will include the deprecation message, so don't duplicate it here + description += `\n\n**Deprecated**`; + } + + if (input.required) { + description += "\n\n**Required**"; + } + + return description; } From 1f3f130a4d11e55423435fc8840b0c80e973e602 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 24 Jan 2023 00:44:38 +0000 Subject: [PATCH 3/3] v0.1.92 --- actions-expressions/package.json | 2 +- actions-languageserver/package.json | 6 ++--- actions-languageservice/package.json | 6 ++--- actions-workflow-parser/package.json | 4 ++-- browser-playground/package.json | 4 ++-- lerna.json | 2 +- package-lock.json | 34 ++++++++++++++-------------- 7 files changed, 29 insertions(+), 29 deletions(-) diff --git a/actions-expressions/package.json b/actions-expressions/package.json index 52f6bd9..57376e9 100755 --- a/actions-expressions/package.json +++ b/actions-expressions/package.json @@ -1,6 +1,6 @@ { "name": "@github/actions-expressions", - "version": "0.1.91", + "version": "0.1.92", "license": "MIT", "type": "module", "source": "./src/index.ts", diff --git a/actions-languageserver/package.json b/actions-languageserver/package.json index 2f373be..d594df9 100644 --- a/actions-languageserver/package.json +++ b/actions-languageserver/package.json @@ -1,6 +1,6 @@ { "name": "@github/actions-languageserver", - "version": "0.1.91", + "version": "0.1.92", "description": "Language server for GitHub Actions", "license": "MIT", "type": "module", @@ -38,8 +38,8 @@ "watch": "tsc --build tsconfig.build.json --watch" }, "dependencies": { - "@github/actions-languageservice": "^0.1.91", - "@github/actions-workflow-parser": "^0.1.91", + "@github/actions-languageservice": "^0.1.92", + "@github/actions-workflow-parser": "^0.1.92", "@octokit/rest": "^19.0.5", "vscode-languageserver": "^8.0.2", "vscode-languageserver-textdocument": "^1.0.7", diff --git a/actions-languageservice/package.json b/actions-languageservice/package.json index 3278d3b..fa3e3c6 100644 --- a/actions-languageservice/package.json +++ b/actions-languageservice/package.json @@ -1,6 +1,6 @@ { "name": "@github/actions-languageservice", - "version": "0.1.91", + "version": "0.1.92", "description": "Language service for GitHub Actions", "license": "MIT", "type": "module", @@ -38,8 +38,8 @@ "watch": "tsc --build tsconfig.build.json --watch" }, "dependencies": { - "@github/actions-expressions": "^0.1.91", - "@github/actions-workflow-parser": "^0.1.91", + "@github/actions-expressions": "^0.1.92", + "@github/actions-workflow-parser": "^0.1.92", "vscode-languageserver-textdocument": "^1.0.7", "vscode-languageserver-types": "^3.17.2", "yaml": "^2.1.1" diff --git a/actions-workflow-parser/package.json b/actions-workflow-parser/package.json index 9302656..4a525b8 100644 --- a/actions-workflow-parser/package.json +++ b/actions-workflow-parser/package.json @@ -1,6 +1,6 @@ { "name": "@github/actions-workflow-parser", - "version": "0.1.91", + "version": "0.1.92", "license": "MIT", "type": "module", "source": "./src/index.ts", @@ -40,7 +40,7 @@ "watch": "tsc --build tsconfig.build.json --watch" }, "dependencies": { - "@github/actions-expressions": "^0.1.91", + "@github/actions-expressions": "^0.1.92", "yaml": "^2.0.0-8" }, "engines": { diff --git a/browser-playground/package.json b/browser-playground/package.json index d2d32bb..d5656ad 100644 --- a/browser-playground/package.json +++ b/browser-playground/package.json @@ -1,12 +1,12 @@ { "name": "browser-playground", - "version": "0.1.91", + "version": "0.1.92", "description": "", "private": true, "main": "index.js", "type": "module", "dependencies": { - "@github/actions-languageserver": "^0.1.91", + "@github/actions-languageserver": "^0.1.92", "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 39120ec..99258c8 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", "useWorkspaces": true, - "version": "0.1.91" + "version": "0.1.92" } diff --git a/package-lock.json b/package-lock.json index 3009b6f..0cb2e1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ }, "actions-expressions": { "name": "@github/actions-expressions", - "version": "0.1.91", + "version": "0.1.92", "license": "MIT", "devDependencies": { "@types/jest": "^29.0.3", @@ -34,11 +34,11 @@ }, "actions-languageserver": { "name": "@github/actions-languageserver", - "version": "0.1.91", + "version": "0.1.92", "license": "MIT", "dependencies": { - "@github/actions-languageservice": "^0.1.91", - "@github/actions-workflow-parser": "^0.1.91", + "@github/actions-languageservice": "^0.1.92", + "@github/actions-workflow-parser": "^0.1.92", "@octokit/rest": "^19.0.5", "vscode-languageserver": "^8.0.2", "vscode-languageserver-textdocument": "^1.0.7", @@ -59,11 +59,11 @@ }, "actions-languageservice": { "name": "@github/actions-languageservice", - "version": "0.1.91", + "version": "0.1.92", "license": "MIT", "dependencies": { - "@github/actions-expressions": "^0.1.91", - "@github/actions-workflow-parser": "^0.1.91", + "@github/actions-expressions": "^0.1.92", + "@github/actions-workflow-parser": "^0.1.92", "vscode-languageserver-textdocument": "^1.0.7", "vscode-languageserver-types": "^3.17.2", "yaml": "^2.1.1" @@ -82,10 +82,10 @@ }, "actions-workflow-parser": { "name": "@github/actions-workflow-parser", - "version": "0.1.91", + "version": "0.1.92", "license": "MIT", "dependencies": { - "@github/actions-expressions": "^0.1.91", + "@github/actions-expressions": "^0.1.92", "yaml": "^2.0.0-8" }, "devDependencies": { @@ -104,10 +104,10 @@ } }, "browser-playground": { - "version": "0.1.91", + "version": "0.1.92", "license": "MIT", "dependencies": { - "@github/actions-languageserver": "^0.1.91", + "@github/actions-languageserver": "^0.1.92", "monaco-editor-webpack-plugin": "^7.0.1", "monaco-editor-workers": "^0.34.2", "monaco-languageclient": "^4.0.3", @@ -14428,8 +14428,8 @@ "@github/actions-languageserver": { "version": "file:actions-languageserver", "requires": { - "@github/actions-languageservice": "^0.1.91", - "@github/actions-workflow-parser": "^0.1.91", + "@github/actions-languageservice": "^0.1.92", + "@github/actions-workflow-parser": "^0.1.92", "@octokit/rest": "^19.0.5", "@types/jest": "^29.0.3", "jest": "^29.0.3", @@ -14446,8 +14446,8 @@ "@github/actions-languageservice": { "version": "file:actions-languageservice", "requires": { - "@github/actions-expressions": "^0.1.91", - "@github/actions-workflow-parser": "^0.1.91", + "@github/actions-expressions": "^0.1.92", + "@github/actions-workflow-parser": "^0.1.92", "@types/jest": "^29.0.3", "jest": "^29.0.3", "prettier": "^2.7.1", @@ -14462,7 +14462,7 @@ "@github/actions-workflow-parser": { "version": "file:actions-workflow-parser", "requires": { - "@github/actions-expressions": "^0.1.91", + "@github/actions-expressions": "^0.1.92", "@types/jest": "^29.0.3", "@typescript-eslint/eslint-plugin": "^5.40.0", "@typescript-eslint/parser": "^5.40.0", @@ -17389,7 +17389,7 @@ "browser-playground": { "version": "file:browser-playground", "requires": { - "@github/actions-languageserver": "^0.1.91", + "@github/actions-languageserver": "^0.1.92", "css-loader": "^6.7.2", "monaco-editor-webpack-plugin": "^7.0.1", "monaco-editor-workers": "^0.34.2",