Convert secrets, complete secrets in called workflow
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
Reference in New Issue
Block a user