Merge branch 'main' into thyeggman/token-completion-range
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import {data} from "@github/actions-expressions";
|
||||
import {CompletionItemKind} from "vscode-languageserver-types";
|
||||
import {data, DescriptionDictionary} from "@github/actions-expressions";
|
||||
import {CompletionItem, CompletionItemKind} from "vscode-languageserver-types";
|
||||
import {complete, getExpressionInput} from "./complete";
|
||||
import {ContextProviderConfig} from "./context-providers/config";
|
||||
import {registerLogger} from "./log";
|
||||
@@ -10,9 +10,10 @@ const contextProviderConfig: ContextProviderConfig = {
|
||||
getContext: async (context: string) => {
|
||||
switch (context) {
|
||||
case "github":
|
||||
return new data.Dictionary({
|
||||
return new DescriptionDictionary({
|
||||
key: "event",
|
||||
value: new data.StringData("push")
|
||||
value: new data.StringData("push"),
|
||||
description: "The event that triggered the workflow"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,6 +30,7 @@ describe("expressions", () => {
|
||||
return getExpressionInput(doc.getText(), pos.character);
|
||||
};
|
||||
|
||||
// With ${{ }}
|
||||
expect(test("${{ gh |")).toBe(" gh ");
|
||||
expect(test("${{ gh |}}")).toBe(" gh ");
|
||||
expect(test("${{ vars| == 'test' }}")).toBe(" vars");
|
||||
@@ -36,6 +38,22 @@ describe("expressions", () => {
|
||||
expect(test("${{ github.| == 'test' }}")).toBe(" github.");
|
||||
expect(test("test ${{ github.| == 'test' }}")).toBe(" github.");
|
||||
expect(test("${{ vars }} ${{ gh |}}")).toBe(" gh ");
|
||||
|
||||
expect(test("${{ test.|")).toBe(" test.");
|
||||
expect(test("${{ test.| }}")).toBe(" test.");
|
||||
expect(test("${{ 1 == (test.|)")).toBe(" 1 == (test.");
|
||||
|
||||
// Without ${{ }}
|
||||
expect(test("gh |")).toBe("gh ");
|
||||
expect(test("gh |}}")).toBe("gh ");
|
||||
expect(test("vars| == 'test' }}")).toBe("vars");
|
||||
expect(test("fromJso|('test').bar == 'test' }}")).toBe("fromJso");
|
||||
expect(test("github.| == 'test' }}")).toBe("github.");
|
||||
expect(test("github.| == 'test' }}")).toBe("github.");
|
||||
|
||||
expect(test("test.|")).toBe("test.");
|
||||
expect(test("test.| }}")).toBe("test.");
|
||||
expect(test("1 == (test.|)")).toBe("1 == (test.");
|
||||
});
|
||||
|
||||
describe("top-level auto-complete", () => {
|
||||
@@ -57,6 +75,30 @@ describe("expressions", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("within parentheses", async () => {
|
||||
const result = await complete(
|
||||
...getPositionFromCursor("run-name: ${{ 1 == (github.|) }}"),
|
||||
undefined,
|
||||
contextProviderConfig
|
||||
);
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
|
||||
it("contains description", async () => {
|
||||
const input = "run-name: ${{ github.| }}";
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, undefined);
|
||||
|
||||
expect(result).toContainEqual<CompletionItem>({
|
||||
label: "api_url",
|
||||
documentation: {
|
||||
kind: "markdown",
|
||||
value: "The URL of the GitHub Actions REST API."
|
||||
},
|
||||
kind: CompletionItemKind.Variable
|
||||
});
|
||||
});
|
||||
|
||||
it("single region with existing input", async () => {
|
||||
const input = "run-name: ${{ g| }}";
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
@@ -187,6 +229,25 @@ jobs:
|
||||
});
|
||||
});
|
||||
|
||||
it("nested with parentheses", async () => {
|
||||
const input = `on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test:
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
foo: '{}'
|
||||
steps:
|
||||
- name: "\${{ fromJSON('test') == (inputs.|) }}"`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["test"]);
|
||||
});
|
||||
|
||||
it("nested auto-complete", async () => {
|
||||
const input = "run-name: ${{ github.| }}";
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
@@ -202,30 +263,90 @@ jobs:
|
||||
expect(result.map(x => x.label)).toEqual(["arch", "name", "os", "temp", "tool_cache"]);
|
||||
});
|
||||
|
||||
it("job if", async () => {
|
||||
const input = `on: push
|
||||
describe("job if", () => {
|
||||
describe("without ${{", () => {
|
||||
it("simple", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
if: github.|
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
|
||||
it("complex", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
if: false && github.| == 'some-repo'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with ${{", () => {
|
||||
it("simple", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
if: \${{ github.| }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
|
||||
it("complex", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
if: \${{ false && github.| == 'some-repo' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("step if", async () => {
|
||||
const input = `on: push
|
||||
describe("step if", () => {
|
||||
it("with ${{", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo
|
||||
if: \${{ github.| }}`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
|
||||
it("without ${{", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo
|
||||
if: github.|`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -319,11 +440,11 @@ env:
|
||||
jobs:
|
||||
a:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
env:
|
||||
envjoba: job_a_env
|
||||
b:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
env:
|
||||
envjobb: job_b_env
|
||||
steps:
|
||||
- name: step a
|
||||
@@ -387,7 +508,7 @@ jobs:
|
||||
it("includes expected keys", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -452,7 +573,7 @@ jobs:
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -469,7 +590,7 @@ jobs:
|
||||
it("includes event payload", async () => {
|
||||
const input = `
|
||||
on: [push, pull_request]
|
||||
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -312,7 +312,7 @@ jobs:
|
||||
|
||||
// Includes detail when available. Using continue-on-error as a sample here.
|
||||
expect(result.map(x => (x.documentation as MarkupContent)?.value)).toContain(
|
||||
"Prevents a job from failing when a step fails. Set to true to allow a job to pass when this step fails."
|
||||
"Prevents a job from failing when a step fails. Set to `true` to allow a job to pass when this step fails."
|
||||
);
|
||||
});
|
||||
|
||||
@@ -337,9 +337,7 @@ o|
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
const onResult = result.find(x => x.label === "on");
|
||||
expect(onResult).not.toBeUndefined();
|
||||
expect((onResult!.documentation as MarkupContent).value).toEqual(
|
||||
"The name of the GitHub event that triggers the workflow. You can provide a single event string, array of events, array of event types, or an event configuration map that schedules a workflow or restricts the execution of a workflow to specific files, tags, or branch changes. For a list of available events, see https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows."
|
||||
);
|
||||
expect((onResult!.documentation as MarkupContent).value).toContain("The GitHub event that triggers the workflow.");
|
||||
});
|
||||
|
||||
it("event list includes descriptions when available ", async () => {
|
||||
@@ -348,8 +346,8 @@ o|
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
const dispatchResult = result.find(x => x.label === "workflow_dispatch");
|
||||
expect(dispatchResult).not.toBeUndefined();
|
||||
expect((dispatchResult!.documentation as MarkupContent).value).toEqual(
|
||||
"You can now create workflows that are manually triggered with the new workflow_dispatch event. You will then see a 'Run workflow' button on the Actions tab, enabling you to easily trigger a run."
|
||||
expect((dispatchResult!.documentation as MarkupContent).value).toContain(
|
||||
"The `workflow_dispatch` event allows you to manually trigger a workflow run."
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ import {CompletionItem, CompletionItemKind, CompletionItemTag, Range, TextEdit}
|
||||
import {ContextProviderConfig} from "./context-providers/config";
|
||||
import {getContext, Mode} from "./context-providers/default";
|
||||
import {getWorkflowContext, WorkflowContext} from "./context/workflow-context";
|
||||
import {validatorFunctions} from "./expression-validation/functions";
|
||||
import {error} from "./log";
|
||||
import {nullTrace} from "./nulltrace";
|
||||
import {findToken} from "./utils/find-token";
|
||||
import {guessIndentation} from "./utils/indentation-guesser";
|
||||
@@ -25,12 +27,14 @@ import {definitionValues} from "./value-providers/definition";
|
||||
|
||||
export function getExpressionInput(input: string, pos: number): string {
|
||||
// Find start marker around the cursor position
|
||||
const startPos = input.lastIndexOf(OPEN_EXPRESSION, pos);
|
||||
let startPos = input.lastIndexOf(OPEN_EXPRESSION, pos);
|
||||
if (startPos === -1) {
|
||||
return input;
|
||||
startPos = 0;
|
||||
} else {
|
||||
startPos += OPEN_EXPRESSION.length;
|
||||
}
|
||||
|
||||
return input.substring(startPos + OPEN_EXPRESSION.length, pos);
|
||||
return input.substring(startPos, pos);
|
||||
}
|
||||
|
||||
export async function complete(
|
||||
@@ -75,13 +79,14 @@ export async function complete(
|
||||
|
||||
// Transform the overall position into a node relative position
|
||||
let relCharPos: number = 0;
|
||||
const lineDiff = newPos.line - token.range!.start[0];
|
||||
if (token.range!.start[0] !== token.range!.end[0]) {
|
||||
const range = mapRange(token.range!);
|
||||
if (range.start.line !== range.end.line) {
|
||||
const lines = currentInput.split("\n");
|
||||
const lineDiff = newPos.line - range.start.line - 1;
|
||||
const linesBeforeCusor = lines.slice(0, lineDiff);
|
||||
relCharPos = linesBeforeCusor.join("\n").length + 1 + newPos.character;
|
||||
relCharPos = linesBeforeCusor.join("\n").length + newPos.character + 1;
|
||||
} else {
|
||||
relCharPos = newPos.character - token.range!.start[1] + 1;
|
||||
relCharPos = newPos.character - range.start.character;
|
||||
}
|
||||
|
||||
const expressionInput = (getExpressionInput(currentInput, relCharPos) || "").trim();
|
||||
@@ -89,9 +94,14 @@ export async function complete(
|
||||
const allowedContext = token.definitionInfo?.allowedContext || [];
|
||||
const context = await getContext(allowedContext, contextProviderConfig, workflowContext, Mode.Completion);
|
||||
|
||||
return completeExpression(expressionInput, context, []).map(item =>
|
||||
mapExpressionCompletionItem(item, currentInput[relCharPos])
|
||||
);
|
||||
try {
|
||||
return completeExpression(expressionInput, context, [], validatorFunctions).map(item =>
|
||||
mapExpressionCompletionItem(item, currentInput[relCharPos])
|
||||
);
|
||||
} catch (e: any) {
|
||||
error(`Error while completing expression: '${e?.message || "<no details>"}'`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +1,10 @@
|
||||
import {data} from "@github/actions-expressions";
|
||||
import {Dictionary} from "@github/actions-expressions/data/dictionary";
|
||||
import {ExpressionData, Pair} from "@github/actions-expressions/data/expressiondata";
|
||||
import {DescriptionDictionary} from "@github/actions-expressions";
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
|
||||
export type ContextProviderConfig = {
|
||||
getContext: (
|
||||
name: string,
|
||||
defaultContext: data.Dictionary | undefined,
|
||||
defaultContext: DescriptionDictionary | undefined,
|
||||
workflowContext: WorkflowContext
|
||||
) => Promise<data.Dictionary | undefined>;
|
||||
) => Promise<DescriptionDictionary | undefined>;
|
||||
};
|
||||
|
||||
/**
|
||||
* DynamicDictionary is a dictionary that returns an empty DynamicDictionary (or other given type)
|
||||
* for any key that is not present.
|
||||
*/
|
||||
export class DynamicDictionary<T extends ExpressionData = Dictionary> extends data.Dictionary {
|
||||
constructor(pairs: Pair[], private creator: () => T = () => new data.Dictionary() as T) {
|
||||
super(...pairs);
|
||||
}
|
||||
|
||||
get(key: string): ExpressionData | undefined {
|
||||
const value = super.get(key);
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return this.creator();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {data} from "@github/actions-expressions";
|
||||
import {data, DescriptionDictionary} from "@github/actions-expressions";
|
||||
import {Kind} from "@github/actions-expressions/data/expressiondata";
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
import {ContextProviderConfig} from "./config";
|
||||
import {getDescription, RootContext} from "./descriptions";
|
||||
import {getEnvContext} from "./env";
|
||||
import {getGithubContext} from "./github";
|
||||
import {getInputsContext} from "./inputs";
|
||||
@@ -13,7 +14,7 @@ import {getStrategyContext} from "./strategy";
|
||||
|
||||
// ContextValue is the type of the value returned by a context provider
|
||||
// Null indicates that the context provider doesn't have any value to provide
|
||||
export type ContextValue = data.Dictionary | data.Null;
|
||||
export type ContextValue = DescriptionDictionary | data.Null;
|
||||
|
||||
export enum Mode {
|
||||
Completion,
|
||||
@@ -26,12 +27,12 @@ export async function getContext(
|
||||
config: ContextProviderConfig | undefined,
|
||||
workflowContext: WorkflowContext,
|
||||
mode: Mode
|
||||
): Promise<data.Dictionary> {
|
||||
const context = new data.Dictionary();
|
||||
): Promise<DescriptionDictionary> {
|
||||
const context = new DescriptionDictionary();
|
||||
|
||||
const filteredNames = filterContextNames(names, workflowContext);
|
||||
for (const contextName of filteredNames) {
|
||||
let value = getDefaultContext(contextName, workflowContext, mode) || new data.Dictionary();
|
||||
let value = getDefaultContext(contextName, workflowContext, mode) || new DescriptionDictionary();
|
||||
if (value.kind === Kind.Null) {
|
||||
context.add(contextName, value);
|
||||
continue;
|
||||
@@ -39,7 +40,7 @@ export async function getContext(
|
||||
|
||||
value = (await config?.getContext(contextName, value, workflowContext)) || value;
|
||||
|
||||
context.add(contextName, value);
|
||||
context.add(contextName, value, getDescription(RootContext, contextName));
|
||||
}
|
||||
|
||||
return context;
|
||||
@@ -75,7 +76,11 @@ function getDefaultContext(name: string, workflowContext: WorkflowContext, mode:
|
||||
});
|
||||
|
||||
case "secrets":
|
||||
return objectToDictionary({GITHUB_TOKEN: "***"});
|
||||
return new DescriptionDictionary({
|
||||
key: "GITHUB_TOKEN",
|
||||
value: new data.StringData("***"),
|
||||
description: getDescription("secrets", "GITHUB_TOKEN")
|
||||
});
|
||||
|
||||
case "steps":
|
||||
return getStepsContext(workflowContext);
|
||||
@@ -87,8 +92,9 @@ function getDefaultContext(name: string, workflowContext: WorkflowContext, mode:
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function objectToDictionary(object: {[key: string]: string}): data.Dictionary {
|
||||
const dictionary = new data.Dictionary();
|
||||
function objectToDictionary(object: {[key: string]: string}): DescriptionDictionary {
|
||||
const dictionary = new DescriptionDictionary();
|
||||
|
||||
for (const key in object) {
|
||||
dictionary.add(key, new data.StringData(object[key]));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
|
||||
{
|
||||
"$schema": "./descriptionsSchema.json",
|
||||
"root": {
|
||||
"github": {
|
||||
"description": "Information about the workflow run. For more information, see [`github` context](https://docs.github.com/actions/learn-github-actions/contexts#github-context)."
|
||||
},
|
||||
"env": {
|
||||
"description": "Contains variables set in a workflow, job, or step. For more information, see [`env` context](https://docs.github.com/actions/learn-github-actions/contexts#env-context)."
|
||||
},
|
||||
"vars": {
|
||||
"description": "Contains variables set at the repository, organization, or environment levels. For more information, see [`vars` context](https://docs.github.com/actions/learn-github-actions/contexts#vars-context)."
|
||||
},
|
||||
"job": {
|
||||
"description": "Information about the currently running job. For more information, see [`job` context](https://docs.github.com/actions/learn-github-actions/contexts#job-context)."
|
||||
},
|
||||
"jobs": {
|
||||
"description": "For reusable workflows only, contains outputs of jobs from the reusable workflow. For more information, see [`jobs` context](https://docs.github.com/actions/learn-github-actions/contexts#jobs-context)."
|
||||
},
|
||||
"steps": {
|
||||
"description": "Information about the steps that have been run in the current job. For more information, see [`steps` context](https://docs.github.com/actions/learn-github-actions/contexts#steps-context)."
|
||||
},
|
||||
"runner": {
|
||||
"description": "Information about the runner that is running the current job. For more information, see [`runner` context](https://docs.github.com/actions/learn-github-actions/contexts#runner-context)."
|
||||
},
|
||||
"secrets": {
|
||||
"description": "Contains the names and values of secrets that are available to a workflow run. For more information, see [`secrets` context](https://docs.github.com/actions/learn-github-actions/contexts#secrets-context)."
|
||||
},
|
||||
"strategy": {
|
||||
"description": "Information about the matrix execution strategy for the current job. For more information, see [`strategy` context](https://docs.github.com/actions/learn-github-actions/contexts#strategy-context)."
|
||||
},
|
||||
"matrix": {
|
||||
"description": "Contains the matrix properties defined in the workflow that apply to the current job. For more information, see [`matrix` context](https://docs.github.com/actions/learn-github-actions/contexts#matrix-context)."
|
||||
},
|
||||
"needs": {
|
||||
"description": "Contains the outputs of all jobs that are defined as a dependency of the current job. For more information, see [`needs` context](https://docs.github.com/actions/learn-github-actions/contexts#needs-context)."
|
||||
},
|
||||
"inputs": {
|
||||
"description": "Contains the inputs of a reusable or manually triggered workflow. For more information, see [`inputs` context](https://docs.github.com/actions/learn-github-actions/contexts#inputs-context)."
|
||||
}
|
||||
},
|
||||
"github": {
|
||||
"action": {
|
||||
"description": "The name of the action currently running, or the [`id`](https://docs.github.com/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idstepsid) of a step. GitHub Actions removes special characters, and uses the name `__run` when the current step runs a script without an `id`. If you use the same action more than once in the same job, the name will include a suffix with the sequence number with underscore before it. For example, the first script you run will have the name `__run`, and the second script will be named `__run_2`. Similarly, the second invocation of `actions/checkout` will be `actionscheckout2`."
|
||||
},
|
||||
"action_path": {
|
||||
"description": "The path where an action is located. This property is only supported in composite actions. You can use this path to access files located in the same repository as the action."
|
||||
},
|
||||
"action_ref": {
|
||||
"description": "For a step executing an action, this is the ref of the action being executed. For example, `v2`."
|
||||
},
|
||||
"action_repository": {
|
||||
"description": "For a step executing an action, this is the owner and repository name of the action. For example, `actions/checkout`."
|
||||
},
|
||||
"action_status": {
|
||||
"description": "For a composite action, the current result of the composite action."
|
||||
},
|
||||
"actor": {
|
||||
"description": "The username of the user that triggered the initial workflow run. If the workflow run is a re-run, this value may differ from `github.triggering_actor`. Any workflow re-runs will use the privileges of `github.actor`, even if the actor initiating the re-run (`github.triggering_actor`) has different privileges."
|
||||
},
|
||||
"api_url": {
|
||||
"description": "The URL of the GitHub Actions REST API."
|
||||
},
|
||||
"base_ref": {
|
||||
"description": "The `base_ref` or target branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either `pull_request` or `pull_request_target`."
|
||||
},
|
||||
"env": {
|
||||
"description": "Path on the runner to the file that sets environment variables from workflow commands. This file is unique to the current step and is a different file for each step in a job. For more information, see [Workflow commands](https://docs.github.com/actions/learn-github-actions/workflow-commands-for-github-actions#setting-an-environment-variable)."
|
||||
},
|
||||
"event": {
|
||||
"description": "The full event webhook payload. You can access individual properties of the event using this context. This object is identical to the webhook payload of the event that triggered the workflow run, and is different for each event. The webhooks for each GitHub Actions event is linked in [Event that trigger workflows](/articles/events-that-trigger-workflows/). For example, for a workflow run triggered by the [`push` event](https://docs.github.com/actions/using-workflows/events-that-trigger-workflows#push), this object contains the contents of the [push webhook payload](https://docs.github.com/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#push)."
|
||||
},
|
||||
"event_name": {
|
||||
"description": "The name of the event that triggered the workflow run."
|
||||
},
|
||||
"event_path": {
|
||||
"description": "The path to the file on the runner that contains the full event webhook payload."
|
||||
},
|
||||
"graphql_url": {
|
||||
"description": "The URL of the GitHub Actions GraphQL API."
|
||||
},
|
||||
"head_ref": {
|
||||
"description": "The `head_ref` or source branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either `pull_request` or `pull_request_target`."
|
||||
},
|
||||
"job": {
|
||||
"description": "The [`job_id`](/actions/reference/workflow-syntax-for-github-actions#jobsjob_id) of the current job. <br /> Note: This context property is set by the Actions runner, and is only available within the execution `steps` of a job. Otherwise, the value of this property will be `null`."
|
||||
},
|
||||
"ref": {
|
||||
"description": "The fully-formed ref of the branch or tag that triggered the workflow run. For workflows triggered by `push`, this is the branch or tag ref that was pushed. For workflows triggered by `pull_request`, this is the pull request merge branch. For workflows triggered by `release`, this is the release tag created. For other triggers, this is the branch or tag ref that triggered the workflow run. This is only set if a branch or tag is available for the event type. The ref given is fully-formed, meaning that for branches the format is `refs/heads/<branch_name>`, for pull requests it is `refs/pull/<pr_number>/merge`, and for tags it is `refs/tags/<tag_name>`. For example, `refs/heads/feature-branch-1`.",
|
||||
"versions": {
|
||||
"ghes": "3.3",
|
||||
"ghae": "3.3"
|
||||
}
|
||||
},
|
||||
"ref_name": {
|
||||
"description": "The short ref name of the branch or tag that triggered the workflow run. This value matches the branch or tag name shown on GitHub. For example, `feature-branch-1`.",
|
||||
"versions": {
|
||||
"ghes": "3.3",
|
||||
"ghae": "3.3"
|
||||
}
|
||||
},
|
||||
"ref_protected": {
|
||||
"description": "`true` if branch protections are configured for the ref that triggered the workflow run.",
|
||||
"versions": {
|
||||
"ghes": "3.3",
|
||||
"ghae": "3.3"
|
||||
}
|
||||
},
|
||||
"ref_type": {
|
||||
"description": "The type of ref that triggered the workflow run. Valid values are `branch` or `tag`.",
|
||||
"versions": {
|
||||
"ghes": "3.3",
|
||||
"ghae": "3.3"
|
||||
}
|
||||
},
|
||||
"path": {
|
||||
"description": "Path on the runner to the file that sets system `PATH` variables from workflow commands. This file is unique to the current step and is a different file for each step in a job. For more information, see [Workflow commands](https://docs.github.com/actions/learn-github-actions/workflow-commands-for-github-actions#adding-a-system-path)."
|
||||
},
|
||||
"repository": {
|
||||
"description": "The owner and repository name. For example, `Codertocat/Hello-World`."
|
||||
},
|
||||
"repository_owner": {
|
||||
"description": "The repository owner's name. For example, `Codertocat`."
|
||||
},
|
||||
"repositoryUrl": {
|
||||
"description": "The Git URL to the repository. For example, `git://github.com/codertocat/hello-world.git`."
|
||||
},
|
||||
"retention_days": {
|
||||
"description": "The number of days that workflow run logs and artifacts are kept."
|
||||
},
|
||||
"run_id": {
|
||||
"description": "A unique number for each workflow run within a repository. This number does not change if you re-run the workflow run."
|
||||
},
|
||||
"run_number": {
|
||||
"description": "A unique number for each run of a particular workflow in a repository. This number begins at 1 for the workflow's first run, and increments with each new run. This number does not change if you re-run the workflow run."
|
||||
},
|
||||
"run_attempt": {
|
||||
"description": "A unique number for each attempt of a particular workflow run in a repository. This number begins at 1 for the workflow run's first attempt, and increments with each re-run.",
|
||||
"versions": {
|
||||
"ghes": "3.5",
|
||||
"ghae": "3.4"
|
||||
}
|
||||
},
|
||||
"secret_source": {
|
||||
"description": "The source of a secret used in a workflow. Possible values are `None`, `Actions`, `Dependabot`, or `Codespaces`.",
|
||||
"versions": {
|
||||
"ghes": "3.3",
|
||||
"ghae": "3.3"
|
||||
}
|
||||
},
|
||||
"server_url": {
|
||||
"description": "The URL of the GitHub server. For example: `https://github.com`."
|
||||
},
|
||||
"sha": {
|
||||
"description": "The commit SHA that triggered the workflow. The value of this commit SHA depends on the event that triggered the workflow. For more information, see [Events that trigger workflows.](https://docs.github.com/actions/using-workflows/events-that-trigger-workflows) For example, `ffac537e6cbbf934b08745a378932722df287a53`."
|
||||
},
|
||||
"token": {
|
||||
"description": "A token to authenticate on behalf of the GitHub App installed on your repository. This is functionally equivalent to the `GITHUB_TOKEN` secret. For more information, see [Automatic token authentication](https://docs.github.com/actions/security-guides/automatic-token-authentication).\nNote: This context property is set by the Actions runner, and is only available within the execution `steps` of a job. Otherwise, the value of this property will be `null`."
|
||||
},
|
||||
"triggering_actor": {
|
||||
"description": "The username of the user that initiated the workflow run. If the workflow run is a re-run, this value may differ from `github.actor`. Any workflow re-runs will use the privileges of `github.actor`, even if the actor initiating the re-run (`github.triggering_actor`) has different privileges."
|
||||
},
|
||||
"workflow": {
|
||||
"description": "The name of the workflow. If the workflow file doesn't specify a `name`, the value of this property is the full path of the workflow file in the repository."
|
||||
},
|
||||
"workspace": {
|
||||
"description": "The default working directory on the runner for steps, and the default location of your repository when using the [`checkout`](https://github.com/actions/checkout) action."
|
||||
}
|
||||
},
|
||||
"secrets": {
|
||||
"GITHUB_TOKEN": {
|
||||
"description": "`GITHUB_TOKEN` is a secret that is automatically created for every workflow run, and is always included in the secrets context. For more information, see [Automatic token authentication](https://docs.github.com/actions/security-guides/automatic-token-authentication)."
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"outputs": {
|
||||
"description": "The set of outputs defined for the step."
|
||||
},
|
||||
"conclusion": {
|
||||
"description": "The result of a completed step after `continue-on-error` is applied. Possible values are `success`, `failure`, `cancelled`, or `skipped`. When a `continue-on-error` step fails, the `outcome` is `failure`, but the final conclusion is `success`."
|
||||
},
|
||||
"outcome": {
|
||||
"description": "The result of a completed step before `continue-on-error` is applied. Possible values are `success`, `failure`, `cancelled`, or `skipped`. When a `continue-on-error` step fails, the `outcome` is `failure`, but the final conclusion is `success`."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import descriptions from "./descriptions.json";
|
||||
|
||||
export const RootContext = "root";
|
||||
|
||||
/**
|
||||
* Get a description for a built-in context
|
||||
* @param context Name of the context, for example `github`
|
||||
* @param key Key of the context, for example `actor`
|
||||
* @returns Description if one is found, otherwise undefined
|
||||
*/
|
||||
export function getDescription(context: string, key: string): string | undefined {
|
||||
// The inferred type doesn't quite match the actual type, use any to work around that
|
||||
return (descriptions as any)[context]?.[key]?.description;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string",
|
||||
"$comment": "Ignore this, just to make VS Code happy"
|
||||
}
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"versions": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import {data} from "@github/actions-expressions";
|
||||
import {data, DescriptionDictionary} from "@github/actions-expressions";
|
||||
import {isScalar, isString} from "@github/actions-workflow-parser";
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token";
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
|
||||
export function getEnvContext(workflowContext: WorkflowContext): data.Dictionary {
|
||||
const d = new data.Dictionary();
|
||||
export function getEnvContext(workflowContext: WorkflowContext): DescriptionDictionary {
|
||||
const d = new DescriptionDictionary();
|
||||
|
||||
//step env
|
||||
if (workflowContext.step?.env) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import label from "./label.json";
|
||||
import marketplace_purchase from "./marketplace_purchase.json";
|
||||
import member from "./member.json";
|
||||
import membership from "./membership.json";
|
||||
import merge_group from "./merge_group.json";
|
||||
import meta from "./meta.json";
|
||||
import milestone from "./milestone.json";
|
||||
import org_block from "./org_block.json";
|
||||
@@ -70,6 +71,7 @@ export const eventPayloads: {[key: string]: Object} = {
|
||||
marketplace_purchase,
|
||||
member,
|
||||
membership,
|
||||
merge_group,
|
||||
meta,
|
||||
milestone,
|
||||
org_block,
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"action": "checks_requested",
|
||||
"merge_group": {
|
||||
"head_sha": "2ffea6db159f6b6c47a24e778fb9ef40cf6b1c7d",
|
||||
"head_ref": "refs/heads/gh-readonly-queue/main/pr-104-929f8209d40f77f4abc622a499c93a83babdbe64",
|
||||
"base_sha": "380387fbc80638b734a49e1be1c4dfec1c01b33c",
|
||||
"base_ref": "refs/heads/main",
|
||||
"head_commit": {
|
||||
"id": "ec26c3e57ca3a959ca5aad62de7213c562f8c821",
|
||||
"tree_id": "31b122c26a97cf9af023e9ddab94a82c6e77b0ea",
|
||||
"message": "Merge pull request #2048 from octo-repo/update-readme\n\nUpdate README.md",
|
||||
"timestamp": "2019-05-15T15:20:30Z",
|
||||
"author": {
|
||||
"name": "Codertocat",
|
||||
"email": "[email protected]"
|
||||
},
|
||||
"committer": {
|
||||
"name": "Codertocat",
|
||||
"email": "[email protected]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"id": 17273051,
|
||||
"node_id": "MDEwOlJlcG9zaXRvcnkxNzI3MzA1MQ==",
|
||||
"name": "octo-repo",
|
||||
"full_name": "octo-org/octo-repo",
|
||||
"private": true,
|
||||
"owner": {
|
||||
"login": "octo-org",
|
||||
"id": 6811672,
|
||||
"node_id": "MDEyOk9yZ2FuaXphdGlvbjY4MTE2NzI=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6811672?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/octo-org",
|
||||
"html_url": "https://github.com/octo-org",
|
||||
"followers_url": "https://api.github.com/users/octo-org/followers",
|
||||
"following_url": "https://api.github.com/users/octo-org/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/octo-org/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/octo-org/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/octo-org/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/octo-org/orgs",
|
||||
"repos_url": "https://api.github.com/users/octo-org/repos",
|
||||
"events_url": "https://api.github.com/users/octo-org/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/octo-org/received_events",
|
||||
"type": "Organization",
|
||||
"site_admin": false
|
||||
},
|
||||
"html_url": "https://github.com/octo-org/octo-repo",
|
||||
"description": "My first repo on GitHub!",
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/octo-org/octo-repo",
|
||||
"forks_url": "https://api.github.com/repos/octo-org/octo-repo/forks",
|
||||
"keys_url": "https://api.github.com/repos/octo-org/octo-repo/keys{/key_id}",
|
||||
"collaborators_url": "https://api.github.com/repos/octo-org/octo-repo/collaborators{/collaborator}",
|
||||
"teams_url": "https://api.github.com/repos/octo-org/octo-repo/teams",
|
||||
"hooks_url": "https://api.github.com/repos/octo-org/octo-repo/hooks",
|
||||
"issue_events_url": "https://api.github.com/repos/octo-org/octo-repo/issues/events{/number}",
|
||||
"events_url": "https://api.github.com/repos/octo-org/octo-repo/events",
|
||||
"assignees_url": "https://api.github.com/repos/octo-org/octo-repo/assignees{/user}",
|
||||
"branches_url": "https://api.github.com/repos/octo-org/octo-repo/branches{/branch}",
|
||||
"tags_url": "https://api.github.com/repos/octo-org/octo-repo/tags",
|
||||
"blobs_url": "https://api.github.com/repos/octo-org/octo-repo/git/blobs{/sha}",
|
||||
"git_tags_url": "https://api.github.com/repos/octo-org/octo-repo/git/tags{/sha}",
|
||||
"git_refs_url": "https://api.github.com/repos/octo-org/octo-repo/git/refs{/sha}",
|
||||
"trees_url": "https://api.github.com/repos/octo-org/octo-repo/git/trees{/sha}",
|
||||
"statuses_url": "https://api.github.com/repos/octo-org/octo-repo/statuses/{sha}",
|
||||
"languages_url": "https://api.github.com/repos/octo-org/octo-repo/languages",
|
||||
"stargazers_url": "https://api.github.com/repos/octo-org/octo-repo/stargazers",
|
||||
"contributors_url": "https://api.github.com/repos/octo-org/octo-repo/contributors",
|
||||
"subscribers_url": "https://api.github.com/repos/octo-org/octo-repo/subscribers",
|
||||
"subscription_url": "https://api.github.com/repos/octo-org/octo-repo/subscription",
|
||||
"commits_url": "https://api.github.com/repos/octo-org/octo-repo/commits{/sha}",
|
||||
"git_commits_url": "https://api.github.com/repos/octo-org/octo-repo/git/commits{/sha}",
|
||||
"comments_url": "https://api.github.com/repos/octo-org/octo-repo/comments{/number}",
|
||||
"issue_comment_url": "https://api.github.com/repos/octo-org/octo-repo/issues/comments{/number}",
|
||||
"contents_url": "https://api.github.com/repos/octo-org/octo-repo/contents/{+path}",
|
||||
"compare_url": "https://api.github.com/repos/octo-org/octo-repo/compare/{base}...{head}",
|
||||
"merges_url": "https://api.github.com/repos/octo-org/octo-repo/merges",
|
||||
"archive_url": "https://api.github.com/repos/octo-org/octo-repo/{archive_format}{/ref}",
|
||||
"downloads_url": "https://api.github.com/repos/octo-org/octo-repo/downloads",
|
||||
"issues_url": "https://api.github.com/repos/octo-org/octo-repo/issues{/number}",
|
||||
"pulls_url": "https://api.github.com/repos/octo-org/octo-repo/pulls{/number}",
|
||||
"milestones_url": "https://api.github.com/repos/octo-org/octo-repo/milestones{/number}",
|
||||
"notifications_url": "https://api.github.com/repos/octo-org/octo-repo/notifications{?since,all,participating}",
|
||||
"labels_url": "https://api.github.com/repos/octo-org/octo-repo/labels{/name}",
|
||||
"releases_url": "https://api.github.com/repos/octo-org/octo-repo/releases{/id}",
|
||||
"deployments_url": "https://api.github.com/repos/octo-org/octo-repo/deployments",
|
||||
"created_at": "2014-02-28T02:42:51Z",
|
||||
"updated_at": "2021-03-11T14:54:13Z",
|
||||
"pushed_at": "2021-03-11T14:54:10Z",
|
||||
"git_url": "git://github.com/octo-org/octo-repo.git",
|
||||
"ssh_url": "[email protected]:octo-org/octo-repo.git",
|
||||
"clone_url": "https://github.com/octo-org/octo-repo.git",
|
||||
"svn_url": "https://github.com/octo-org/octo-repo",
|
||||
"homepage": "",
|
||||
"size": 300,
|
||||
"stargazers_count": 0,
|
||||
"watchers_count": 0,
|
||||
"language": "JavaScript",
|
||||
"has_issues": true,
|
||||
"has_projects": false,
|
||||
"has_downloads": true,
|
||||
"has_wiki": false,
|
||||
"has_pages": true,
|
||||
"forks_count": 0,
|
||||
"mirror_url": null,
|
||||
"archived": false,
|
||||
"disabled": false,
|
||||
"open_issues_count": 39,
|
||||
"license": null,
|
||||
"forks": 0,
|
||||
"open_issues": 39,
|
||||
"watchers": 0,
|
||||
"default_branch": "main"
|
||||
},
|
||||
"organization": {
|
||||
"login": "octo-org",
|
||||
"id": 6811672,
|
||||
"node_id": "MDEyOk9yZ2FuaXphdGlvbjY4MTE2NzI=",
|
||||
"url": "https://api.github.com/orgs/octo-org",
|
||||
"repos_url": "https://api.github.com/orgs/octo-org/repos",
|
||||
"events_url": "https://api.github.com/orgs/octo-org/events",
|
||||
"hooks_url": "https://api.github.com/orgs/octo-org/hooks",
|
||||
"issues_url": "https://api.github.com/orgs/octo-org/issues",
|
||||
"members_url": "https://api.github.com/orgs/octo-org/members{/member}",
|
||||
"public_members_url": "https://api.github.com/orgs/octo-org/public_members{/member}",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6811672?v=4",
|
||||
"description": "Working better together!"
|
||||
},
|
||||
"sender": {
|
||||
"login": "Codertocat",
|
||||
"id": 21031067,
|
||||
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/Codertocat",
|
||||
"html_url": "https://github.com/Codertocat",
|
||||
"followers_url": "https://api.github.com/users/Codertocat/followers",
|
||||
"following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/Codertocat/orgs",
|
||||
"repos_url": "https://api.github.com/users/Codertocat/repos",
|
||||
"events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/Codertocat/received_events",
|
||||
"type": "User",
|
||||
"site_admin": false
|
||||
},
|
||||
"installation": {
|
||||
"id": 1,
|
||||
"node_id": "MDEyOk9yZ2FuaXphdGlvbjY4MTE2NzI="
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
{
|
||||
"action": "on-demand-test",
|
||||
"branch": "master",
|
||||
"client_payload": {
|
||||
"unit": false,
|
||||
"integration": true
|
||||
},
|
||||
"client_payload": {},
|
||||
"repository": {
|
||||
"id": 17273051,
|
||||
"node_id": "MDEwOlJlcG9zaXRvcnkxNzI3MzA1MQ==",
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import {data} from "@github/actions-expressions";
|
||||
import {data, DescriptionDictionary} from "@github/actions-expressions";
|
||||
import {ExpressionData} from "@github/actions-expressions/data/expressiondata";
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
import {getDescription} from "./descriptions";
|
||||
import {eventPayloads} from "./events/eventPayloads";
|
||||
import {getInputsContext} from "./inputs";
|
||||
|
||||
export function getGithubContext(workflowContext: WorkflowContext): data.Dictionary {
|
||||
// https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
|
||||
export function getGithubContext(workflowContext: WorkflowContext): DescriptionDictionary {
|
||||
// https://docs.github.com/en/actions/learn-github-actions/contexts#github-cwontext
|
||||
const keys = [
|
||||
"action",
|
||||
"action_path",
|
||||
@@ -43,13 +44,23 @@ export function getGithubContext(workflowContext: WorkflowContext): data.Diction
|
||||
"workspace"
|
||||
];
|
||||
|
||||
return new data.Dictionary(
|
||||
return new DescriptionDictionary(
|
||||
...keys.map(key => {
|
||||
const description = getDescription("github", key);
|
||||
|
||||
if (key == "event") {
|
||||
return {key, value: getEventContext(workflowContext)};
|
||||
return {
|
||||
key,
|
||||
value: getEventContext(workflowContext),
|
||||
description
|
||||
};
|
||||
}
|
||||
|
||||
return {key, value: new data.Null()};
|
||||
return {
|
||||
key,
|
||||
value: new data.Null(),
|
||||
description
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -88,6 +99,13 @@ function getEventContext(workflowContext: WorkflowContext): ExpressionData {
|
||||
function merge(d: data.Dictionary, toAdd: Object): data.Dictionary {
|
||||
for (const [key, value] of Object.entries(toAdd)) {
|
||||
if (value && typeof value === "object" && !d.get(key)) {
|
||||
|
||||
if (!Array.isArray(value) && Object.entries(value).length === 0) {
|
||||
// Allow an empty object to be any value
|
||||
d.add(key, new data.Null());
|
||||
continue;
|
||||
}
|
||||
|
||||
d.add(key, merge(new data.Dictionary(), value));
|
||||
} else {
|
||||
d.add(key, new data.Null());
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {data} from "@github/actions-expressions";
|
||||
import {data, DescriptionDictionary} from "@github/actions-expressions";
|
||||
import {InputConfig} from "@github/actions-workflow-parser/model/workflow-template";
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
|
||||
export function getInputsContext(workflowContext: WorkflowContext): data.Dictionary {
|
||||
const d = new data.Dictionary();
|
||||
export function getInputsContext(workflowContext: WorkflowContext): DescriptionDictionary {
|
||||
const d = new DescriptionDictionary();
|
||||
|
||||
const events = workflowContext?.template?.events;
|
||||
if (!events) {
|
||||
@@ -23,22 +23,22 @@ export function getInputsContext(workflowContext: WorkflowContext): data.Diction
|
||||
return d;
|
||||
}
|
||||
|
||||
function addInputs(d: data.Dictionary, inputs: {[inputName: string]: InputConfig}) {
|
||||
function addInputs(d: DescriptionDictionary, inputs: {[inputName: string]: InputConfig}) {
|
||||
for (const inputName of Object.keys(inputs)) {
|
||||
const input = inputs[inputName];
|
||||
switch (input.type) {
|
||||
case "choice":
|
||||
if (input.default) {
|
||||
d.add(inputName, new data.StringData(input.default as string));
|
||||
d.add(inputName, new data.StringData(input.default as string), input.description);
|
||||
} else {
|
||||
// Default to the first input or an empty string
|
||||
d.add(inputName, new data.StringData((input.options || [""])[0]));
|
||||
d.add(inputName, new data.StringData((input.options || [""])[0]), input.description);
|
||||
}
|
||||
break;
|
||||
|
||||
case "environment":
|
||||
if (input.default) {
|
||||
d.add(inputName, new data.StringData(input.default as string));
|
||||
d.add(inputName, new data.StringData(input.default as string), input.description);
|
||||
} else {
|
||||
// For now default to an empty value if there is no default value. This will always be an environment, so
|
||||
// we could also dynamically look up environments and default to the first one, but leaving this as a
|
||||
@@ -48,12 +48,12 @@ function addInputs(d: data.Dictionary, inputs: {[inputName: string]: InputConfig
|
||||
break;
|
||||
|
||||
case "boolean":
|
||||
d.add(inputName, new data.BooleanData((input.default as boolean) || false));
|
||||
d.add(inputName, new data.BooleanData((input.default as boolean) || false), input.description);
|
||||
break;
|
||||
|
||||
case "string":
|
||||
default:
|
||||
d.add(inputName, new data.StringData((input.default as string) || inputName));
|
||||
d.add(inputName, new data.StringData((input.default as string) || inputName), input.description);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {data} from "@github/actions-expressions";
|
||||
import {data, DescriptionDictionary} from "@github/actions-expressions";
|
||||
import {isMapping, isSequence} from "@github/actions-workflow-parser";
|
||||
import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token";
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
|
||||
export function getJobContext(workflowContext: WorkflowContext): data.Dictionary {
|
||||
export function getJobContext(workflowContext: WorkflowContext): DescriptionDictionary {
|
||||
// https://docs.github.com/en/actions/learn-github-actions/contexts#job-context
|
||||
const jobContext = new data.Dictionary();
|
||||
const jobContext = new DescriptionDictionary();
|
||||
const job = workflowContext.job;
|
||||
if (!job) {
|
||||
return jobContext;
|
||||
@@ -21,7 +21,7 @@ export function getJobContext(workflowContext: WorkflowContext): data.Dictionary
|
||||
// Services
|
||||
const jobServices = job.services;
|
||||
if (jobServices && isMapping(jobServices)) {
|
||||
const servicesContext = new data.Dictionary();
|
||||
const servicesContext = new DescriptionDictionary();
|
||||
for (const service of jobServices) {
|
||||
if (!isMapping(service.value)) {
|
||||
continue;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {data} from "@github/actions-expressions";
|
||||
import {data, DescriptionDictionary} from "@github/actions-expressions";
|
||||
import {Job} from "@github/actions-workflow-parser/model/workflow-template";
|
||||
import {BasicExpressionToken} from "@github/actions-workflow-parser/templates/tokens/basic-expression-token";
|
||||
import {ExpressionToken} from "@github/actions-workflow-parser/templates/tokens/expression-token";
|
||||
import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token";
|
||||
import {SequenceToken} from "@github/actions-workflow-parser/templates/tokens/sequence-token";
|
||||
import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token";
|
||||
@@ -64,7 +63,7 @@ describe("matrix context", () => {
|
||||
expect(workflowContext.job).toBeUndefined();
|
||||
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
expect(context).toEqual(new data.Dictionary());
|
||||
expect(context).toEqual(new DescriptionDictionary());
|
||||
});
|
||||
|
||||
it("strategy not defined", () => {
|
||||
@@ -73,7 +72,7 @@ describe("matrix context", () => {
|
||||
expect(workflowContext.job!.strategy).toBeUndefined();
|
||||
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
expect(context).toEqual(new data.Dictionary());
|
||||
expect(context).toEqual(new DescriptionDictionary());
|
||||
});
|
||||
|
||||
it("strategy is not a mapping token", () => {
|
||||
@@ -81,7 +80,7 @@ describe("matrix context", () => {
|
||||
expect(workflowContext.job!.strategy).toBeDefined();
|
||||
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
expect(context).toEqual(new data.Dictionary());
|
||||
expect(context).toEqual(new DescriptionDictionary());
|
||||
});
|
||||
|
||||
it("matrix is not defined", () => {
|
||||
@@ -107,7 +106,7 @@ describe("matrix context", () => {
|
||||
const workflowContext = contextFromStrategy(strategy);
|
||||
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
expect(context).toEqual(new data.Dictionary());
|
||||
expect(context).toEqual(new DescriptionDictionary());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -161,7 +160,7 @@ describe("matrix context", () => {
|
||||
const context = getMatrixContext(workflowContext, Mode.Completion);
|
||||
|
||||
expect(context).toEqual(
|
||||
new data.Dictionary({
|
||||
new DescriptionDictionary({
|
||||
key: "node",
|
||||
value: new data.Array(new data.StringData("12"), new data.StringData("14"))
|
||||
})
|
||||
@@ -181,7 +180,7 @@ describe("matrix context", () => {
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
|
||||
expect(context).toEqual(
|
||||
new data.Dictionary({
|
||||
new DescriptionDictionary({
|
||||
key: "version",
|
||||
value: new data.Null()
|
||||
})
|
||||
@@ -195,7 +194,7 @@ describe("matrix context", () => {
|
||||
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
expect(context).toEqual(
|
||||
new data.Dictionary({
|
||||
new DescriptionDictionary({
|
||||
key: "os",
|
||||
value: new data.Array(new data.StringData("ubuntu-latest"), new data.StringData("windows-latest"))
|
||||
})
|
||||
@@ -210,7 +209,7 @@ describe("matrix context", () => {
|
||||
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
expect(context).toEqual(
|
||||
new data.Dictionary(
|
||||
new DescriptionDictionary(
|
||||
{
|
||||
key: "os",
|
||||
value: new data.Array(new data.StringData("ubuntu-latest"), new data.StringData("windows-latest"))
|
||||
@@ -238,7 +237,7 @@ describe("matrix context", () => {
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
|
||||
expect(context).toEqual(
|
||||
new data.Dictionary(
|
||||
new DescriptionDictionary(
|
||||
{
|
||||
key: "os",
|
||||
value: new data.Array(
|
||||
@@ -272,7 +271,7 @@ describe("matrix context", () => {
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
|
||||
expect(context).toEqual(
|
||||
new data.Dictionary(
|
||||
new DescriptionDictionary(
|
||||
{
|
||||
key: "site",
|
||||
value: new data.Array(new data.StringData("production"), new data.StringData("staging"))
|
||||
@@ -306,7 +305,7 @@ describe("matrix context", () => {
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
|
||||
expect(context).toEqual(
|
||||
new data.Dictionary(
|
||||
new DescriptionDictionary(
|
||||
{
|
||||
key: "os",
|
||||
value: new data.Array(new data.StringData("macos-latest"), new data.StringData("windows-latest"))
|
||||
@@ -340,7 +339,7 @@ describe("matrix context", () => {
|
||||
|
||||
const context = getMatrixContext(workflowContext, Mode.Validation);
|
||||
|
||||
expect(context).toEqual(new data.Dictionary());
|
||||
expect(context).toEqual(new DescriptionDictionary());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {data} from "@github/actions-expressions";
|
||||
import {data, DescriptionDictionary} from "@github/actions-expressions";
|
||||
import {isBasicExpression, isMapping, isSequence, isString} from "@github/actions-workflow-parser";
|
||||
import {KeyValuePair} from "@github/actions-workflow-parser/templates/tokens/key-value-pair";
|
||||
import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token";
|
||||
@@ -10,7 +10,7 @@ export function getMatrixContext(workflowContext: WorkflowContext, mode: Mode):
|
||||
// https://docs.github.com/en/actions/learn-github-actions/contexts#matrix-context
|
||||
const strategy = workflowContext.job?.strategy;
|
||||
if (!strategy || !isMapping(strategy)) {
|
||||
return new data.Dictionary();
|
||||
return new DescriptionDictionary();
|
||||
}
|
||||
|
||||
const matrix = strategy.find("matrix");
|
||||
@@ -25,7 +25,7 @@ export function getMatrixContext(workflowContext: WorkflowContext, mode: Mode):
|
||||
return new data.Null();
|
||||
}
|
||||
|
||||
const d = new data.Dictionary();
|
||||
const d = new DescriptionDictionary();
|
||||
for (const [key, value] of properties) {
|
||||
if (value === undefined) {
|
||||
d.add(key, new data.Null());
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {data} from "@github/actions-expressions";
|
||||
import {data, DescriptionDictionary} from "@github/actions-expressions";
|
||||
import {isScalar, isString} from "@github/actions-workflow-parser";
|
||||
import {Job} from "@github/actions-workflow-parser/model/workflow-template";
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
|
||||
export function getNeedsContext(workflowContext: WorkflowContext): data.Dictionary {
|
||||
const d = new data.Dictionary();
|
||||
export function getNeedsContext(workflowContext: WorkflowContext): DescriptionDictionary {
|
||||
const d = new DescriptionDictionary();
|
||||
if (!workflowContext.job || !workflowContext.job.needs) {
|
||||
return d;
|
||||
}
|
||||
@@ -17,9 +17,9 @@ export function getNeedsContext(workflowContext: WorkflowContext): data.Dictiona
|
||||
return d;
|
||||
}
|
||||
|
||||
function needsJobContext(job?: Job): data.Dictionary {
|
||||
function needsJobContext(job?: Job): DescriptionDictionary {
|
||||
// https://docs.github.com/en/actions/learn-github-actions/contexts#needs-context
|
||||
const d = new data.Dictionary();
|
||||
const d = new DescriptionDictionary();
|
||||
|
||||
d.add("outputs", jobOutputs(job));
|
||||
|
||||
@@ -28,8 +28,8 @@ function needsJobContext(job?: Job): data.Dictionary {
|
||||
return d;
|
||||
}
|
||||
|
||||
function jobOutputs(job?: Job): data.Dictionary {
|
||||
const d = new data.Dictionary();
|
||||
function jobOutputs(job?: Job): DescriptionDictionary {
|
||||
const d = new DescriptionDictionary();
|
||||
if (!job?.outputs) {
|
||||
return d;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import {data} from "@github/actions-expressions";
|
||||
import {data, DescriptionDictionary} from "@github/actions-expressions";
|
||||
import {Step} from "@github/actions-workflow-parser/model/workflow-template";
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
import {getDescription} from "./descriptions";
|
||||
|
||||
export function getStepsContext(workflowContext: WorkflowContext): data.Dictionary {
|
||||
const d = new data.Dictionary();
|
||||
export function getStepsContext(workflowContext: WorkflowContext): DescriptionDictionary {
|
||||
const d = new DescriptionDictionary();
|
||||
if (!workflowContext.job?.steps) {
|
||||
return d;
|
||||
}
|
||||
@@ -26,15 +27,15 @@ export function getStepsContext(workflowContext: WorkflowContext): data.Dictiona
|
||||
return d;
|
||||
}
|
||||
|
||||
function stepContext(): data.Dictionary {
|
||||
function stepContext(): DescriptionDictionary {
|
||||
// https://docs.github.com/en/actions/learn-github-actions/contexts#steps-context
|
||||
const d = new data.Dictionary();
|
||||
const d = new DescriptionDictionary();
|
||||
|
||||
d.add("outputs", new data.Null());
|
||||
d.add("outputs", new data.Null(), getDescription("steps", "outputs"));
|
||||
|
||||
// Can be "success", "failure", "cancelled", or "skipped"
|
||||
d.add("conclusion", new data.Null());
|
||||
d.add("outcome", new data.Null());
|
||||
d.add("conclusion", new data.Null(), getDescription("steps", "conclusion"));
|
||||
d.add("outcome", new data.Null(), getDescription("steps", "outcome"));
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import {data} from "@github/actions-expressions";
|
||||
import {data, DescriptionDictionary} from "@github/actions-expressions";
|
||||
import {isMapping, isScalar, isString} from "@github/actions-workflow-parser";
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
import {scalarToData} from "../utils/scalar-to-data";
|
||||
|
||||
export function getStrategyContext(workflowContext: WorkflowContext): data.Dictionary {
|
||||
export function getStrategyContext(workflowContext: WorkflowContext): DescriptionDictionary {
|
||||
// https://docs.github.com/en/actions/learn-github-actions/contexts#strategy-context
|
||||
const keys = ["fail-fast", "job-index", "job-total", "max-parallel"];
|
||||
|
||||
const strategy = workflowContext.job?.strategy;
|
||||
if (!strategy || !isMapping(strategy)) {
|
||||
return new data.Dictionary(
|
||||
return new DescriptionDictionary(
|
||||
...keys.map(key => {
|
||||
return {key, value: new data.Null()};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const strategyContext = new data.Dictionary();
|
||||
const strategyContext = new DescriptionDictionary();
|
||||
for (const pair of strategy) {
|
||||
if (!isString(pair.key)) {
|
||||
continue;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import {data, wellKnownFunctions} from "@github/actions-expressions";
|
||||
|
||||
// Custom implementations for standard actions-expression functions used during validation and auto-completion.
|
||||
// For example, for fromJson we'll most likely not have a valid input. In order to not throw, we'll always
|
||||
// return an empty dictionary.
|
||||
export const validatorFunctions = new Map(
|
||||
Object.entries({
|
||||
fromjson: {
|
||||
...wellKnownFunctions.fromjson,
|
||||
call: () => new data.Dictionary()
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -1,6 +1,25 @@
|
||||
import {hover} from "./hover";
|
||||
import {isString} from "@github/actions-workflow-parser/.";
|
||||
import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token";
|
||||
import {DescriptionProvider, hover, HoverConfig} from "./hover";
|
||||
import {getPositionFromCursor} from "./test-utils/cursor-position";
|
||||
|
||||
function testHoverConfig(tokenValue: string, tokenKey: string, description?: string) {
|
||||
return {
|
||||
descriptionProvider: {
|
||||
getDescription: async (_, token, __) => {
|
||||
if (!isString(token)) {
|
||||
throw new Error("Test provider only supports string tokens");
|
||||
}
|
||||
|
||||
expect((token as StringToken).value).toEqual(tokenValue);
|
||||
expect(token.definition!.key).toEqual(tokenKey);
|
||||
|
||||
return description;
|
||||
}
|
||||
} satisfies DescriptionProvider
|
||||
} satisfies HoverConfig
|
||||
}
|
||||
|
||||
describe("hover", () => {
|
||||
it("on a key", async () => {
|
||||
const input = `o|n: push
|
||||
@@ -9,9 +28,7 @@ jobs:
|
||||
runs-on: [self-hosted]`;
|
||||
const result = await hover(...getPositionFromCursor(input));
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result?.contents).toEqual(
|
||||
"The name of the GitHub event that triggers the workflow. You can provide a single event string, array of events, array of event types, or an event configuration map that schedules a workflow or restricts the execution of a workflow to specific files, tags, or branch changes. For a list of available events, see https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows."
|
||||
);
|
||||
expect(result?.contents).toContain("The GitHub event that triggers the workflow.");
|
||||
});
|
||||
|
||||
it("on a value", async () => {
|
||||
@@ -44,8 +61,8 @@ jobs:
|
||||
pe|rmissions: read-all`;
|
||||
const result = await hover(...getPositionFromCursor(input));
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result?.contents).toEqual(
|
||||
"You can modify the default permissions granted to the GITHUB_TOKEN, adding or removing access as required, so that you only allow the minimum required access."
|
||||
expect(result?.contents).toContain(
|
||||
"You can use `permissions` to modify the default permissions granted to the `GITHUB_TOKEN`"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -78,4 +95,62 @@ jobs:
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result?.contents).toEqual("Runs your workflow when you push a commit or tag.");
|
||||
});
|
||||
|
||||
|
||||
it("shows context inherited from parent nodes", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref|: main
|
||||
`;
|
||||
|
||||
const result = await hover(...getPositionFromCursor(input));
|
||||
expect(result).not.toBeUndefined();
|
||||
|
||||
// The `ref` is a `string` definition and inherits the context from `step-with`
|
||||
const expected = "**Context:** github, inputs, vars, needs, strategy, matrix, secrets, steps, job, runner, env, hashFiles(1,255)"
|
||||
expect(result?.contents).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hover with description provider", () => {
|
||||
it("uses the description provider", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref|: main
|
||||
`;
|
||||
|
||||
const result = await hover(...getPositionFromCursor(input), testHoverConfig("ref", "string", "The branch, tag or SHA to checkout."));
|
||||
expect(result).not.toBeUndefined();
|
||||
|
||||
const expected = "The branch, tag or SHA to checkout.\n\n" +
|
||||
"**Context:** github, inputs, vars, needs, strategy, matrix, secrets, steps, job, runner, env, hashFiles(1,255)"
|
||||
expect(result?.contents).toEqual(expected);
|
||||
});
|
||||
|
||||
it("falls back to the token description", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted]
|
||||
steps:
|
||||
- uses|: actions/checkout@v2
|
||||
`;
|
||||
|
||||
const result = await hover(...getPositionFromCursor(input), testHoverConfig("uses", "non-empty-string", undefined));
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result?.contents).toEqual("Selects an action to run as part of a step in your job. An action is a reusable unit of code. You can use an action defined in the same repository as the workflow, a public repository, or in a published Docker container image.");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,49 +1,66 @@
|
||||
import {parseWorkflow} from "@github/actions-workflow-parser";
|
||||
import {convertWorkflowTemplate, parseWorkflow, ParseWorkflowResult} from "@github/actions-workflow-parser";
|
||||
import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert";
|
||||
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
|
||||
import {File} from "@github/actions-workflow-parser/workflows/file";
|
||||
import {Position, TextDocument} from "vscode-languageserver-textdocument";
|
||||
import {Hover} from "vscode-languageserver-types";
|
||||
import {getWorkflowContext, WorkflowContext} from "./context/workflow-context";
|
||||
import {info} from "./log";
|
||||
import {nullTrace} from "./nulltrace";
|
||||
import {findToken} from "./utils/find-token";
|
||||
import {mapRange} from "./utils/range";
|
||||
|
||||
export type DescriptionProvider = {
|
||||
getDescription(context: WorkflowContext, token: TemplateToken, path: TemplateToken[]): Promise<string | undefined>;
|
||||
};
|
||||
|
||||
export type HoverConfig = {
|
||||
descriptionProvider?: DescriptionProvider;
|
||||
};
|
||||
|
||||
// Render value description and Context when hovering over a key in a MappingToken
|
||||
export async function hover(document: TextDocument, position: Position): Promise<Hover | null> {
|
||||
export async function hover(document: TextDocument, position: Position, config?: HoverConfig): Promise<Hover | null> {
|
||||
const file: File = {
|
||||
name: document.uri,
|
||||
content: document.getText()
|
||||
};
|
||||
const result = parseWorkflow(file.name, [file], nullTrace);
|
||||
|
||||
const {token} = findToken(position, result.value);
|
||||
|
||||
if (result.value && token) {
|
||||
return getHover(token);
|
||||
const {token, path} = findToken(position, result.value);
|
||||
if (!token?.definition) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
|
||||
info(`Calculating hover for token with definition ${token.definition.key}`);
|
||||
|
||||
let description = await getDescription(document, config, result, token, path);
|
||||
|
||||
const allowedContext = token.definitionInfo?.allowedContext;
|
||||
if (allowedContext && allowedContext?.length > 0) {
|
||||
// Only add padding if there is a description
|
||||
description += `${description.length > 0 ? `\n\n` : ""}**Context:** ${allowedContext.join(", ")}`;
|
||||
}
|
||||
|
||||
return {
|
||||
contents: description,
|
||||
range: mapRange(token.range)
|
||||
} satisfies Hover;
|
||||
}
|
||||
|
||||
function getHover(token: TemplateToken): Hover | null {
|
||||
if (token.definition) {
|
||||
info(`Calculating hover for token with definition ${token.definition.key}`);
|
||||
|
||||
let description = "";
|
||||
if (token.description) {
|
||||
description = token.description;
|
||||
}
|
||||
|
||||
if (token.definition.evaluatorContext.length > 0) {
|
||||
// Only add padding if there is a description
|
||||
description += `${description.length > 0 ? `\n\n` : ""}**Context:** ${token.definition.evaluatorContext.join(
|
||||
", "
|
||||
)}`;
|
||||
}
|
||||
|
||||
return {
|
||||
contents: description,
|
||||
range: mapRange(token.range)
|
||||
} as Hover;
|
||||
async function getDescription(
|
||||
document: TextDocument,
|
||||
config: HoverConfig | undefined,
|
||||
result: ParseWorkflowResult | undefined,
|
||||
token: TemplateToken,
|
||||
path: TemplateToken[]
|
||||
) {
|
||||
const defaultDescription = token.description || "";
|
||||
if (!result?.value || !config?.descriptionProvider) {
|
||||
return defaultDescription;
|
||||
}
|
||||
return null;
|
||||
|
||||
const template = convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
|
||||
const workflowContext = getWorkflowContext(document.uri, template, path);
|
||||
const description = await config.descriptionProvider.getDescription(workflowContext, token, path);
|
||||
return description || defaultDescription;
|
||||
}
|
||||
|
||||
@@ -21,4 +21,12 @@ describe("getPositionFromCursor", () => {
|
||||
expect(position).toEqual({line: 0, character: 0});
|
||||
expect(newDoc.getText()).toEqual("on: push\njobs:");
|
||||
});
|
||||
|
||||
it("handles a cursor in the middle of the document", () => {
|
||||
const input = "on: push\n jobs|:\n build:";
|
||||
const [newDoc, position] = getPositionFromCursor(input);
|
||||
|
||||
expect(position).toEqual({line: 1, character: 6});
|
||||
expect(newDoc.getText()).toEqual("on: push\n jobs:\n build:");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {TokenRange} from "@github/actions-workflow-parser/templates/tokens/token-range";
|
||||
import {Range} from "vscode-languageserver-types";
|
||||
import {Position as TokenPosition, TokenRange} from "@github/actions-workflow-parser/templates/tokens/token-range";
|
||||
import {Position, Range} from "vscode-languageserver-types";
|
||||
|
||||
export function mapRange(range: TokenRange | undefined): Range {
|
||||
if (!range) {
|
||||
@@ -16,13 +16,14 @@ export function mapRange(range: TokenRange | undefined): Range {
|
||||
}
|
||||
|
||||
return {
|
||||
start: {
|
||||
line: range.start[0] - 1,
|
||||
character: range.start[1] - 1
|
||||
},
|
||||
end: {
|
||||
line: range.end[0] - 1,
|
||||
character: range.end[1] - 1
|
||||
}
|
||||
start: mapPosition(range.start),
|
||||
end: mapPosition(range.end)
|
||||
};
|
||||
}
|
||||
|
||||
export function mapPosition(position: TokenPosition): Position {
|
||||
return {
|
||||
line: position[0] - 1,
|
||||
character: position[1] - 1
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
jobs:
|
||||
build:
|
||||
runs-on:
|
||||
- dummy`);
|
||||
- key`);
|
||||
expect(newPos.character).toEqual(9);
|
||||
});
|
||||
|
||||
@@ -53,7 +53,23 @@ jobs:
|
||||
jobs:
|
||||
build:
|
||||
runs-on:
|
||||
dummy:`);
|
||||
key:`);
|
||||
expect(newPos.character).toEqual(7);
|
||||
});
|
||||
|
||||
it("does not transform expression lines", () => {
|
||||
const [doc, pos] = getPositionFromCursor(`on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on:
|
||||
\${{ github| }}`);
|
||||
const [newDoc, newPos] = transform(doc, pos);
|
||||
|
||||
expect(newDoc.getText()).toEqual(`on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on:
|
||||
\${{ github }}`);
|
||||
expect(newPos.character).toEqual(16);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Position, TextDocument} from "vscode-languageserver-textdocument";
|
||||
import {Range} from "vscode-languageserver-types";
|
||||
|
||||
const DUMMY_KEY = "dummy";
|
||||
const PLACEHOLDER_KEY = "key";
|
||||
|
||||
// Transform a document to work around YAML parsing issues
|
||||
// Based on `_transform` in https://github.com/cschleiden/github-actions-parser/blob/main/src/lib/parser/complete.ts#L311
|
||||
@@ -27,29 +27,33 @@ export function transform(doc: TextDocument, pos: Position): [TextDocument, Posi
|
||||
// Special case for Actions, if this line contains an expression marker, do _not_ transform. This is
|
||||
// an ugly fix for auto-completion in multi-line YAML strings. At this point in the process, we cannot
|
||||
// determine if a line is in such a multi-line string.
|
||||
if (line.indexOf("${{") === -1) {
|
||||
const colon = line.indexOf(":");
|
||||
if (colon === -1) {
|
||||
const trimmedLine = line.trim();
|
||||
if (trimmedLine === "" || trimmedLine === "-") {
|
||||
// Node in sequence or empty line
|
||||
let spacer = "";
|
||||
if (trimmedLine === "-" && !line.endsWith(" ")) {
|
||||
spacer = " ";
|
||||
offset++;
|
||||
}
|
||||
if (line.indexOf("${{") !== -1) {
|
||||
return [doc, pos];
|
||||
}
|
||||
|
||||
line =
|
||||
line.substring(0, linePos) + spacer + DUMMY_KEY + (trimmedLine === "-" ? "" : ":") + line.substring(linePos);
|
||||
|
||||
// Adjust pos by one to prevent a sequence node being marked as active
|
||||
const containsColon = line.indexOf(":") !== -1;
|
||||
if (!containsColon) {
|
||||
const trimmedLine = line.trim();
|
||||
if (trimmedLine === "" || trimmedLine === "-") {
|
||||
// Pos in sequence or empty line
|
||||
let spacer = "";
|
||||
if (trimmedLine === "-" && !line.endsWith(" ")) {
|
||||
spacer = " ";
|
||||
offset++;
|
||||
} else if (!trimmedLine.startsWith("-")) {
|
||||
// Add `:` to end of line
|
||||
line = line + ":";
|
||||
}
|
||||
} else {
|
||||
offset = offset - 1;
|
||||
|
||||
line =
|
||||
line.substring(0, linePos) +
|
||||
spacer +
|
||||
PLACEHOLDER_KEY +
|
||||
(trimmedLine === "-" ? "" : ":") +
|
||||
line.substring(linePos);
|
||||
|
||||
// Adjust pos by one to prevent a sequence node being marked as active
|
||||
offset++;
|
||||
} else if (!trimmedLine.startsWith("-")) {
|
||||
// Add `:` to end of line
|
||||
line = line + ":";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -359,7 +359,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: step a
|
||||
env:
|
||||
env:
|
||||
step_env: job_a_env
|
||||
run: echo "hello \${{ env.step_env }}
|
||||
`;
|
||||
@@ -376,7 +376,7 @@ env:
|
||||
jobs:
|
||||
a:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
env:
|
||||
envjoba: job_a_env
|
||||
steps:
|
||||
- name: step a
|
||||
@@ -395,7 +395,7 @@ env:
|
||||
jobs:
|
||||
a:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
env:
|
||||
envjoba: job_a_env
|
||||
steps:
|
||||
- name: step a
|
||||
@@ -1185,6 +1185,9 @@ jobs:
|
||||
- run: echo "hello \${{ github.event.inputs.name }}"
|
||||
- run: echo "hello \${{ github.event.inputs.third-name }}"
|
||||
- run: echo "hello \${{ github.event.inputs.random }}"
|
||||
- run: echo \${{ fromJSON(inputs.random2) }}
|
||||
- run: echo "hello \${{ inputs.random }}"
|
||||
name: "\${{ fromJSON('test') == inputs.name }}"
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
@@ -1202,8 +1205,54 @@ jobs:
|
||||
}
|
||||
},
|
||||
severity: DiagnosticSeverity.Warning
|
||||
},
|
||||
{
|
||||
message: "Context access might be invalid: random2",
|
||||
range: {
|
||||
end: {
|
||||
character: 47,
|
||||
line: 20
|
||||
},
|
||||
start: {
|
||||
character: 16,
|
||||
line: 20
|
||||
}
|
||||
},
|
||||
severity: 2
|
||||
},
|
||||
{
|
||||
message: "Context access might be invalid: random",
|
||||
range: {
|
||||
end: {
|
||||
character: 43,
|
||||
line: 21
|
||||
},
|
||||
start: {
|
||||
character: 23,
|
||||
line: 21
|
||||
}
|
||||
},
|
||||
severity: 2
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("allows any property in client_payload", async () => {
|
||||
const input = `
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [test]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo \${{ github.event.client_payload.anything }}
|
||||
- run: echo \${{ github.event.client_payload.branch }}`
|
||||
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Evaluator, Lexer, Parser} from "@github/actions-expressions";
|
||||
import {Evaluator, ExpressionEvaluationError, Lexer, Parser} from "@github/actions-expressions";
|
||||
import {Expr} from "@github/actions-expressions/ast";
|
||||
import {
|
||||
convertWorkflowTemplate,
|
||||
@@ -22,6 +22,7 @@ import {ContextProviderConfig} from "./context-providers/config";
|
||||
import {getContext, Mode} from "./context-providers/default";
|
||||
import {getWorkflowContext, WorkflowContext} from "./context/workflow-context";
|
||||
import {AccessError, wrapDictionary} from "./expression-validation/error-dictionary";
|
||||
import {validatorFunctions} from "./expression-validation/functions";
|
||||
import {error} from "./log";
|
||||
import {nullTrace} from "./nulltrace";
|
||||
import {findToken} from "./utils/find-token";
|
||||
@@ -202,7 +203,7 @@ async function validateExpression(
|
||||
try {
|
||||
const context = await getContext(namedContexts, contextProviderConfig, workflowContext, Mode.Validation);
|
||||
|
||||
const e = new Evaluator(expr, wrapDictionary(context));
|
||||
const e = new Evaluator(expr, wrapDictionary(context), validatorFunctions);
|
||||
e.evaluate();
|
||||
|
||||
// Any invalid context access would've thrown an error via the `ErrorDictionary`, for now we don't have to check the actual
|
||||
@@ -214,6 +215,12 @@ async function validateExpression(
|
||||
severity: DiagnosticSeverity.Warning,
|
||||
range: mapRange(expression.range)
|
||||
});
|
||||
} else if (e instanceof ExpressionEvaluationError) {
|
||||
diagnostics.push({
|
||||
message: `Expression might be invalid: ${e.message}`,
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: mapRange(expression.range)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export interface Value {
|
||||
/** Label of this value */
|
||||
label: string;
|
||||
|
||||
/** Optional description to show when auto-completing or hovering */
|
||||
/** Optional description to show when auto-completing */
|
||||
description?: string;
|
||||
|
||||
/** Whether this value is deprecated */
|
||||
|
||||
Reference in New Issue
Block a user