Convert secrets, complete secrets in called workflow

This commit is contained in:
Beth Brennan
2023-03-07 15:11:16 -05:00
parent 94e537e072
commit 87e3131958
11 changed files with 226 additions and 14 deletions
+4 -2
View File
@@ -1,5 +1,6 @@
import {DescriptionDictionary} from "@github/actions-expressions"; import {DescriptionDictionary} from "@github/actions-expressions";
import {ContextProviderConfig} from "@github/actions-languageservice"; 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 {WorkflowContext} from "@github/actions-languageservice/context/workflow-context";
import {Octokit} from "@octokit/rest"; import {Octokit} from "@octokit/rest";
import {getSecrets} from "./context-providers/secrets"; import {getSecrets} from "./context-providers/secrets";
@@ -20,11 +21,12 @@ export function contextProviders(
const getContext = async ( const getContext = async (
name: string, name: string,
defaultContext: DescriptionDictionary | undefined, defaultContext: DescriptionDictionary | undefined,
workflowContext: WorkflowContext workflowContext: WorkflowContext,
mode: Mode
) => { ) => {
switch (name) { switch (name) {
case "secrets": case "secrets":
return await getSecrets(workflowContext, client, cache, repo, defaultContext); return await getSecrets(workflowContext, client, cache, repo, defaultContext, mode);
case "vars": case "vars":
return await getVariables(workflowContext, client, cache, repo, defaultContext); return await getVariables(workflowContext, client, cache, repo, defaultContext);
case "steps": case "steps":
@@ -1,5 +1,6 @@
import {data, DescriptionDictionary} from "@github/actions-expressions"; import {data, DescriptionDictionary} from "@github/actions-expressions";
import {StringData} from "@github/actions-expressions/data/string"; 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 {WorkflowContext} from "@github/actions-languageservice/context/workflow-context";
import {isMapping, isString} from "@github/actions-workflow-parser"; import {isMapping, isString} from "@github/actions-workflow-parser";
import {Octokit} from "@octokit/rest"; import {Octokit} from "@octokit/rest";
@@ -11,7 +12,8 @@ export async function getSecrets(
octokit: Octokit, octokit: Octokit,
cache: TTLCache, cache: TTLCache,
repo: RepositoryContext, repo: RepositoryContext,
defaultContext: DescriptionDictionary | undefined defaultContext: DescriptionDictionary | undefined,
mode: Mode
): Promise<DescriptionDictionary> { ): Promise<DescriptionDictionary> {
let environmentName: string | undefined; let environmentName: string | undefined;
if (workflowContext?.job?.environment) { if (workflowContext?.job?.environment) {
@@ -30,6 +32,15 @@ export async function getSecrets(
} }
const secretsContext = defaultContext || new DescriptionDictionary(); 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 { try {
const secrets = await getRemoteSecrets(octokit, cache, repo, environmentName); const secrets = await getRemoteSecrets(octokit, cache, repo, environmentName);
@@ -535,6 +535,28 @@ jobs:
expect(result).toEqual([]); 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", () => { describe("github context", () => {
it("includes expected keys", async () => { it("includes expected keys", async () => {
const input = ` const input = `
@@ -1,10 +1,12 @@
import {DescriptionDictionary} from "@github/actions-expressions"; import {DescriptionDictionary} from "@github/actions-expressions";
import {WorkflowContext} from "../context/workflow-context"; import {WorkflowContext} from "../context/workflow-context";
import {Mode} from "./default";
export type ContextProviderConfig = { export type ContextProviderConfig = {
getContext: ( getContext: (
name: string, name: string,
defaultContext: DescriptionDictionary | undefined, defaultContext: DescriptionDictionary | undefined,
workflowContext: WorkflowContext workflowContext: WorkflowContext,
mode: Mode
) => Promise<DescriptionDictionary | undefined>; ) => Promise<DescriptionDictionary | undefined>;
}; };
@@ -10,6 +10,7 @@ import {getJobContext} from "./job";
import {getJobsContext} from "./jobs"; import {getJobsContext} from "./jobs";
import {getMatrixContext} from "./matrix"; import {getMatrixContext} from "./matrix";
import {getNeedsContext} from "./needs"; import {getNeedsContext} from "./needs";
import {getSecretsContext} from "./secrets";
import {getStepsContext} from "./steps"; import {getStepsContext} from "./steps";
import {getStrategyContext} from "./strategy"; import {getStrategyContext} from "./strategy";
@@ -39,7 +40,7 @@ export async function getContext(
continue; 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)); context.add(contextName, value, getDescription(RootContext, contextName));
} }
@@ -81,11 +82,7 @@ function getDefaultContext(name: string, workflowContext: WorkflowContext, mode:
}); });
case "secrets": case "secrets":
return new DescriptionDictionary({ return getSecretsContext(workflowContext, mode);
key: "GITHUB_TOKEN",
value: new data.StringData("***"),
description: getDescription("secrets", "GITHUB_TOKEN")
});
case "steps": case "steps":
return getStepsContext(workflowContext); return getStepsContext(workflowContext);
@@ -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;
}
+67
View File
@@ -314,4 +314,71 @@ jobs:
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"
}
]
});
});
}); });
@@ -15,6 +15,7 @@ import {
} from "../workflow-template"; } from "../workflow-template";
import {isValidCron} from "./cron"; import {isValidCron} from "./cron";
import {convertStringList} from "./string-list"; import {convertStringList} from "./string-list";
import {convertEventWorkflowCall} from "./workflow-call";
import {convertEventWorkflowDispatchInputs} from "./workflow-dispatch"; import {convertEventWorkflowDispatchInputs} from "./workflow-dispatch";
export function convertOn(context: TemplateContext, token: TemplateToken): EventsConfig { export function convertOn(context: TemplateContext, token: TemplateToken): EventsConfig {
@@ -66,8 +67,9 @@ export function convertOn(context: TemplateContext, token: TemplateToken): Event
...convertPatternFilter("paths", eventToken), ...convertPatternFilter("paths", eventToken),
...convertFilter("types", eventToken), ...convertFilter("types", eventToken),
...convertFilter("workflows", eventToken), ...convertFilter("workflows", eventToken),
// TODO - share input parsing for now, but workflow_call also needs outputs and secrets // workflow_call and workflow_dispatch share input parsing
...convertEventWorkflowDispatchInputs(context, eventToken) ...convertEventWorkflowDispatchInputs(context, eventToken),
...convertEventWorkflowCall(context, eventToken)
}; };
} }
@@ -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;
}
@@ -158,10 +158,10 @@ export type WorkflowDispatchConfig = {
}; };
export type WorkflowCallConfig = { 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 // TODO - these are supported in C# and Go but not in TS yet
// outputs: { [outputName: string]: OutputConfig } // outputs: { [outputName: string]: OutputConfig }
// secrets: { [secretName: string]: SecretConfig }
}; };
export enum InputType { export enum InputType {
@@ -179,6 +179,11 @@ export type InputConfig = {
options?: string[]; options?: string[];
}; };
export type SecretConfig = {
description?: string;
required?: boolean;
};
export type ScheduleConfig = { export type ScheduleConfig = {
cron: string; cron: string;
}; };
+10
View File
@@ -204,6 +204,10 @@ on:
description: 'Foo' description: 'Foo'
required: true required: true
default: 'bar' default: 'bar'
secrets:
password:
description: 'Password'
required: true
workflow_run: workflow_run:
workflows: ci workflows: ci
types: types:
@@ -500,6 +504,12 @@ jobs:
"required": true, "required": true,
"default": "bar" "default": "bar"
} }
},
"secrets": {
"password": {
"description": "Password",
"required": true
}
} }
}, },
"workflow_run": { "workflow_run": {