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 {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":
@@ -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<DescriptionDictionary> {
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);
@@ -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 = `
@@ -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<DescriptionDictionary | undefined>;
};
@@ -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);
@@ -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: []
});
});
// 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";
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)
};
}
@@ -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 = {
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;
};
+10
View File
@@ -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": {