Complete reusable workflow secrets, handle placeholder key
This commit is contained in:
@@ -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"
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<string>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,7 +21,7 @@ export enum ValueProviderKind {
|
||||
|
||||
export type ValueProvider = {
|
||||
kind: ValueProviderKind;
|
||||
get: (context: WorkflowContext) => Promise<Value[]>;
|
||||
get: (context: WorkflowContext, existingValues?: Set<string>) => Promise<Value[]>;
|
||||
};
|
||||
|
||||
export interface ValueProviderConfig {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<string>): 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;
|
||||
}
|
||||
Reference in New Issue
Block a user