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"]);
|
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 {token, keyToken, parent, path} = findToken(newPos, result.value);
|
||||||
const template = convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
|
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
|
// 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.
|
// 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 expressionInput = (getExpressionInput(currentInput, relCharPos) || "").trim();
|
||||||
|
|
||||||
const allowedContext = getAllowedContext(token, parent!);
|
const allowedContext = getAllowedContext(token, parent!);
|
||||||
const context = await getContext(allowedContext, contextProviderConfig);
|
const context = await getContext(allowedContext, contextProviderConfig, workflowContext);
|
||||||
|
|
||||||
return completeExpression(expressionInput, context, []);
|
return completeExpression(expressionInput, context, []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflowContext = getWorkflowContext(textDocument.uri, template, path);
|
|
||||||
const values = await getValues(token, parent, valueProviderConfig, workflowContext);
|
const values = await getValues(token, parent, valueProviderConfig, workflowContext);
|
||||||
return values.map(value => CompletionItem.create(value.label));
|
return values.map(value => CompletionItem.create(value.label));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import {data} from "@github/actions-expressions";
|
import {data} from "@github/actions-expressions";
|
||||||
|
import {WorkflowContext} from "../context/workflow-context";
|
||||||
import {ContextProviderConfig} from "./config";
|
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();
|
const context = new data.Dictionary();
|
||||||
|
|
||||||
for (const contextName of names) {
|
for (const contextName of names) {
|
||||||
let value: data.Dictionary | undefined;
|
let value: data.Dictionary | undefined;
|
||||||
|
|
||||||
value = getDefaultContext(contextName);
|
value = getDefaultContext(contextName, workflowContext);
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
value = await config?.getContext(contextName);
|
value = await config?.getContext(contextName);
|
||||||
@@ -23,7 +29,7 @@ export async function getContext(names: string[], config: ContextProviderConfig
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultContext(name: string): data.Dictionary | undefined {
|
function getDefaultContext(name: string, workflowContext: WorkflowContext): data.Dictionary | undefined {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case "runner":
|
case "runner":
|
||||||
return objectToDictionary({
|
return objectToDictionary({
|
||||||
@@ -33,6 +39,8 @@ function getDefaultContext(name: string): data.Dictionary | undefined {
|
|||||||
tool_cache: "/opt/hostedtoolcache",
|
tool_cache: "/opt/hostedtoolcache",
|
||||||
temp: "/home/runner/work/_temp"
|
temp: "/home/runner/work/_temp"
|
||||||
});
|
});
|
||||||
|
case "needs":
|
||||||
|
return getNeedsContext(workflowContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
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)) {
|
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)) {
|
||||||
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
|
// Allowed values coming from the schema have already been validated. Only check if
|
||||||
@@ -196,7 +201,8 @@ function getProviderContext(
|
|||||||
async function validateExpression(
|
async function validateExpression(
|
||||||
diagnostics: Diagnostic[],
|
diagnostics: Diagnostic[],
|
||||||
token: BasicExpressionToken,
|
token: BasicExpressionToken,
|
||||||
contextProviderConfig: ContextProviderConfig | undefined
|
contextProviderConfig: ContextProviderConfig | undefined,
|
||||||
|
workflowContext: WorkflowContext
|
||||||
) {
|
) {
|
||||||
// Validate the expression
|
// Validate the expression
|
||||||
for (const expression of token.originalExpressions || [token]) {
|
for (const expression of token.originalExpressions || [token]) {
|
||||||
@@ -217,7 +223,7 @@ async function validateExpression(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const context = await getContext(namedContexts, contextProviderConfig);
|
const context = await getContext(namedContexts, contextProviderConfig, workflowContext);
|
||||||
|
|
||||||
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