Merge pull request #23 from github/joshmgross/needs-context-completion

Add a `needs` context provider for auto-completion
This commit is contained in:
Josh Gross
2022-11-30 12:59:16 -05:00
committed by GitHub
6 changed files with 132 additions and 8 deletions
@@ -148,4 +148,68 @@ jobs:
expect(result.map(x => x.label)).toEqual(["DEPLOY_KEY"]);
});
it("needs context only includes referenced jobs", async () => {
const input = `
on: push
jobs:
a:
runs-on: ubuntu-latest
steps:
- run: echo hello a
b:
needs: [a]
runs-on: ubuntu-latest
steps:
- run: echo hello b
c:
needs: [b]
runs-on: ubuntu-latest
steps:
- run: echo "hello \${{ needs.|
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["b"]);
});
it("needs.<job_id>", async () => {
const input = `
on: push
jobs:
a:
runs-on: ubuntu-latest
steps:
- run: echo hello a
b:
needs: [a]
runs-on: ubuntu-latest
steps:
- run: echo "hello \${{ needs.a.|
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["outputs", "result"]);
});
it.failing("needs.<job_id>.outputs includes outputs", async () => {
const input = `
on: push
jobs:
a:
outputs:
build_id: my-build-id
runs-on: ubuntu-latest
steps:
- run: echo hello a
b:
needs: [a]
runs-on: ubuntu-latest
steps:
- run: echo "hello \${{ needs.a.outputs.|
`;
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
expect(result.map(x => x.label)).toEqual(["build_id"]);
});
});
+2 -2
View File
@@ -65,6 +65,7 @@ export async function complete(
const {token, keyToken, parent, path} = findToken(newPos, result.value);
const template = convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
const workflowContext = getWorkflowContext(textDocument.uri, template, path);
// If we are inside an expression, take a different code-path. The workflow parser does not correctly create
// expression nodes for invalid expressions and during editing expressions are invalid most of the time.
@@ -82,13 +83,12 @@ export async function complete(
const expressionInput = (getExpressionInput(currentInput, relCharPos) || "").trim();
const allowedContext = getAllowedContext(token, parent!);
const context = await getContext(allowedContext, contextProviderConfig);
const context = await getContext(allowedContext, contextProviderConfig, workflowContext);
return completeExpression(expressionInput, context, []);
}
}
const workflowContext = getWorkflowContext(textDocument.uri, template, path);
const values = await getValues(token, parent, valueProviderConfig, workflowContext);
return values.map(value => CompletionItem.create(value.label));
}
@@ -1,13 +1,19 @@
import {data} from "@github/actions-expressions";
import {WorkflowContext} from "../context/workflow-context";
import {ContextProviderConfig} from "./config";
import {getNeedsContext} from "./needs";
export async function getContext(names: string[], config: ContextProviderConfig | undefined): Promise<data.Dictionary> {
export async function getContext(
names: string[],
config: ContextProviderConfig | undefined,
workflowContext: WorkflowContext
): Promise<data.Dictionary> {
const context = new data.Dictionary();
for (const contextName of names) {
let value: data.Dictionary | undefined;
value = getDefaultContext(contextName);
value = getDefaultContext(contextName, workflowContext);
if (!value) {
value = await config?.getContext(contextName);
@@ -23,7 +29,7 @@ export async function getContext(names: string[], config: ContextProviderConfig
return context;
}
function getDefaultContext(name: string): data.Dictionary | undefined {
function getDefaultContext(name: string, workflowContext: WorkflowContext): data.Dictionary | undefined {
switch (name) {
case "runner":
return objectToDictionary({
@@ -33,6 +39,8 @@ function getDefaultContext(name: string): data.Dictionary | undefined {
tool_cache: "/opt/hostedtoolcache",
temp: "/home/runner/work/_temp"
});
case "needs":
return getNeedsContext(workflowContext);
}
return undefined;
@@ -0,0 +1,27 @@
import {data} from "@github/actions-expressions/.";
import {WorkflowContext} from "../context/workflow-context";
export function getNeedsContext(workflowContext: WorkflowContext): data.Dictionary {
const d = new data.Dictionary();
if (!workflowContext.job || !workflowContext.job.needs) {
return d;
}
for (const job of workflowContext.job.needs) {
d.add(job, defaultNeedsValues());
}
return d;
}
function defaultNeedsValues(): data.Dictionary {
// https://docs.github.com/en/actions/learn-github-actions/contexts#needs-context
const d = new data.Dictionary();
// Once job config contains outputs, this can be populated
d.add("outputs", new data.Null());
// Can be "success", "failure", "cancelled", or "skipped"
d.add("result", new data.Null());
return d;
}
@@ -54,4 +54,23 @@ describe("expression validation", () => {
}
]);
});
it.failing("needs.<job_id>", async () => {
const input = `
on: push
jobs:
a:
runs-on: ubuntu-latest
steps:
- run: echo hello a
b:
needs: [a]
runs-on: ubuntu-latest
steps:
- run: echo "hello \${{ needs.a }}"
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toEqual([]);
});
});
+9 -3
View File
@@ -116,7 +116,12 @@ async function additionalValidations(
for (const token of TemplateToken.traverse(root)) {
// If this is an expression, validate it
if (isBasicExpression(token)) {
await validateExpression(diagnostics, token, contextProviderConfig);
await validateExpression(
diagnostics,
token,
contextProviderConfig,
getProviderContext(documentUri, template, root, token)
);
}
// Allowed values coming from the schema have already been validated. Only check if
@@ -196,7 +201,8 @@ function getProviderContext(
async function validateExpression(
diagnostics: Diagnostic[],
token: BasicExpressionToken,
contextProviderConfig: ContextProviderConfig | undefined
contextProviderConfig: ContextProviderConfig | undefined,
workflowContext: WorkflowContext
) {
// Validate the expression
for (const expression of token.originalExpressions || [token]) {
@@ -217,7 +223,7 @@ async function validateExpression(
}
try {
const context = await getContext(namedContexts, contextProviderConfig);
const context = await getContext(namedContexts, contextProviderConfig, workflowContext);
const e = new Evaluator(expr, wrapDictionary(context));
e.evaluate();