Add a context provider for secrets
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
import {data} from "@github/actions-expressions";
|
||||||
|
import {ContextProviderConfig} from "@github/actions-languageservice/.";
|
||||||
|
import {Octokit} from "@octokit/rest";
|
||||||
|
import {getSecrets} from "./context-providers/secrets";
|
||||||
|
import {RepositoryContext} from "./initializationOptions";
|
||||||
|
import {TTLCache} from "./utils/cache";
|
||||||
|
|
||||||
|
export function contextProviders(
|
||||||
|
sessionToken: string | undefined,
|
||||||
|
repo: RepositoryContext | undefined,
|
||||||
|
cache: TTLCache
|
||||||
|
): ContextProviderConfig {
|
||||||
|
if (!repo || !sessionToken) {
|
||||||
|
return {getContext: (_: string) => Promise.resolve(undefined)};
|
||||||
|
}
|
||||||
|
|
||||||
|
const octokit = new Octokit({
|
||||||
|
auth: sessionToken
|
||||||
|
});
|
||||||
|
|
||||||
|
const getContext = async (name: string) => {
|
||||||
|
switch (name) {
|
||||||
|
case "secrets":
|
||||||
|
const secrets = await getSecrets(octokit, cache, repo.owner, repo.name);
|
||||||
|
const secretContext = new data.Dictionary();
|
||||||
|
secrets.forEach(secret => secretContext.add(secret.value, secret));
|
||||||
|
return secretContext;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {getContext};
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import {StringData} from "@github/actions-expressions/data/string";
|
||||||
|
import {Octokit} from "@octokit/rest";
|
||||||
|
import {TTLCache} from "../utils/cache";
|
||||||
|
|
||||||
|
export function getSecrets(octokit: Octokit, cache: TTLCache, owner: string, name: string): Promise<StringData[]> {
|
||||||
|
return cache.get(`${owner}/${name}/secrets`, undefined, () => fetchSecrets(octokit, owner, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSecrets(octokit: Octokit, owner: string, name: string): Promise<StringData[]> {
|
||||||
|
try {
|
||||||
|
const response = await octokit.actions.listRepoSecrets({
|
||||||
|
owner,
|
||||||
|
repo: name
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data.secrets.map(secret => new StringData(secret.name));
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failure to retrieve secrets: ", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { complete } from "@github/actions-languageservice/complete";
|
import { complete } from "@github/actions-languageservice/complete";
|
||||||
import { CompletionItem, Position } from "vscode-languageserver";
|
import { CompletionItem, Position } from "vscode-languageserver";
|
||||||
import { TextDocument } from "vscode-languageserver-textdocument";
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
||||||
|
import { contextProviders } from "./context-providers";
|
||||||
import { RepositoryContext } from "./initializationOptions";
|
import { RepositoryContext } from "./initializationOptions";
|
||||||
import { TTLCache } from "./utils/cache";
|
import { TTLCache } from "./utils/cache";
|
||||||
import { valueProviders } from "./value-providers";
|
import { valueProviders } from "./value-providers";
|
||||||
@@ -15,6 +16,7 @@ export async function onCompletion(
|
|||||||
return await complete(
|
return await complete(
|
||||||
document,
|
document,
|
||||||
position,
|
position,
|
||||||
repoContext && valueProviders(sessionToken, repoContext, cache)
|
repoContext && valueProviders(sessionToken, repoContext, cache),
|
||||||
|
repoContext && contextProviders(sessionToken, repoContext, cache)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,18 @@ import {ContextProviderConfig} from "./context-providers/config";
|
|||||||
import {getPositionFromCursor} from "./test-utils/cursor-position";
|
import {getPositionFromCursor} from "./test-utils/cursor-position";
|
||||||
|
|
||||||
const contextProviderConfig: ContextProviderConfig = {
|
const contextProviderConfig: ContextProviderConfig = {
|
||||||
getContext: (context: string) => {
|
getContext: async (context: string) => {
|
||||||
switch (context) {
|
switch (context) {
|
||||||
case "github":
|
case "github":
|
||||||
return new data.Dictionary({
|
return new data.Dictionary({
|
||||||
key: "event",
|
key: "event",
|
||||||
value: new data.StringData("push")
|
value: new data.StringData("push")
|
||||||
});
|
});
|
||||||
|
case "secrets":
|
||||||
|
return new data.Dictionary({
|
||||||
|
key: "DEPLOY_KEY",
|
||||||
|
value: new data.StringData("DEPLOY_KEY")
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -125,4 +130,22 @@ jobs:
|
|||||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("context inherited from parent", async () => {
|
||||||
|
// The token definition is just a `string` and
|
||||||
|
// the parent `step-with` holds the allowed context
|
||||||
|
const input = `
|
||||||
|
on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/deploy@v100
|
||||||
|
with:
|
||||||
|
deploy-key: \${{ secrets.|
|
||||||
|
`;
|
||||||
|
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||||
|
|
||||||
|
expect(result.map(x => x.label)).toEqual(["DEPLOY_KEY"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {ContextProviderConfig} from "./context-providers/config";
|
|||||||
import {getContext} from "./context-providers/default";
|
import {getContext} from "./context-providers/default";
|
||||||
import {getWorkflowContext, WorkflowContext} from "./context/workflow-context";
|
import {getWorkflowContext, WorkflowContext} from "./context/workflow-context";
|
||||||
import {nullTrace} from "./nulltrace";
|
import {nullTrace} from "./nulltrace";
|
||||||
|
import {getAllowedContext} from "./utils/allowed-context";
|
||||||
import {findToken} from "./utils/find-token";
|
import {findToken} from "./utils/find-token";
|
||||||
import {transform} from "./utils/transform";
|
import {transform} from "./utils/transform";
|
||||||
import {Value, ValueProviderConfig} from "./value-providers/config";
|
import {Value, ValueProviderConfig} from "./value-providers/config";
|
||||||
@@ -80,7 +81,8 @@ export async function complete(
|
|||||||
|
|
||||||
const expressionInput = (getExpressionInput(currentInput, relCharPos) || "").trim();
|
const expressionInput = (getExpressionInput(currentInput, relCharPos) || "").trim();
|
||||||
|
|
||||||
const context = getContext(token.definition?.readerContext || [], contextProviderConfig);
|
const allowedContext = getAllowedContext(token, parent!);
|
||||||
|
const context = await getContext(allowedContext, contextProviderConfig);
|
||||||
|
|
||||||
return completeExpression(expressionInput, context, []);
|
return completeExpression(expressionInput, context, []);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {Dictionary} from "@github/actions-expressions/data/dictionary";
|
|||||||
import {ExpressionData, Pair} from "@github/actions-expressions/data/expressiondata";
|
import {ExpressionData, Pair} from "@github/actions-expressions/data/expressiondata";
|
||||||
|
|
||||||
export type ContextProviderConfig = {
|
export type ContextProviderConfig = {
|
||||||
getContext: (name: string) => data.Dictionary | undefined;
|
getContext: (name: string) => Promise<data.Dictionary | undefined>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {data} from "@github/actions-expressions";
|
import {data} from "@github/actions-expressions";
|
||||||
import {ContextProviderConfig} from "./config";
|
import {ContextProviderConfig} from "./config";
|
||||||
|
|
||||||
export function getContext(names: string[], config: ContextProviderConfig | undefined): data.Dictionary {
|
export async function getContext(names: string[], config: ContextProviderConfig | undefined): Promise<data.Dictionary> {
|
||||||
const context = new data.Dictionary();
|
const context = new data.Dictionary();
|
||||||
|
|
||||||
for (const contextName of names) {
|
for (const contextName of names) {
|
||||||
@@ -10,7 +10,7 @@ export function getContext(names: string[], config: ContextProviderConfig | unde
|
|||||||
value = getDefaultContext(contextName);
|
value = getDefaultContext(contextName);
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
value = config?.getContext(contextName);
|
value = await config?.getContext(contextName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
|
||||||
|
|
||||||
|
export function getAllowedContext(token: TemplateToken, parent: TemplateToken): string[] {
|
||||||
|
// Workaround for https://github.com/github/c2c-actions-experience/issues/6876
|
||||||
|
// Context is inherited from the parent
|
||||||
|
const allowedContext = new Set<string>();
|
||||||
|
for (const t of [token, parent]) {
|
||||||
|
if (t.definition?.readerContext) {
|
||||||
|
for (const context of t.definition.readerContext) {
|
||||||
|
allowedContext.add(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(allowedContext);
|
||||||
|
}
|
||||||
@@ -116,7 +116,7 @@ async function additionalValidations(
|
|||||||
for (const token of TemplateToken.traverse(root)) {
|
for (const token of TemplateToken.traverse(root)) {
|
||||||
// If this is an expression, validate it
|
// If this is an expression, validate it
|
||||||
if (isBasicExpression(token)) {
|
if (isBasicExpression(token)) {
|
||||||
validateExpression(diagnostics, token, contextProviderConfig);
|
await validateExpression(diagnostics, token, contextProviderConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allowed values coming from the schema have already been validated. Only check if
|
// Allowed values coming from the schema have already been validated. Only check if
|
||||||
@@ -193,7 +193,7 @@ function getProviderContext(
|
|||||||
return getWorkflowContext(documentUri, template, path);
|
return getWorkflowContext(documentUri, template, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateExpression(
|
async function validateExpression(
|
||||||
diagnostics: Diagnostic[],
|
diagnostics: Diagnostic[],
|
||||||
token: BasicExpressionToken,
|
token: BasicExpressionToken,
|
||||||
contextProviderConfig: ContextProviderConfig | undefined
|
contextProviderConfig: ContextProviderConfig | undefined
|
||||||
@@ -217,7 +217,7 @@ function validateExpression(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const context = getContext(namedContexts, contextProviderConfig);
|
const context = await getContext(namedContexts, contextProviderConfig);
|
||||||
|
|
||||||
const e = new Evaluator(expr, wrapDictionary(context));
|
const e = new Evaluator(expr, wrapDictionary(context));
|
||||||
e.evaluate();
|
e.evaluate();
|
||||||
|
|||||||
Reference in New Issue
Block a user