From 381d04730ee00dfeee067de7d17ffd21ca8618eb Mon Sep 17 00:00:00 2001 From: Beth Brennan Date: Wed, 1 Mar 2023 11:10:35 -0500 Subject: [PATCH 1/8] Complete reusable workflow secrets, handle placeholder key --- .../src/complete.reusable-workflows.test.ts | 73 ++++++++++++++++++- languageservice/src/complete.ts | 8 +- .../src/test-utils/test-file-provider.ts | 7 ++ languageservice/src/utils/transform.ts | 12 +++ .../src/validate.workflow-references.test.ts | 4 + languageservice/src/value-providers/config.ts | 2 +- .../src/value-providers/default.ts | 5 ++ .../value-providers/reusable-job-secrets.ts | 53 ++++++++++++++ 8 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 languageservice/src/value-providers/reusable-job-secrets.ts diff --git a/languageservice/src/complete.reusable-workflows.test.ts b/languageservice/src/complete.reusable-workflows.test.ts index 98fb858..85ef204 100644 --- a/languageservice/src/complete.reusable-workflows.test.ts +++ b/languageservice/src/complete.reusable-workflows.test.ts @@ -5,7 +5,7 @@ import {testFileProvider} from "./test-utils/test-file-provider"; function mapResult(result: CompletionItem[]) { return result.map(x => { - return {label: x.label, description: (x.documentation as MarkupContent).value}; + return {label: x.label, description: (x.documentation as MarkupContent)?.value}; }); } @@ -24,6 +24,9 @@ jobs: expect(result).not.toBeUndefined(); expect(mapResult(result)).toEqual([ + { + label: "key" + }, { label: "name", description: "An optional name" @@ -50,10 +53,78 @@ jobs: expect(result).not.toBeUndefined(); expect(mapResult(result)).toEqual([ + { + label: "key" + }, { label: "name", description: "An optional name" } ]); }); + + it("completes job secrets", async () => { + const input = ` +on: push + +jobs: + build: + uses: ./reusable-workflow-with-inputs.yaml + secrets: + | +`; + const result = await complete(...getPositionFromCursor(input), {fileProvider: testFileProvider}); + + expect(result).not.toBeUndefined(); + // includes inherit since no secrets have been passed in + expect(mapResult(result)).toEqual([ + { + label: "envPAT", + description: "A secret for the environment" + }, + { + label: "inherit" + }, + { + label: "serverPAT" + } + ]); + }); + + it("completes inherit secrets", async () => { + const input = ` +on: push + +jobs: + build: + uses: ./reusable-workflow-with-inputs.yaml + secrets: | +`; + const result = await complete(...getPositionFromCursor(input), {fileProvider: testFileProvider}); + + expect(result).not.toBeUndefined(); + //TODO contains instead of equals + expect(mapResult(result)).toContainEqual({label: "inherit"}); + }); + + it("filters existing secrets", async () => { + const input = ` +on: push + +jobs: + build: + uses: ./reusable-workflow-with-inputs.yaml + secrets: + envPAT: "myPAT" + | +`; + const result = await complete(...getPositionFromCursor(input), {fileProvider: testFileProvider}); + + expect(result).not.toBeUndefined(); + expect(mapResult(result)).toEqual([ + { + label: "serverPAT" + } + ]); + }); }); diff --git a/languageservice/src/complete.ts b/languageservice/src/complete.ts index 8050570..5489c63 100644 --- a/languageservice/src/complete.ts +++ b/languageservice/src/complete.ts @@ -27,7 +27,7 @@ import {findToken} from "./utils/find-token"; import {guessIndentation} from "./utils/indentation-guesser"; import {mapRange} from "./utils/range"; import {getRelCharOffset} from "./utils/rel-char-pos"; -import {transform} from "./utils/transform"; +import {isPlaceholder, transform} from "./utils/transform"; import {Value, ValueProviderConfig} from "./value-providers/config"; import {defaultValueProviders} from "./value-providers/default"; import {definitionValues} from "./value-providers/definition"; @@ -157,7 +157,7 @@ async function getValues( const defaultValueProvider = valueProviderToken?.definition?.key && defaultValueProviders[valueProviderToken.definition.key]; if (defaultValueProvider) { - const values = await defaultValueProvider.get(workflowContext); + const values = await defaultValueProvider.get(workflowContext, existingValues); return filterAndSortCompletionOptions(values, existingValues); } @@ -197,8 +197,8 @@ export function getExistingValues(token: TemplateToken | null, parent: TemplateT const mapKeys = new Set(); const mapToken = parent as MappingToken; - for (const {key} of mapToken) { - if (isString(key)) { + for (const {key, value} of mapToken) { + if (isString(key) && !isPlaceholder(key, value)) { mapKeys.add(key.value); } } diff --git a/languageservice/src/test-utils/test-file-provider.ts b/languageservice/src/test-utils/test-file-provider.ts index 99d075d..d80469d 100644 --- a/languageservice/src/test-utils/test-file-provider.ts +++ b/languageservice/src/test-utils/test-file-provider.ts @@ -59,6 +59,13 @@ on: description: 'An optional name' required: false type: string + key: + type: string + secrets: + envPAT: + required: true + description: 'A secret for the environment' + serverPAT: jobs: build: diff --git a/languageservice/src/utils/transform.ts b/languageservice/src/utils/transform.ts index a71ef9e..d3e2a78 100644 --- a/languageservice/src/utils/transform.ts +++ b/languageservice/src/utils/transform.ts @@ -1,3 +1,6 @@ +import {isString} from "@github/actions-workflow-parser"; +import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token"; +import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token"; import {Position, TextDocument} from "vscode-languageserver-textdocument"; import {Range} from "vscode-languageserver-types"; @@ -72,3 +75,12 @@ export function transform(doc: TextDocument, pos: Position): [TextDocument, Posi return [newDoc, newDoc.positionAt(offset)]; } + +// Detect placeholder key and value added by transform +export function isPlaceholder(key: StringToken, value: TemplateToken) { + if (key.value === PLACEHOLDER_KEY && isString(value) && value.value == "") { + return true; + } + + return false; +} diff --git a/languageservice/src/validate.workflow-references.test.ts b/languageservice/src/validate.workflow-references.test.ts index 48614cb..3532840 100644 --- a/languageservice/src/validate.workflow-references.test.ts +++ b/languageservice/src/validate.workflow-references.test.ts @@ -98,6 +98,8 @@ on: push jobs: build: uses: ./reusable-workflow-with-inputs.yaml + secrets: + envPAT: pat `; const result = await validate(createDocument("wf.yaml", input), { fileProvider: testFileProvider @@ -129,6 +131,8 @@ jobs: uses: ./reusable-workflow-with-inputs.yaml with: username: monalisa + secrets: + envPAT: pat `; const result = await validate(createDocument("wf.yaml", input), { fileProvider: testFileProvider diff --git a/languageservice/src/value-providers/config.ts b/languageservice/src/value-providers/config.ts index 40b88df..8531651 100644 --- a/languageservice/src/value-providers/config.ts +++ b/languageservice/src/value-providers/config.ts @@ -21,7 +21,7 @@ export enum ValueProviderKind { export type ValueProvider = { kind: ValueProviderKind; - get: (context: WorkflowContext) => Promise; + get: (context: WorkflowContext, existingValues?: Set) => Promise; }; export interface ValueProviderConfig { diff --git a/languageservice/src/value-providers/default.ts b/languageservice/src/value-providers/default.ts index 8081f45..693d506 100644 --- a/languageservice/src/value-providers/default.ts +++ b/languageservice/src/value-providers/default.ts @@ -2,6 +2,7 @@ import {WorkflowContext} from "../context/workflow-context"; import {ValueProviderConfig, ValueProviderKind} from "./config"; import {needs} from "./needs"; import {reusableJobInputs} from "./reusable-job-inputs"; +import {reusableJobSecrets} from "./reusable-job-secrets"; import {stringsToValues} from "./strings-to-values"; export const DEFAULT_RUNNER_LABELS = [ @@ -28,6 +29,10 @@ export const defaultValueProviders: ValueProviderConfig = { kind: ValueProviderKind.AllowedValues, get: async context => reusableJobInputs(context) }, + "workflow-job-secrets": { + kind: ValueProviderKind.SuggestedValues, + get: async (context, existingValues) => reusableJobSecrets(context, existingValues) + }, "runs-on": { kind: ValueProviderKind.SuggestedValues, get: async (_: WorkflowContext) => stringsToValues(DEFAULT_RUNNER_LABELS) diff --git a/languageservice/src/value-providers/reusable-job-secrets.ts b/languageservice/src/value-providers/reusable-job-secrets.ts new file mode 100644 index 0000000..9eeb45f --- /dev/null +++ b/languageservice/src/value-providers/reusable-job-secrets.ts @@ -0,0 +1,53 @@ +import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token"; +import {isMapping, isString} from "@github/actions-workflow-parser/templates/tokens/type-guards"; +import {WorkflowContext} from "../context/workflow-context"; +import {Value} from "./config"; + +export function reusableJobSecrets(context: WorkflowContext, existingValues?: Set): Value[] { + if (!context.reusableWorkflowJob) { + return []; + } + + const values: Value[] = []; + + const inheritSecrets = context.reusableWorkflowJob["inherit-secrets"]; + if (inheritSecrets) { + return values; + } + + // Suggest inherit if no other secrets have been set + if (!existingValues || existingValues.size === 0) { + values.push({ + label: "inherit" + }); + } + + if (context.reusableWorkflowJob?.["secret-definitions"]) { + for (const {key, value} of context.reusableWorkflowJob["secret-definitions"]) { + if (!isString(key)) { + continue; + } + + values.push({ + label: key.value, + description: secretDescription(value), + insertText: `${key.value}: ` + }); + } + } + + return values; +} + +function secretDescription(secretDef: TemplateToken): string | undefined { + if (!isMapping(secretDef)) { + return ""; + } + + const descriptionToken = secretDef.find("description"); + if (!descriptionToken || !isString(descriptionToken)) { + return ""; + } + + return descriptionToken.value; +} From f3acb5b573ed23aaf67adffabc90412f3dbadb20 Mon Sep 17 00:00:00 2001 From: Beth Brennan Date: Wed, 1 Mar 2023 12:58:40 -0500 Subject: [PATCH 2/8] Handle erros while converting job --- workflow-parser/src/model/converter/job.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/workflow-parser/src/model/converter/job.ts b/workflow-parser/src/model/converter/job.ts index a9d5163..6d650f6 100644 --- a/workflow-parser/src/model/converter/job.ts +++ b/workflow-parser/src/model/converter/job.ts @@ -1,6 +1,6 @@ import {TemplateContext} from "../../templates/template-context"; import {BasicExpressionToken, MappingToken, ScalarToken, StringToken, TemplateToken} from "../../templates/tokens"; -import {isSequence, isString} from "../../templates/tokens/type-guards"; +import {isMapping, isSequence, isString} from "../../templates/tokens/type-guards"; import {Step, WorkflowJob} from "../workflow-template"; import {convertConcurrency} from "./concurrency"; import {convertToJobContainer, convertToJobServices} from "./container"; @@ -38,7 +38,9 @@ export function convertJob(context: TemplateContext, jobKey: StringToken, token: break; case "env": - env = item.value.assertMapping("job env"); + handleTemplateTokenErrors(item.value, context, undefined, () => { + env = item.value.assertMapping("job env"); + }); break; case "environment": @@ -69,7 +71,9 @@ export function convertJob(context: TemplateContext, jobKey: StringToken, token: } case "outputs": - outputs = item.value.assertMapping("job outputs"); + handleTemplateTokenErrors(item.value, context, undefined, () => { + outputs = item.value.assertMapping("job outputs"); + }); break; case "runs-on": @@ -95,14 +99,19 @@ export function convertJob(context: TemplateContext, jobKey: StringToken, token: break; case "with": - workflowJobInputs = item.value.assertMapping("uses-with value"); + handleTemplateTokenErrors(item.value, context, undefined, () => { + workflowJobInputs = item.value.assertMapping("uses-with value"); + }); break; case "secrets": if (isString(item.value) && item.value.value === "inherit") { inheritSecrets = true; } else { - workflowJobSecrets = item.value.assertMapping("uses-secrets value"); + handleTemplateTokenErrors(item.value, context, undefined, () => { + workflowJobSecrets = item.value.assertMapping("uses-secrets value"); + }); } + break; } } From da5f864901d0eea0576a309e167a68b612c3de2d Mon Sep 17 00:00:00 2001 From: Beth Brennan Date: Wed, 1 Mar 2023 14:37:10 -0500 Subject: [PATCH 3/8] Insert colon in inputs --- languageservice/src/value-providers/reusable-job-inputs.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/languageservice/src/value-providers/reusable-job-inputs.ts b/languageservice/src/value-providers/reusable-job-inputs.ts index 391d5a8..7611b65 100644 --- a/languageservice/src/value-providers/reusable-job-inputs.ts +++ b/languageservice/src/value-providers/reusable-job-inputs.ts @@ -19,7 +19,8 @@ export function reusableJobInputs(context: WorkflowContext): Value[] { values.push({ label: key.value, - description: inputDescription(value) + description: inputDescription(value), + insertText: `${key.value}: ` }); } From 203e5195887f19fe130d1b57745aa87b6b2a25c3 Mon Sep 17 00:00:00 2001 From: Beth Brennan Date: Wed, 1 Mar 2023 14:43:51 -0500 Subject: [PATCH 4/8] Remove todo --- languageservice/src/complete.reusable-workflows.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/languageservice/src/complete.reusable-workflows.test.ts b/languageservice/src/complete.reusable-workflows.test.ts index 85ef204..92b02c9 100644 --- a/languageservice/src/complete.reusable-workflows.test.ts +++ b/languageservice/src/complete.reusable-workflows.test.ts @@ -103,7 +103,6 @@ jobs: const result = await complete(...getPositionFromCursor(input), {fileProvider: testFileProvider}); expect(result).not.toBeUndefined(); - //TODO contains instead of equals expect(mapResult(result)).toContainEqual({label: "inherit"}); }); From 20170641a59970971311155b1ee2b67dc7e71024 Mon Sep 17 00:00:00 2001 From: Beth Brennan Date: Tue, 7 Mar 2023 15:36:47 -0500 Subject: [PATCH 5/8] Pass existingValues consistently --- languageservice/src/complete.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languageservice/src/complete.ts b/languageservice/src/complete.ts index dac1bab..6aa9c42 100644 --- a/languageservice/src/complete.ts +++ b/languageservice/src/complete.ts @@ -160,7 +160,7 @@ async function getValues( const customValueProvider = valueProviderToken?.definition?.key && valueProviderConfig?.[valueProviderToken.definition.key]; if (customValueProvider) { - const customValues = await customValueProvider.get(workflowContext); + const customValues = await customValueProvider.get(workflowContext, existingValues); if (customValues) { return filterAndSortCompletionOptions(customValues, existingValues); } From 710386eb334d5752c02511809e687e57bd2e1549 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 7 Mar 2023 20:57:46 +0000 Subject: [PATCH 6/8] v0.1.158 --- browser-playground/package.json | 4 ++-- expressions/package.json | 2 +- languageserver/package.json | 6 +++--- languageservice/package.json | 6 +++--- lerna.json | 2 +- package-lock.json | 34 ++++++++++++++++----------------- workflow-parser/package.json | 4 ++-- 7 files changed, 29 insertions(+), 29 deletions(-) diff --git a/browser-playground/package.json b/browser-playground/package.json index 067a069..a8e16f0 100644 --- a/browser-playground/package.json +++ b/browser-playground/package.json @@ -1,12 +1,12 @@ { "name": "browser-playground", - "version": "0.1.157", + "version": "0.1.158", "description": "", "private": true, "main": "index.js", "type": "module", "dependencies": { - "@github/actions-languageserver": "^0.1.157", + "@github/actions-languageserver": "^0.1.158", "monaco-editor-webpack-plugin": "^7.0.1", "monaco-editor-workers": "^0.34.2", "monaco-languageclient": "^4.0.3", diff --git a/expressions/package.json b/expressions/package.json index 8f1613b..553ece9 100755 --- a/expressions/package.json +++ b/expressions/package.json @@ -1,6 +1,6 @@ { "name": "@github/actions-expressions", - "version": "0.1.157", + "version": "0.1.158", "license": "MIT", "type": "module", "source": "./src/index.ts", diff --git a/languageserver/package.json b/languageserver/package.json index cfb19f8..d6bed10 100644 --- a/languageserver/package.json +++ b/languageserver/package.json @@ -1,6 +1,6 @@ { "name": "@github/actions-languageserver", - "version": "0.1.157", + "version": "0.1.158", "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.157", - "@github/actions-workflow-parser": "^0.1.157", + "@github/actions-languageservice": "^0.1.158", + "@github/actions-workflow-parser": "^0.1.158", "@octokit/rest": "^19.0.7", "vscode-languageserver": "^8.0.2", "vscode-languageserver-textdocument": "^1.0.7", diff --git a/languageservice/package.json b/languageservice/package.json index 9aa131b..c554144 100644 --- a/languageservice/package.json +++ b/languageservice/package.json @@ -1,6 +1,6 @@ { "name": "@github/actions-languageservice", - "version": "0.1.157", + "version": "0.1.158", "description": "Language service for GitHub Actions", "license": "MIT", "type": "module", @@ -39,8 +39,8 @@ "watch": "tsc --build tsconfig.build.json --watch" }, "dependencies": { - "@github/actions-expressions": "^0.1.157", - "@github/actions-workflow-parser": "^0.1.157", + "@github/actions-expressions": "^0.1.158", + "@github/actions-workflow-parser": "^0.1.158", "vscode-languageserver-textdocument": "^1.0.7", "vscode-languageserver-types": "^3.17.2", "yaml": "^2.1.1" diff --git a/lerna.json b/lerna.json index 0135705..b5b0444 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", "useWorkspaces": true, - "version": "0.1.157" + "version": "0.1.158" } diff --git a/package-lock.json b/package-lock.json index 36b8fee..e518d02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -111,10 +111,10 @@ } }, "browser-playground": { - "version": "0.1.157", + "version": "0.1.158", "license": "MIT", "dependencies": { - "@github/actions-languageserver": "^0.1.157", + "@github/actions-languageserver": "^0.1.158", "monaco-editor-webpack-plugin": "^7.0.1", "monaco-editor-workers": "^0.34.2", "monaco-languageclient": "^4.0.3", @@ -135,7 +135,7 @@ }, "expressions": { "name": "@github/actions-expressions", - "version": "0.1.157", + "version": "0.1.158", "license": "MIT", "devDependencies": { "@types/jest": "^29.0.3", @@ -151,11 +151,11 @@ }, "languageserver": { "name": "@github/actions-languageserver", - "version": "0.1.157", + "version": "0.1.158", "license": "MIT", "dependencies": { - "@github/actions-languageservice": "^0.1.157", - "@github/actions-workflow-parser": "^0.1.157", + "@github/actions-languageservice": "^0.1.158", + "@github/actions-workflow-parser": "^0.1.158", "@octokit/rest": "^19.0.7", "vscode-languageserver": "^8.0.2", "vscode-languageserver-textdocument": "^1.0.7", @@ -176,11 +176,11 @@ }, "languageservice": { "name": "@github/actions-languageservice", - "version": "0.1.157", + "version": "0.1.158", "license": "MIT", "dependencies": { - "@github/actions-expressions": "^0.1.157", - "@github/actions-workflow-parser": "^0.1.157", + "@github/actions-expressions": "^0.1.158", + "@github/actions-workflow-parser": "^0.1.158", "vscode-languageserver-textdocument": "^1.0.7", "vscode-languageserver-types": "^3.17.2", "yaml": "^2.1.1" @@ -14265,10 +14265,10 @@ }, "workflow-parser": { "name": "@github/actions-workflow-parser", - "version": "0.1.157", + "version": "0.1.158", "license": "MIT", "dependencies": { - "@github/actions-expressions": "^0.1.157", + "@github/actions-expressions": "^0.1.158", "cronstrue": "^2.21.0", "yaml": "^2.0.0-8" }, @@ -14827,8 +14827,8 @@ "@github/actions-languageserver": { "version": "file:languageserver", "requires": { - "@github/actions-languageservice": "^0.1.157", - "@github/actions-workflow-parser": "^0.1.157", + "@github/actions-languageservice": "^0.1.158", + "@github/actions-workflow-parser": "^0.1.158", "@octokit/rest": "^19.0.7", "@types/jest": "^29.0.3", "fetch-mock": "^9.11.0", @@ -14845,8 +14845,8 @@ "@github/actions-languageservice": { "version": "file:languageservice", "requires": { - "@github/actions-expressions": "^0.1.157", - "@github/actions-workflow-parser": "^0.1.157", + "@github/actions-expressions": "^0.1.158", + "@github/actions-workflow-parser": "^0.1.158", "@types/jest": "^29.0.3", "jest": "^29.0.3", "prettier": "^2.8.3", @@ -14863,7 +14863,7 @@ "@github/actions-workflow-parser": { "version": "file:workflow-parser", "requires": { - "@github/actions-expressions": "^0.1.157", + "@github/actions-expressions": "^0.1.158", "@types/jest": "^29.0.3", "@typescript-eslint/eslint-plugin": "^5.40.0", "@typescript-eslint/parser": "^5.40.0", @@ -17857,7 +17857,7 @@ "browser-playground": { "version": "file:browser-playground", "requires": { - "@github/actions-languageserver": "^0.1.157", + "@github/actions-languageserver": "^0.1.158", "css-loader": "^6.7.2", "monaco-editor-webpack-plugin": "^7.0.1", "monaco-editor-workers": "^0.34.2", diff --git a/workflow-parser/package.json b/workflow-parser/package.json index d4b0e26..27ca78d 100644 --- a/workflow-parser/package.json +++ b/workflow-parser/package.json @@ -1,6 +1,6 @@ { "name": "@github/actions-workflow-parser", - "version": "0.1.157", + "version": "0.1.158", "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.157", + "@github/actions-expressions": "^0.1.158", "cronstrue": "^2.21.0", "yaml": "^2.0.0-8" }, From 47250a4ac8356a01648650a26a748902c7ef5f6b Mon Sep 17 00:00:00 2001 From: Laura Yu <60276246+lauraway@users.noreply.github.com> Date: Tue, 7 Mar 2023 13:21:59 -0800 Subject: [PATCH 7/8] Check with repo-permission before accessing secrets and variables (#175) --- .../src/context-providers/secrets.ts | 95 ++++++++++--------- .../src/context-providers/variables.ts | 95 ++++++++++--------- languageserver/src/utils/repo-permission.ts | 46 +++++++++ languageserver/src/utils/username.ts | 16 ++++ 4 files changed, 160 insertions(+), 92 deletions(-) create mode 100644 languageserver/src/utils/repo-permission.ts create mode 100644 languageserver/src/utils/username.ts diff --git a/languageserver/src/context-providers/secrets.ts b/languageserver/src/context-providers/secrets.ts index 217b417..c5cf4b3 100644 --- a/languageserver/src/context-providers/secrets.ts +++ b/languageserver/src/context-providers/secrets.ts @@ -5,6 +5,7 @@ import {isMapping, isString} from "@github/actions-workflow-parser"; import {Octokit} from "@octokit/rest"; import {RepositoryContext} from "../initializationOptions"; import {TTLCache} from "../utils/cache"; +import {getRepoPermission} from "../utils/repo-permission"; export async function getSecrets( workflowContext: WorkflowContext, @@ -13,6 +14,13 @@ export async function getSecrets( repo: RepositoryContext, defaultContext: DescriptionDictionary | undefined ): Promise { + const permission = await getRepoPermission(octokit, cache, repo); + if (permission === "none") { + const secretsContext = defaultContext || new DescriptionDictionary(); + secretsContext.complete = false; + return secretsContext; + } + let environmentName: string | undefined; if (workflowContext?.job?.environment) { if (isString(workflowContext.job.environment)) { @@ -30,54 +38,49 @@ export async function getSecrets( } const secretsContext = defaultContext || new DescriptionDictionary(); - try { - const secrets = await getRemoteSecrets(octokit, cache, repo, environmentName); + const secrets = await getRemoteSecrets(octokit, cache, repo, environmentName); - // Build combined map of secrets - const secretsMap = new Map< - string, - { - key: string; - value: data.StringData; - description?: string; - } - >(); - - secrets.orgSecrets.forEach(secret => - secretsMap.set(secret.value.toLowerCase(), { - key: secret.value, - value: new data.StringData("***"), - description: "Organization secret" - }) - ); - - // Override org secrets with repo secrets - secrets.repoSecrets.forEach(secret => - secretsMap.set(secret.value.toLowerCase(), { - key: secret.value, - value: new data.StringData("***"), - description: "Repository secret" - }) - ); - - // Override repo secrets with environment secrets (if defined) - secrets.environmentSecrets.forEach(secret => - secretsMap.set(secret.value.toLowerCase(), { - key: secret.value, - value: new data.StringData("***"), - description: `Secret for environment \`${environmentName}\`` - }) - ); - - // Sort secrets by key and add to context - Array.from(secretsMap.values()) - .sort((a, b) => a.key.localeCompare(b.key)) - .forEach(secret => secretsContext?.add(secret.key, secret.value, secret.description)); - } catch (e: any) { - if (e.status === 403 || e.status === 404) { - secretsContext.complete = false; + // Build combined map of secrets + const secretsMap = new Map< + string, + { + key: string; + value: data.StringData; + description?: string; } - } + >(); + + secrets.orgSecrets.forEach(secret => + secretsMap.set(secret.value.toLowerCase(), { + key: secret.value, + value: new data.StringData("***"), + description: "Organization secret" + }) + ); + + // Override org secrets with repo secrets + secrets.repoSecrets.forEach(secret => + secretsMap.set(secret.value.toLowerCase(), { + key: secret.value, + value: new data.StringData("***"), + description: "Repository secret" + }) + ); + + // Override repo secrets with environment secrets (if defined) + secrets.environmentSecrets.forEach(secret => + secretsMap.set(secret.value.toLowerCase(), { + key: secret.value, + value: new data.StringData("***"), + description: `Secret for environment \`${environmentName}\`` + }) + ); + + // Sort secrets by key and add to context + Array.from(secretsMap.values()) + .sort((a, b) => a.key.localeCompare(b.key)) + .forEach(secret => secretsContext?.add(secret.key, secret.value, secret.description)); + return secretsContext; } diff --git a/languageserver/src/context-providers/variables.ts b/languageserver/src/context-providers/variables.ts index f78c650..aa3121b 100644 --- a/languageserver/src/context-providers/variables.ts +++ b/languageserver/src/context-providers/variables.ts @@ -6,6 +6,7 @@ import {Pair} from "@github/actions-expressions/data/expressiondata"; import {RepositoryContext} from "../initializationOptions"; import {StringData} from "@github/actions-expressions/data/index"; import {TTLCache} from "../utils/cache"; +import {getRepoPermission} from "../utils/repo-permission"; export async function getVariables( workflowContext: WorkflowContext, @@ -14,6 +15,13 @@ export async function getVariables( repo: RepositoryContext, defaultContext: DescriptionDictionary | undefined ): Promise { + const permission = await getRepoPermission(octokit, cache, repo); + if (permission === "none") { + const secretsContext = defaultContext || new DescriptionDictionary(); + secretsContext.complete = false; + return secretsContext; + } + let environmentName: string | undefined; if (workflowContext?.job?.environment) { if (isString(workflowContext.job.environment)) { @@ -31,54 +39,49 @@ export async function getVariables( } const variablesContext = defaultContext || new DescriptionDictionary(); - try { - const variables = await getRemoteVariables(octokit, cache, repo, environmentName); + const variables = await getRemoteVariables(octokit, cache, repo, environmentName); - // Build combined map of variables - const variablesMap = new Map< - string, - { - key: string; - value: data.StringData; - description?: string; - } - >(); - - variables.organizationVariables.forEach(variable => - variablesMap.set(variable.key.toLowerCase(), { - key: variable.key, - value: new data.StringData(variable.value.coerceString()), - description: `${variable.value.coerceString()} - Organization variable` - }) - ); - - // Override org variables with repo variables - variables.repoVariables.forEach(variable => - variablesMap.set(variable.key.toLowerCase(), { - key: variable.key, - value: new data.StringData(variable.value.coerceString()), - description: `${variable.value.coerceString()} - Repository variable` - }) - ); - - // Override repo variables with environment veriables (if defined) - variables.environmentVariables.forEach(variable => - variablesMap.set(variable.key.toLowerCase(), { - key: variable.key, - value: new data.StringData(variable.value.coerceString()), - description: `${variable.value.coerceString()} - Variable for environment \`${environmentName}\`` - }) - ); - - // Sort variables by key and add to context - Array.from(variablesMap.values()) - .sort((a, b) => a.key.localeCompare(b.key)) - .forEach(variable => variablesContext?.add(variable.key, variable.value, variable.description)); - } catch (e: any) { - if (e.status === 403 || e.status === 404) { - variablesContext.complete = false; + // Build combined map of variables + const variablesMap = new Map< + string, + { + key: string; + value: data.StringData; + description?: string; } - } + >(); + + variables.organizationVariables.forEach(variable => + variablesMap.set(variable.key.toLowerCase(), { + key: variable.key, + value: new data.StringData(variable.value.coerceString()), + description: `${variable.value.coerceString()} - Organization variable` + }) + ); + + // Override org variables with repo variables + variables.repoVariables.forEach(variable => + variablesMap.set(variable.key.toLowerCase(), { + key: variable.key, + value: new data.StringData(variable.value.coerceString()), + description: `${variable.value.coerceString()} - Repository variable` + }) + ); + + // Override repo variables with environment veriables (if defined) + variables.environmentVariables.forEach(variable => + variablesMap.set(variable.key.toLowerCase(), { + key: variable.key, + value: new data.StringData(variable.value.coerceString()), + description: `${variable.value.coerceString()} - Variable for environment \`${environmentName}\`` + }) + ); + + // Sort variables by key and add to context + Array.from(variablesMap.values()) + .sort((a, b) => a.key.localeCompare(b.key)) + .forEach(variable => variablesContext?.add(variable.key, variable.value, variable.description)); + return variablesContext; } diff --git a/languageserver/src/utils/repo-permission.ts b/languageserver/src/utils/repo-permission.ts new file mode 100644 index 0000000..5cb760d --- /dev/null +++ b/languageserver/src/utils/repo-permission.ts @@ -0,0 +1,46 @@ +import {error} from "@github/actions-languageservice/log"; +import {Octokit} from "@octokit/rest"; +import {RepositoryContext} from "../initializationOptions"; +import {TTLCache} from "./cache"; +import {getUsername} from "./username"; + +export type RepoPermission = "admin" | "write" | "read" | "none"; + +export async function getRepoPermission( + octokit: Octokit, + cache: TTLCache, + repo: RepositoryContext +): Promise { + const username = await getUsername(octokit, cache); + const permission = await cache.get(`${repo.owner}/${repo.name}/${username}/permission`, undefined, () => + fetchRepoPermission(octokit, repo, username) + ); + + switch (permission) { + case "admin": + case "write": + case "read": + case "none": + return permission; + default: + error(`Unknown permission: ${permission}`); + return "none"; + } +} + +async function fetchRepoPermission(octokit: Octokit, repo: RepositoryContext, username: string): Promise { + try { + const res = await octokit.request("GET /repos/{owner}/{repo}/collaborators/{username}/permission", { + owner: repo.owner, + repo: repo.name, + username: username + }); + const permission = res.data?.permission; + return permission; + } catch (e: any) { + if (e.status === 404 || e.status === 403) { + return "none"; + } + throw e; + } +} diff --git a/languageserver/src/utils/username.ts b/languageserver/src/utils/username.ts new file mode 100644 index 0000000..e09f79d --- /dev/null +++ b/languageserver/src/utils/username.ts @@ -0,0 +1,16 @@ +import {Octokit} from "@octokit/rest"; +import {TTLCache} from "./cache"; + +export async function getUsername(octokit: Octokit, cache: TTLCache): Promise { + return await cache.get(`/username`, undefined, () => fetchUsername(octokit)); +} + +async function fetchUsername(octokit: Octokit): Promise { + try { + const username = await octokit.request("GET /user").then(res => res.data.login); + return username; + } catch (e) { + console.log("Failure to retrieve username: ", e); + throw e; + } +} From 19ad83361085a63e2a60c8894579c8f0106c14b0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 7 Mar 2023 21:24:59 +0000 Subject: [PATCH 8/8] v0.1.159 --- browser-playground/package.json | 4 ++-- expressions/package.json | 2 +- languageserver/package.json | 6 +++--- languageservice/package.json | 6 +++--- lerna.json | 2 +- package-lock.json | 34 ++++++++++++++++----------------- workflow-parser/package.json | 4 ++-- 7 files changed, 29 insertions(+), 29 deletions(-) diff --git a/browser-playground/package.json b/browser-playground/package.json index a8e16f0..e77a377 100644 --- a/browser-playground/package.json +++ b/browser-playground/package.json @@ -1,12 +1,12 @@ { "name": "browser-playground", - "version": "0.1.158", + "version": "0.1.159", "description": "", "private": true, "main": "index.js", "type": "module", "dependencies": { - "@github/actions-languageserver": "^0.1.158", + "@github/actions-languageserver": "^0.1.159", "monaco-editor-webpack-plugin": "^7.0.1", "monaco-editor-workers": "^0.34.2", "monaco-languageclient": "^4.0.3", diff --git a/expressions/package.json b/expressions/package.json index 553ece9..7eff53b 100755 --- a/expressions/package.json +++ b/expressions/package.json @@ -1,6 +1,6 @@ { "name": "@github/actions-expressions", - "version": "0.1.158", + "version": "0.1.159", "license": "MIT", "type": "module", "source": "./src/index.ts", diff --git a/languageserver/package.json b/languageserver/package.json index d6bed10..84883f9 100644 --- a/languageserver/package.json +++ b/languageserver/package.json @@ -1,6 +1,6 @@ { "name": "@github/actions-languageserver", - "version": "0.1.158", + "version": "0.1.159", "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.158", - "@github/actions-workflow-parser": "^0.1.158", + "@github/actions-languageservice": "^0.1.159", + "@github/actions-workflow-parser": "^0.1.159", "@octokit/rest": "^19.0.7", "vscode-languageserver": "^8.0.2", "vscode-languageserver-textdocument": "^1.0.7", diff --git a/languageservice/package.json b/languageservice/package.json index c554144..8a84018 100644 --- a/languageservice/package.json +++ b/languageservice/package.json @@ -1,6 +1,6 @@ { "name": "@github/actions-languageservice", - "version": "0.1.158", + "version": "0.1.159", "description": "Language service for GitHub Actions", "license": "MIT", "type": "module", @@ -39,8 +39,8 @@ "watch": "tsc --build tsconfig.build.json --watch" }, "dependencies": { - "@github/actions-expressions": "^0.1.158", - "@github/actions-workflow-parser": "^0.1.158", + "@github/actions-expressions": "^0.1.159", + "@github/actions-workflow-parser": "^0.1.159", "vscode-languageserver-textdocument": "^1.0.7", "vscode-languageserver-types": "^3.17.2", "yaml": "^2.1.1" diff --git a/lerna.json b/lerna.json index b5b0444..8844b9a 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", "useWorkspaces": true, - "version": "0.1.158" + "version": "0.1.159" } diff --git a/package-lock.json b/package-lock.json index e518d02..a69925d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -111,10 +111,10 @@ } }, "browser-playground": { - "version": "0.1.158", + "version": "0.1.159", "license": "MIT", "dependencies": { - "@github/actions-languageserver": "^0.1.158", + "@github/actions-languageserver": "^0.1.159", "monaco-editor-webpack-plugin": "^7.0.1", "monaco-editor-workers": "^0.34.2", "monaco-languageclient": "^4.0.3", @@ -135,7 +135,7 @@ }, "expressions": { "name": "@github/actions-expressions", - "version": "0.1.158", + "version": "0.1.159", "license": "MIT", "devDependencies": { "@types/jest": "^29.0.3", @@ -151,11 +151,11 @@ }, "languageserver": { "name": "@github/actions-languageserver", - "version": "0.1.158", + "version": "0.1.159", "license": "MIT", "dependencies": { - "@github/actions-languageservice": "^0.1.158", - "@github/actions-workflow-parser": "^0.1.158", + "@github/actions-languageservice": "^0.1.159", + "@github/actions-workflow-parser": "^0.1.159", "@octokit/rest": "^19.0.7", "vscode-languageserver": "^8.0.2", "vscode-languageserver-textdocument": "^1.0.7", @@ -176,11 +176,11 @@ }, "languageservice": { "name": "@github/actions-languageservice", - "version": "0.1.158", + "version": "0.1.159", "license": "MIT", "dependencies": { - "@github/actions-expressions": "^0.1.158", - "@github/actions-workflow-parser": "^0.1.158", + "@github/actions-expressions": "^0.1.159", + "@github/actions-workflow-parser": "^0.1.159", "vscode-languageserver-textdocument": "^1.0.7", "vscode-languageserver-types": "^3.17.2", "yaml": "^2.1.1" @@ -14265,10 +14265,10 @@ }, "workflow-parser": { "name": "@github/actions-workflow-parser", - "version": "0.1.158", + "version": "0.1.159", "license": "MIT", "dependencies": { - "@github/actions-expressions": "^0.1.158", + "@github/actions-expressions": "^0.1.159", "cronstrue": "^2.21.0", "yaml": "^2.0.0-8" }, @@ -14827,8 +14827,8 @@ "@github/actions-languageserver": { "version": "file:languageserver", "requires": { - "@github/actions-languageservice": "^0.1.158", - "@github/actions-workflow-parser": "^0.1.158", + "@github/actions-languageservice": "^0.1.159", + "@github/actions-workflow-parser": "^0.1.159", "@octokit/rest": "^19.0.7", "@types/jest": "^29.0.3", "fetch-mock": "^9.11.0", @@ -14845,8 +14845,8 @@ "@github/actions-languageservice": { "version": "file:languageservice", "requires": { - "@github/actions-expressions": "^0.1.158", - "@github/actions-workflow-parser": "^0.1.158", + "@github/actions-expressions": "^0.1.159", + "@github/actions-workflow-parser": "^0.1.159", "@types/jest": "^29.0.3", "jest": "^29.0.3", "prettier": "^2.8.3", @@ -14863,7 +14863,7 @@ "@github/actions-workflow-parser": { "version": "file:workflow-parser", "requires": { - "@github/actions-expressions": "^0.1.158", + "@github/actions-expressions": "^0.1.159", "@types/jest": "^29.0.3", "@typescript-eslint/eslint-plugin": "^5.40.0", "@typescript-eslint/parser": "^5.40.0", @@ -17857,7 +17857,7 @@ "browser-playground": { "version": "file:browser-playground", "requires": { - "@github/actions-languageserver": "^0.1.158", + "@github/actions-languageserver": "^0.1.159", "css-loader": "^6.7.2", "monaco-editor-webpack-plugin": "^7.0.1", "monaco-editor-workers": "^0.34.2", diff --git a/workflow-parser/package.json b/workflow-parser/package.json index 27ca78d..e8d8f7d 100644 --- a/workflow-parser/package.json +++ b/workflow-parser/package.json @@ -1,6 +1,6 @@ { "name": "@github/actions-workflow-parser", - "version": "0.1.158", + "version": "0.1.159", "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.158", + "@github/actions-expressions": "^0.1.159", "cronstrue": "^2.21.0", "yaml": "^2.0.0-8" },