Merge pull request #23 from github/joshmgross/needs-context-completion
Add a `needs` context provider for auto-completion
This commit is contained in:
@@ -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"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user