diff --git a/languageserver/src/context-providers.ts b/languageserver/src/context-providers.ts index 965475c..b8ce462 100644 --- a/languageserver/src/context-providers.ts +++ b/languageserver/src/context-providers.ts @@ -1,5 +1,6 @@ import {DescriptionDictionary} from "@github/actions-expressions"; import {ContextProviderConfig} from "@github/actions-languageservice"; +import {Mode} from "@github/actions-languageservice/context-providers/default"; import {WorkflowContext} from "@github/actions-languageservice/context/workflow-context"; import {Octokit} from "@octokit/rest"; import {getSecrets} from "./context-providers/secrets"; @@ -20,11 +21,12 @@ export function contextProviders( const getContext = async ( name: string, defaultContext: DescriptionDictionary | undefined, - workflowContext: WorkflowContext + workflowContext: WorkflowContext, + mode: Mode ) => { switch (name) { case "secrets": - return await getSecrets(workflowContext, client, cache, repo, defaultContext); + return await getSecrets(workflowContext, client, cache, repo, defaultContext, mode); case "vars": return await getVariables(workflowContext, client, cache, repo, defaultContext); case "steps": diff --git a/languageserver/src/context-providers/secrets.ts b/languageserver/src/context-providers/secrets.ts index 217b417..0f9cd9a 100644 --- a/languageserver/src/context-providers/secrets.ts +++ b/languageserver/src/context-providers/secrets.ts @@ -1,5 +1,6 @@ import {data, DescriptionDictionary} from "@github/actions-expressions"; import {StringData} from "@github/actions-expressions/data/string"; +import {Mode} from "@github/actions-languageservice/context-providers/default"; import {WorkflowContext} from "@github/actions-languageservice/context/workflow-context"; import {isMapping, isString} from "@github/actions-workflow-parser"; import {Octokit} from "@octokit/rest"; @@ -11,7 +12,8 @@ export async function getSecrets( octokit: Octokit, cache: TTLCache, repo: RepositoryContext, - defaultContext: DescriptionDictionary | undefined + defaultContext: DescriptionDictionary | undefined, + mode: Mode ): Promise { let environmentName: string | undefined; if (workflowContext?.job?.environment) { @@ -30,6 +32,15 @@ export async function getSecrets( } const secretsContext = defaultContext || new DescriptionDictionary(); + + // Exit early if workflow_call is the only trigger + if (mode === Mode.Completion) { + const eventsConfig = workflowContext?.template?.events; + if (eventsConfig?.workflow_call && Object.keys(eventsConfig).length == 1) { + return secretsContext; + } + } + try { const secrets = await getRemoteSecrets(octokit, cache, repo, environmentName); diff --git a/languageservice/src/complete.expressions.test.ts b/languageservice/src/complete.expressions.test.ts index fc16165..9f570fa 100644 --- a/languageservice/src/complete.expressions.test.ts +++ b/languageservice/src/complete.expressions.test.ts @@ -535,6 +535,28 @@ jobs: expect(result).toEqual([]); }); + it("secrets", async () => { + const input = ` +on: + workflow_call: + secrets: + secret1: + required: true + description: "first secret" + secret2: +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/deploy@v100 + with: + deploy-key: \${{ secrets.| +`; + + const result = await complete(...getPositionFromCursor(input), {contextProviderConfig}); + expect(result.map(x => x.label)).toEqual(["GITHUB_TOKEN", "secret1", "secret2"]); + }); + describe("github context", () => { it("includes expected keys", async () => { const input = ` diff --git a/languageservice/src/context-providers/config.ts b/languageservice/src/context-providers/config.ts index 2c2f30b..cf35973 100644 --- a/languageservice/src/context-providers/config.ts +++ b/languageservice/src/context-providers/config.ts @@ -1,10 +1,12 @@ import {DescriptionDictionary} from "@github/actions-expressions"; import {WorkflowContext} from "../context/workflow-context"; +import {Mode} from "./default"; export type ContextProviderConfig = { getContext: ( name: string, defaultContext: DescriptionDictionary | undefined, - workflowContext: WorkflowContext + workflowContext: WorkflowContext, + mode: Mode ) => Promise; }; diff --git a/languageservice/src/context-providers/default.ts b/languageservice/src/context-providers/default.ts index da0f169..4322940 100644 --- a/languageservice/src/context-providers/default.ts +++ b/languageservice/src/context-providers/default.ts @@ -10,6 +10,7 @@ import {getJobContext} from "./job"; import {getJobsContext} from "./jobs"; import {getMatrixContext} from "./matrix"; import {getNeedsContext} from "./needs"; +import {getSecretsContext} from "./secrets"; import {getStepsContext} from "./steps"; import {getStrategyContext} from "./strategy"; @@ -39,7 +40,7 @@ export async function getContext( continue; } - value = (await config?.getContext(contextName, value, workflowContext)) || value; + value = (await config?.getContext(contextName, value, workflowContext, mode)) || value; context.add(contextName, value, getDescription(RootContext, contextName)); } @@ -81,11 +82,7 @@ function getDefaultContext(name: string, workflowContext: WorkflowContext, mode: }); case "secrets": - return new DescriptionDictionary({ - key: "GITHUB_TOKEN", - value: new data.StringData("***"), - description: getDescription("secrets", "GITHUB_TOKEN") - }); + return getSecretsContext(workflowContext, mode); case "steps": return getStepsContext(workflowContext); diff --git a/languageservice/src/context-providers/secrets.ts b/languageservice/src/context-providers/secrets.ts new file mode 100644 index 0000000..d06fad0 --- /dev/null +++ b/languageservice/src/context-providers/secrets.ts @@ -0,0 +1,29 @@ +import {data, DescriptionDictionary} from "@github/actions-expressions"; +import {StringData} from "@github/actions-expressions/data/string"; +import {WorkflowContext} from "../context/workflow-context"; +import {Mode} from "./default"; +import {getDescription} from "./descriptions"; + +export function getSecretsContext(workflowContext: WorkflowContext, mode: Mode): DescriptionDictionary { + const d = new DescriptionDictionary({ + key: "GITHUB_TOKEN", + value: new data.StringData("***"), + description: getDescription("secrets", "GITHUB_TOKEN") + }); + + if (mode === Mode.Completion) { + const eventsConfig = workflowContext?.template?.events; + if (eventsConfig?.workflow_call?.secrets) { + for (const [name, value] of Object.entries(eventsConfig.workflow_call.secrets)) { + d.add(name, new StringData(""), value.description); + } + } + } + + const events = workflowContext?.template?.events; + if (!events) { + return d; + } + + return d; +} diff --git a/workflow-parser/src/model/convert.test.ts b/workflow-parser/src/model/convert.test.ts index 5fb1ec8..05cc026 100644 --- a/workflow-parser/src/model/convert.test.ts +++ b/workflow-parser/src/model/convert.test.ts @@ -314,4 +314,71 @@ jobs: jobs: [] }); }); + + // Extra coverage since workflow_call components are not all covered by x-lang parsers + it("converts workflow_call on", async () => { + const result = parseWorkflow( + { + name: "wf.yaml", + content: `on: + workflow_call: + inputs: + test: + type: string + description: 'a test' + required: true + default: 'testing 123' + secrets: + secret1: + description: 'first secret' + required: true + secret2: +jobs: + build: + runs-on: ubuntu-latest` + }, + nullTrace + ); + + const template = await convertWorkflowTemplate(result.context, result.value!, undefined, { + errorPolicy: ErrorPolicy.TryConversion + }); + + expect(serializeTemplate(template)).toEqual({ + events: { + workflow_call: { + inputs: { + test: { + description: "a test", + required: true, + type: "string", + default: "testing 123" + } + }, + secrets: { + secret1: { + description: "first secret", + required: true + }, + secret2: {} + } + } + }, + jobs: [ + { + id: "build", + if: { + expr: "success()", + type: 3 + }, + name: "build", + needs: undefined, + outputs: undefined, + "runs-on": "ubuntu-latest", + steps: [], + type: "job" + } + ] + }); + }); }); diff --git a/workflow-parser/src/model/converter/events.ts b/workflow-parser/src/model/converter/events.ts index 1db676f..dcfede6 100644 --- a/workflow-parser/src/model/converter/events.ts +++ b/workflow-parser/src/model/converter/events.ts @@ -15,6 +15,7 @@ import { } from "../workflow-template"; import {isValidCron} from "./cron"; import {convertStringList} from "./string-list"; +import {convertEventWorkflowCall} from "./workflow-call"; import {convertEventWorkflowDispatchInputs} from "./workflow-dispatch"; export function convertOn(context: TemplateContext, token: TemplateToken): EventsConfig { @@ -66,8 +67,9 @@ export function convertOn(context: TemplateContext, token: TemplateToken): Event ...convertPatternFilter("paths", eventToken), ...convertFilter("types", eventToken), ...convertFilter("workflows", eventToken), - // TODO - share input parsing for now, but workflow_call also needs outputs and secrets - ...convertEventWorkflowDispatchInputs(context, eventToken) + // workflow_call and workflow_dispatch share input parsing + ...convertEventWorkflowDispatchInputs(context, eventToken), + ...convertEventWorkflowCall(context, eventToken) }; } diff --git a/workflow-parser/src/model/converter/workflow-call.ts b/workflow-parser/src/model/converter/workflow-call.ts new file mode 100644 index 0000000..bc699d1 --- /dev/null +++ b/workflow-parser/src/model/converter/workflow-call.ts @@ -0,0 +1,65 @@ +import {TemplateContext} from "../../templates/template-context"; +import {MappingToken, TemplateToken} from "../../templates/tokens"; +import {isMapping} from "../../templates/tokens/type-guards"; +import {SecretConfig, WorkflowCallConfig} from "../workflow-template"; + +export function convertEventWorkflowCall(context: TemplateContext, token: MappingToken): WorkflowCallConfig { + const result: WorkflowCallConfig = {}; + + for (const item of token) { + const key = item.key.assertString("workflow dispatch input key"); + + switch (key.value) { + case "inputs": + // Ignore, these are handled by convertEventWorkflowDispatchInputs + break; + + case "secrets": + result.secrets = convertWorkflowCallSecrets(context, item.value.assertMapping("workflow dispatch inputs")); + break; + + case "outputs": + // TODO - outputs + break; + } + } + + return result; +} + +function convertWorkflowCallSecrets( + context: TemplateContext, + token: MappingToken +): {[secretName: string]: SecretConfig} { + const result: {[secretName: string]: SecretConfig} = {}; + + for (const item of token) { + const secretName = item.key.assertString("secret name"); + + result[secretName.value] = convertWorkflowCallSecret(context, item.value); + } + + return result; +} + +function convertWorkflowCallSecret(context: TemplateContext, token: TemplateToken): SecretConfig { + const result: SecretConfig = {}; + + if (isMapping(token)) { + for (const item of token) { + const key = item.key.assertString("workflow call secret key"); + + switch (key.value) { + case "description": + result.description = item.value.assertString("secret description").value; + break; + + case "required": + result.required = item.value.assertBoolean("secret required").value; + break; + } + } + } + + return result; +} diff --git a/workflow-parser/src/model/workflow-template.ts b/workflow-parser/src/model/workflow-template.ts index ae0ecbb..ff3d172 100644 --- a/workflow-parser/src/model/workflow-template.ts +++ b/workflow-parser/src/model/workflow-template.ts @@ -158,10 +158,10 @@ export type WorkflowDispatchConfig = { }; export type WorkflowCallConfig = { - inputs: {[inputName: string]: InputConfig}; + inputs?: {[inputName: string]: InputConfig}; + secrets?: {[secretName: string]: SecretConfig}; // TODO - these are supported in C# and Go but not in TS yet // outputs: { [outputName: string]: OutputConfig } - // secrets: { [secretName: string]: SecretConfig } }; export enum InputType { @@ -179,6 +179,11 @@ export type InputConfig = { options?: string[]; }; +export type SecretConfig = { + description?: string; + required?: boolean; +}; + export type ScheduleConfig = { cron: string; }; diff --git a/workflow-parser/testdata/reader/events-mapping-all.yml b/workflow-parser/testdata/reader/events-mapping-all.yml index 264c751..0cb40a8 100644 --- a/workflow-parser/testdata/reader/events-mapping-all.yml +++ b/workflow-parser/testdata/reader/events-mapping-all.yml @@ -204,6 +204,10 @@ on: description: 'Foo' required: true default: 'bar' + secrets: + password: + description: 'Password' + required: true workflow_run: workflows: ci types: @@ -500,6 +504,12 @@ jobs: "required": true, "default": "bar" } + }, + "secrets": { + "password": { + "description": "Password", + "required": true + } } }, "workflow_run": {