Merge branch 'main' into joshmgross/read-file-request-types
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@github/actions-expressions",
|
||||
"version": "0.1.126",
|
||||
"version": "0.1.132",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"source": "./src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@github/actions-languageserver",
|
||||
"version": "0.1.126",
|
||||
"version": "0.1.132",
|
||||
"description": "Language server for GitHub Actions",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -38,8 +38,8 @@
|
||||
"watch": "tsc --build tsconfig.build.json --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@github/actions-languageservice": "^0.1.126",
|
||||
"@github/actions-workflow-parser": "^0.1.126",
|
||||
"@github/actions-languageservice": "^0.1.132",
|
||||
"@github/actions-workflow-parser": "^0.1.132",
|
||||
"@octokit/rest": "^19.0.7",
|
||||
"vscode-languageserver": "^8.0.2",
|
||||
"vscode-languageserver-textdocument": "^1.0.7",
|
||||
|
||||
@@ -120,6 +120,7 @@ export function initConnection(connection: Connection) {
|
||||
|
||||
connection.onCompletion(async ({position, textDocument}: TextDocumentPositionParams): Promise<CompletionItem[]> => {
|
||||
return await onCompletion(
|
||||
connection,
|
||||
position,
|
||||
documents.get(textDocument.uri)!,
|
||||
client,
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import {complete} from "@github/actions-languageservice/complete";
|
||||
import {Octokit} from "@octokit/rest";
|
||||
import {CompletionItem, Position} from "vscode-languageserver";
|
||||
import {CompletionItem, Connection, Position} from "vscode-languageserver";
|
||||
import {TextDocument} from "vscode-languageserver-textdocument";
|
||||
import {contextProviders} from "./context-providers";
|
||||
import {getFileProvider} from "./file-provider";
|
||||
import {RepositoryContext} from "./initializationOptions";
|
||||
import {TTLCache} from "./utils/cache";
|
||||
import {valueProviders} from "./value-providers";
|
||||
|
||||
export async function onCompletion(
|
||||
connection: Connection,
|
||||
position: Position,
|
||||
document: TextDocument,
|
||||
client: Octokit | undefined,
|
||||
repoContext: RepositoryContext | undefined,
|
||||
cache: TTLCache
|
||||
): Promise<CompletionItem[]> {
|
||||
return await complete(
|
||||
document,
|
||||
position,
|
||||
repoContext && valueProviders(client, repoContext, cache),
|
||||
repoContext && contextProviders(client, repoContext, cache)
|
||||
);
|
||||
return await complete(document, position, {
|
||||
valueProviderConfig: repoContext && valueProviders(client, repoContext, cache),
|
||||
contextProviderConfig: repoContext && contextProviders(client, repoContext, cache),
|
||||
fileProvider: getFileProvider(client, cache, repoContext?.workspaceUri, async path => {
|
||||
return await connection.sendRequest("actions/readFile", {path});
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,31 +1,16 @@
|
||||
import {Value} from "@github/actions-languageservice/value-providers/config";
|
||||
import {DEFAULT_RUNNER_LABELS} from "@github/actions-languageservice/value-providers/default";
|
||||
import {Octokit} from "@octokit/rest";
|
||||
import {TTLCache} from "../utils/cache";
|
||||
|
||||
// Limitation: getRunnerLabels returns default hosted labels and labels for repository self-hosted runners.
|
||||
// It doesn't return labels for organization runners visible to the repository.
|
||||
export async function getRunnerLabels(client: Octokit, cache: TTLCache, owner: string, name: string): Promise<Value[]> {
|
||||
const defaultLabels = [
|
||||
"ubuntu-latest",
|
||||
"ubuntu-22.04",
|
||||
"ubuntu-20.04",
|
||||
"ubuntu-18.04",
|
||||
"windows-latest",
|
||||
"windows-2022",
|
||||
"windows-2019",
|
||||
"windows-2016",
|
||||
"macos-latest",
|
||||
"macos-12",
|
||||
"macos-11",
|
||||
"macos-10.15",
|
||||
"self-hosted"
|
||||
];
|
||||
|
||||
const repoLabels = await cache.get(`${owner}/${name}/runner-labels`, undefined, () =>
|
||||
fetchRunnerLabels(client, owner, name)
|
||||
);
|
||||
|
||||
for (const label of defaultLabels) {
|
||||
for (const label of DEFAULT_RUNNER_LABELS) {
|
||||
repoLabels.add(label);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@github/actions-languageservice",
|
||||
"version": "0.1.126",
|
||||
"version": "0.1.132",
|
||||
"description": "Language service for GitHub Actions",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -38,8 +38,8 @@
|
||||
"watch": "tsc --build tsconfig.build.json --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@github/actions-expressions": "^0.1.126",
|
||||
"@github/actions-workflow-parser": "^0.1.126",
|
||||
"@github/actions-expressions": "^0.1.132",
|
||||
"@github/actions-workflow-parser": "^0.1.132",
|
||||
"vscode-languageserver-textdocument": "^1.0.7",
|
||||
"vscode-languageserver-types": "^3.17.2",
|
||||
"yaml": "^2.1.1"
|
||||
|
||||
@@ -59,7 +59,7 @@ describe("expressions", () => {
|
||||
describe("top-level auto-complete", () => {
|
||||
it("single region", async () => {
|
||||
const input = "run-name: ${{ | }}";
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, undefined);
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
expect(result.map(x => x.label)).toEqual([
|
||||
"github",
|
||||
@@ -76,18 +76,16 @@ describe("expressions", () => {
|
||||
});
|
||||
|
||||
it("within parentheses", async () => {
|
||||
const result = await complete(
|
||||
...getPositionFromCursor("run-name: ${{ 1 == (github.|) }}"),
|
||||
undefined,
|
||||
const result = await complete(...getPositionFromCursor("run-name: ${{ 1 == (github.|) }}"), {
|
||||
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);
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
expect(result).toContainEqual<CompletionItem>({
|
||||
label: "api_url",
|
||||
@@ -101,7 +99,7 @@ describe("expressions", () => {
|
||||
|
||||
it("single region with existing input", async () => {
|
||||
const input = "run-name: ${{ g| }}";
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual([
|
||||
"github",
|
||||
@@ -119,7 +117,7 @@ describe("expressions", () => {
|
||||
|
||||
it("single region with existing condition", async () => {
|
||||
const input = "run-name: ${{ g| == 'test' }}";
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual([
|
||||
"github",
|
||||
@@ -137,7 +135,7 @@ describe("expressions", () => {
|
||||
|
||||
it("multiple regions with partial function", async () => {
|
||||
const input = "run-name: Run a ${{ inputs.test }} one-line script ${{ from|('test') == inputs.name }}";
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual([
|
||||
"github",
|
||||
@@ -155,7 +153,7 @@ describe("expressions", () => {
|
||||
|
||||
it("multiple regions - first region", async () => {
|
||||
const input = "run-name: test-${{ git| == 1 }}-${{ github.event }}";
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual([
|
||||
"github",
|
||||
@@ -173,7 +171,7 @@ describe("expressions", () => {
|
||||
|
||||
it("multiple regions", async () => {
|
||||
const input = "run-name: test-${{ github }}-${{ | }}";
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual([
|
||||
"github",
|
||||
@@ -199,7 +197,7 @@ jobs:
|
||||
first line
|
||||
test \${{ github.| }}
|
||||
test2`;
|
||||
const result = await complete(...getPositionFromCursor(input, 1), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input, 1), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
@@ -213,7 +211,7 @@ jobs:
|
||||
first line
|
||||
test \${{ github.| }}
|
||||
test2`;
|
||||
const result = await complete(...getPositionFromCursor(input, 1), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input, 1), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
@@ -227,7 +225,7 @@ jobs:
|
||||
first line
|
||||
test \${{ github.| }}
|
||||
test2`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
@@ -241,7 +239,7 @@ jobs:
|
||||
first line
|
||||
test \${{ github.| }}
|
||||
test2`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
@@ -261,21 +259,28 @@ jobs:
|
||||
foo: '{}'
|
||||
steps:
|
||||
- name: "\${{ fromJSON('test') == (inputs.|) }}"`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {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);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
|
||||
it("auto-complete partial", async () => {
|
||||
const input = "run-name: ${{ github.ev| }}";
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
|
||||
it("auto-complete complex partial", async () => {
|
||||
const input = 'run-name: "run ${{ github.ev| }} run"';
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
@@ -283,7 +288,7 @@ jobs:
|
||||
it("using default context provider", async () => {
|
||||
const input =
|
||||
"on: push\njobs:\n build:\n runs-on: ubuntu-latest\n environment:\n url: ${{ runner.| }}\n steps:\n - run: echo";
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["arch", "name", "os", "temp", "tool_cache"]);
|
||||
});
|
||||
@@ -298,7 +303,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
@@ -311,7 +316,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
@@ -326,7 +331,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
@@ -339,7 +344,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
@@ -355,7 +360,7 @@ jobs:
|
||||
steps:
|
||||
- run: echo
|
||||
if: \${{ github.| }}`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
@@ -368,7 +373,7 @@ jobs:
|
||||
steps:
|
||||
- run: echo
|
||||
if: github.|`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["event"]);
|
||||
});
|
||||
@@ -388,7 +393,7 @@ jobs:
|
||||
with:
|
||||
deploy-key: \${{ secrets.|
|
||||
`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["GITHUB_TOKEN"]);
|
||||
});
|
||||
@@ -412,7 +417,7 @@ jobs:
|
||||
steps:
|
||||
- run: echo "hello \${{ needs.|
|
||||
`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["b"]);
|
||||
});
|
||||
@@ -431,7 +436,7 @@ jobs:
|
||||
steps:
|
||||
- run: echo "hello \${{ needs.a.|
|
||||
`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["outputs", "result"]);
|
||||
});
|
||||
@@ -452,7 +457,7 @@ jobs:
|
||||
steps:
|
||||
- run: echo "hello \${{ needs.a.outputs.|
|
||||
`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["build_id"]);
|
||||
});
|
||||
@@ -481,7 +486,7 @@ jobs:
|
||||
env:
|
||||
envstepb: step_b_env
|
||||
`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["envjobb", "envstepb", "envwf"]);
|
||||
});
|
||||
@@ -509,7 +514,7 @@ jobs:
|
||||
steps:
|
||||
- run: echo "hello \${{ inputs.|
|
||||
`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["another-name", "name", "third-name"]);
|
||||
});
|
||||
@@ -524,7 +529,7 @@ jobs:
|
||||
steps:
|
||||
- run: echo "hello \${{ inputs.|
|
||||
`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
@@ -542,7 +547,7 @@ jobs:
|
||||
- run: echo \${{ github.| }}
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, undefined);
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
expect(result.map(x => x.label)).toContain("actor");
|
||||
});
|
||||
@@ -570,7 +575,7 @@ jobs:
|
||||
steps:
|
||||
- run: echo "hello \${{ github.event.inputs.|
|
||||
`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, undefined);
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["another-name", "name", "third-name"]);
|
||||
});
|
||||
@@ -587,7 +592,7 @@ jobs:
|
||||
- run: echo \${{ github.event.| }}
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, undefined);
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
expect(result.map(x => x.label)).not.toContain("inputs");
|
||||
expect(result.map(x => x.label)).not.toContain("schedule");
|
||||
@@ -607,7 +612,7 @@ jobs:
|
||||
- run: echo \${{ github.event.| }}
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, undefined);
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["repository", "schedule", "workflow"]);
|
||||
});
|
||||
@@ -623,7 +628,7 @@ jobs:
|
||||
- run: echo \${{ github.event.| }}
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, undefined);
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
// forced is part of the push event payload
|
||||
expect(result.map(x => x.label)).toContain("forced");
|
||||
@@ -642,7 +647,7 @@ jobs:
|
||||
- run: echo \${{ github.event.| }}
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, undefined);
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
// We don't validate github.event for workflow_call,
|
||||
// but there should still be auto-completion suggestions
|
||||
@@ -665,7 +670,7 @@ jobs:
|
||||
- id: c
|
||||
run: echo "hello \${{ steps.|
|
||||
`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["a", "b"]);
|
||||
});
|
||||
@@ -681,7 +686,7 @@ jobs:
|
||||
run: echo hello a
|
||||
- run: echo "hello \${{ steps.a.|
|
||||
`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["conclusion", "outcome", "outputs"]);
|
||||
});
|
||||
@@ -700,7 +705,7 @@ jobs:
|
||||
- id: c
|
||||
run: echo hello c
|
||||
`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["a"]);
|
||||
});
|
||||
@@ -717,7 +722,7 @@ jobs:
|
||||
run: echo hello b
|
||||
- run: echo "hello \${{ steps.|
|
||||
`;
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["b"]);
|
||||
});
|
||||
@@ -736,7 +741,7 @@ jobs:
|
||||
- run: npm test > test-job-\${{ | }}.txt
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).not.toContain("strategy");
|
||||
});
|
||||
@@ -757,7 +762,7 @@ jobs:
|
||||
- run: npm test > test-job-\${{ | }}.txt
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toContain("strategy");
|
||||
});
|
||||
@@ -778,7 +783,7 @@ jobs:
|
||||
- run: npm test > test-job-\${{ strategy.| }}.txt
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["fail-fast", "job-index", "job-total", "max-parallel"]);
|
||||
});
|
||||
@@ -797,7 +802,7 @@ jobs:
|
||||
- run: npm test > test-job-\${{ | }}.txt
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).not.toContain("strategy");
|
||||
});
|
||||
@@ -818,7 +823,7 @@ jobs:
|
||||
- run: npm test > test-job-\${{ | }}.txt
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toContain("strategy");
|
||||
});
|
||||
@@ -841,7 +846,7 @@ jobs:
|
||||
node-version: \${{ matrix.| }}
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["node", "os"]);
|
||||
});
|
||||
@@ -873,7 +878,7 @@ jobs:
|
||||
node-version: \${{ matrix.| }}
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["animal", "color", "fruit", "shape"]);
|
||||
});
|
||||
@@ -894,7 +899,7 @@ jobs:
|
||||
node-version: \${{ matrix.| }}
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual([]);
|
||||
});
|
||||
@@ -918,7 +923,7 @@ jobs:
|
||||
node-version: \${{ matrix.| }}
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["animal", "fruit"]);
|
||||
});
|
||||
@@ -940,7 +945,7 @@ jobs:
|
||||
node-version: \${{ matrix.| }}
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.map(x => x.label)).toEqual(["color"]);
|
||||
});
|
||||
@@ -963,7 +968,7 @@ jobs:
|
||||
- run: echo \${{ job.| }}
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
expect(result.map(x => x.label)).toEqual(["container", "services", "status"]);
|
||||
});
|
||||
|
||||
@@ -980,7 +985,7 @@ jobs:
|
||||
run: echo hi
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
expect(result.map(x => x.label)).toEqual([
|
||||
"env",
|
||||
"github",
|
||||
@@ -1014,7 +1019,7 @@ jobs:
|
||||
run: echo hi
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
expect(result.map(x => x.label)).toEqual(["foo"]);
|
||||
});
|
||||
|
||||
@@ -1031,7 +1036,7 @@ jobs:
|
||||
- run: echo \${{ job.container.| }}
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
expect(result.map(x => x.label)).toEqual(["id", "network"]);
|
||||
});
|
||||
|
||||
@@ -1051,7 +1056,7 @@ jobs:
|
||||
- run: echo \${{ job.services.| }}
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
expect(result.map(x => x.label)).toEqual(["nginx", "redis"]);
|
||||
});
|
||||
|
||||
@@ -1072,7 +1077,7 @@ jobs:
|
||||
- run: echo \${{ job.services.nginx.| }}
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
expect(result.map(x => x.label)).toEqual(["id", "network", "ports"]);
|
||||
});
|
||||
});
|
||||
@@ -1089,7 +1094,7 @@ jobs:
|
||||
- run: echo \${{ | }}.txt
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
// Built-in function
|
||||
const toJSON = result.find(x => x.label === "toJson");
|
||||
@@ -1121,7 +1126,7 @@ jobs:
|
||||
- run: echo \${{ toJS|(github.event) }}
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig);
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
|
||||
expect(result.find(x => x.label === "toJson")!.insertText).toBe("toJson");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import {CompletionItem, MarkupContent} from "vscode-languageserver-types";
|
||||
import {complete} from "./complete";
|
||||
import {getPositionFromCursor} from "./test-utils/cursor-position";
|
||||
import {testFileProvider} from "./test-utils/test-file-provider";
|
||||
|
||||
function mapResult(result: CompletionItem[]) {
|
||||
return result.map(x => {
|
||||
return {label: x.label, description: (x.documentation as MarkupContent).value};
|
||||
});
|
||||
}
|
||||
|
||||
describe("completion with reusable workflows", () => {
|
||||
it("completes job inputs", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./reusable-workflow-with-inputs.yaml
|
||||
with:
|
||||
|
|
||||
`;
|
||||
const result = await complete(...getPositionFromCursor(input), {fileProvider: testFileProvider});
|
||||
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(mapResult(result)).toEqual([
|
||||
{
|
||||
label: "name",
|
||||
description: "An optional name"
|
||||
},
|
||||
{
|
||||
label: "username",
|
||||
description: "A username passed from the caller workflow"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("filters out existing job inputs", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./reusable-workflow-with-inputs.yaml
|
||||
with:
|
||||
username: monalisa
|
||||
|
|
||||
`;
|
||||
const result = await complete(...getPositionFromCursor(input), {fileProvider: testFileProvider});
|
||||
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(mapResult(result)).toEqual([
|
||||
{
|
||||
label: "name",
|
||||
description: "An optional name"
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -14,8 +14,9 @@ describe("completion", () => {
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result.length).toEqual(11);
|
||||
expect(result[0].label).toEqual("macos-10.13");
|
||||
expect(result.length).toEqual(12);
|
||||
const labels = result.map(x => x.label);
|
||||
expect(labels).toContain("macos-latest");
|
||||
});
|
||||
|
||||
it("needs", async () => {
|
||||
@@ -46,26 +47,15 @@ jobs:
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted, u|]`;
|
||||
runs-on: [ubuntu-latest, u|]`;
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result.length).toEqual(10);
|
||||
expect(result.length).toEqual(11);
|
||||
|
||||
expect(result[0].label).toEqual("macos-10.13");
|
||||
});
|
||||
|
||||
it("sequence filters out existing values", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [ubuntu-latest, u|]`;
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result.length).toEqual(10);
|
||||
|
||||
expect(result[0].label).toEqual("macos-10.13");
|
||||
const labels = result.map(x => x.label);
|
||||
expect(labels).toContain("macos-latest");
|
||||
expect(labels).not.toContain("ubuntu-latest");
|
||||
});
|
||||
|
||||
it("one-of definition completion", async () => {
|
||||
@@ -192,7 +182,7 @@ jobs:
|
||||
}
|
||||
}
|
||||
};
|
||||
const result = await complete(...getPositionFromCursor(input), config);
|
||||
const result = await complete(...getPositionFromCursor(input), {valueProviderConfig: config});
|
||||
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result.length).toEqual(1);
|
||||
@@ -210,7 +200,7 @@ jobs:
|
||||
}
|
||||
}
|
||||
};
|
||||
const result = await complete(...getPositionFromCursor(input), config);
|
||||
const result = await complete(...getPositionFromCursor(input), {valueProviderConfig: config});
|
||||
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result.length).toEqual(1);
|
||||
|
||||
@@ -13,6 +13,7 @@ import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/in
|
||||
import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token";
|
||||
import {TokenType} from "@github/actions-workflow-parser/templates/tokens/types";
|
||||
import {File} from "@github/actions-workflow-parser/workflows/file";
|
||||
import {FileProvider} from "@github/actions-workflow-parser/workflows/file-provider";
|
||||
import {Position, TextDocument} from "vscode-languageserver-textdocument";
|
||||
import {CompletionItem, CompletionItemKind, CompletionItemTag, Range, TextEdit} from "vscode-languageserver-types";
|
||||
import {ContextProviderConfig} from "./context-providers/config";
|
||||
@@ -43,11 +44,16 @@ export function getExpressionInput(input: string, pos: number): string {
|
||||
return input.substring(startPos, pos);
|
||||
}
|
||||
|
||||
export type CompletionConfig = {
|
||||
valueProviderConfig?: ValueProviderConfig;
|
||||
contextProviderConfig?: ContextProviderConfig;
|
||||
fileProvider?: FileProvider;
|
||||
};
|
||||
|
||||
export async function complete(
|
||||
textDocument: TextDocument,
|
||||
position: Position,
|
||||
valueProviderConfig?: ValueProviderConfig,
|
||||
contextProviderConfig?: ContextProviderConfig
|
||||
config?: CompletionConfig
|
||||
): Promise<CompletionItem[]> {
|
||||
// Edge case: when completing a key like `foo:|`, do not calculate auto-completions
|
||||
const charBeforePos = textDocument.getText({
|
||||
@@ -71,7 +77,15 @@ export async function complete(
|
||||
}
|
||||
|
||||
const {token, keyToken, parent, path} = findToken(newPos, result.value);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(
|
||||
result.context,
|
||||
result.value,
|
||||
ErrorPolicy.TryConversion,
|
||||
config?.fileProvider,
|
||||
{
|
||||
fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0
|
||||
}
|
||||
);
|
||||
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
|
||||
@@ -79,7 +93,7 @@ export async function complete(
|
||||
if (token) {
|
||||
if (isBasicExpression(token) || isPotentiallyExpression(token)) {
|
||||
const allowedContext = token.definitionInfo?.allowedContext || [];
|
||||
const context = await getContext(allowedContext, contextProviderConfig, workflowContext, Mode.Completion);
|
||||
const context = await getContext(allowedContext, config?.contextProviderConfig, workflowContext, Mode.Completion);
|
||||
|
||||
return getExpressionCompletionItems(token, context, newPos);
|
||||
}
|
||||
@@ -88,7 +102,7 @@ export async function complete(
|
||||
const indentation = guessIndentation(newDoc, 2, true); // Use 2 spaces as default and most common for YAML
|
||||
const indentString = " ".repeat(indentation.tabSize);
|
||||
|
||||
const values = await getValues(token, keyToken, parent, valueProviderConfig, workflowContext, indentString);
|
||||
const values = await getValues(token, keyToken, parent, config?.valueProviderConfig, workflowContext, indentString);
|
||||
|
||||
let replaceRange: Range | undefined;
|
||||
if (token?.range) {
|
||||
@@ -203,20 +217,18 @@ function getExpressionCompletionItems(
|
||||
context: DescriptionDictionary,
|
||||
pos: Position
|
||||
): CompletionItem[] {
|
||||
let expressionInput = "";
|
||||
let currentInput = "";
|
||||
let relCharOffset: number = 0;
|
||||
|
||||
if (isBasicExpression(token)) {
|
||||
expressionInput = currentInput = token.expression;
|
||||
relCharOffset = getRelCharOffset(token.range!, expressionInput, pos);
|
||||
currentInput = token.source || token.expression;
|
||||
} else {
|
||||
const stringToken = token.assertString("Expected string token for expression completion");
|
||||
currentInput = stringToken.source || stringToken.value;
|
||||
relCharOffset = getRelCharOffset(stringToken.range!, currentInput, pos);
|
||||
expressionInput = (getExpressionInput(currentInput, relCharOffset) || "").trim();
|
||||
}
|
||||
|
||||
const relCharOffset = getRelCharOffset(token.range!, currentInput, pos);
|
||||
const expressionInput = (getExpressionInput(currentInput, relCharOffset) || "").trim();
|
||||
|
||||
try {
|
||||
return completeExpression(expressionInput, context, [], validatorFunctions).map(item =>
|
||||
mapExpressionCompletionItem(item, currentInput[relCharOffset])
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import {DescriptionDictionary} from "@github/actions-expressions";
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
import {testGetWorkflowContext} from "../test-utils/test-workflow-context";
|
||||
import {getNeedsContext} from "./needs";
|
||||
|
||||
describe("needs context", () => {
|
||||
describe("invalid workflow context", () => {
|
||||
it("jobs not defined", () => {
|
||||
const workflowContext = {} as WorkflowContext;
|
||||
expect(workflowContext.job).toBeUndefined();
|
||||
expect(workflowContext.reusableWorkflowJob).toBeUndefined();
|
||||
|
||||
const context = getNeedsContext(workflowContext);
|
||||
expect(context).toEqual(new DescriptionDictionary());
|
||||
});
|
||||
});
|
||||
|
||||
it("job without needs", async () => {
|
||||
const workflowContext = await testGetWorkflowContext(`on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: ec|ho`);
|
||||
|
||||
const context = getNeedsContext(workflowContext);
|
||||
expect(context).toEqual(new DescriptionDictionary());
|
||||
});
|
||||
|
||||
it("job with needs", async () => {
|
||||
const workflowContext = await testGetWorkflowContext(`on: push
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test]
|
||||
steps:
|
||||
- run: ec|ho`);
|
||||
|
||||
const context = getNeedsContext(workflowContext);
|
||||
expect(context.pairs().map(x => x.key)).toEqual(["test"]);
|
||||
});
|
||||
|
||||
it("reusable job without needs", async () => {
|
||||
const workflowContext = await testGetWorkflowContext(`on: push
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo
|
||||
build:
|
||||
uses: ./.github/workflows/some-reusable-wor|kflow.yml`);
|
||||
|
||||
const context = getNeedsContext(workflowContext);
|
||||
expect(context).toEqual(new DescriptionDictionary());
|
||||
});
|
||||
|
||||
it("reusable job with needs", async () => {
|
||||
const workflowContext = await testGetWorkflowContext(`on: push
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo
|
||||
build:
|
||||
uses: ./.github/workflows/some-reusable-wor|kflow.yml
|
||||
needs: [test]`);
|
||||
|
||||
const context = getNeedsContext(workflowContext);
|
||||
expect(context.pairs().map(x => x.key)).toEqual(["test"]);
|
||||
});
|
||||
|
||||
describe("outputs", () => {
|
||||
it("regular job with outputs", async () => {
|
||||
const workflowContext = await testGetWorkflowContext(`
|
||||
on: push
|
||||
jobs:
|
||||
a:
|
||||
outputs:
|
||||
build_id: my-build-id
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo
|
||||
b:
|
||||
uses: ./.github/workflows/some-reusable-wor|kflow.yml
|
||||
needs: [a]
|
||||
`);
|
||||
|
||||
const context = getNeedsContext(workflowContext);
|
||||
|
||||
const needs = context.get("a") as DescriptionDictionary;
|
||||
expect(needs).toBeDefined();
|
||||
|
||||
const outputs = needs.get("outputs") as DescriptionDictionary;
|
||||
expect(outputs).toBeDefined();
|
||||
|
||||
expect(outputs.pairs().map(x => x.key)).toEqual(["build_id"]);
|
||||
});
|
||||
|
||||
it("reusable job with outputs", async () => {
|
||||
const workflowContext = await testGetWorkflowContext(`
|
||||
on: push
|
||||
jobs:
|
||||
a:
|
||||
uses: ./reusable-workflow-with-outputs.yaml
|
||||
|
||||
b:
|
||||
needs: [a]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: ec|ho
|
||||
`);
|
||||
|
||||
const context = getNeedsContext(workflowContext);
|
||||
|
||||
const needs = context.get("a") as DescriptionDictionary;
|
||||
expect(needs).toBeDefined();
|
||||
|
||||
const outputs = needs.get("outputs") as DescriptionDictionary;
|
||||
expect(outputs).toBeDefined();
|
||||
|
||||
expect(outputs.pairs().map(x => x.key)).toEqual(["build_id"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,18 @@
|
||||
import {data, DescriptionDictionary} from "@github/actions-expressions";
|
||||
import {isScalar, isString} from "@github/actions-workflow-parser";
|
||||
import {isJob} from "@github/actions-workflow-parser/model/type-guards";
|
||||
import {Job, WorkflowJob} from "@github/actions-workflow-parser/model/workflow-template";
|
||||
import {WorkflowJob} from "@github/actions-workflow-parser/model/workflow-template";
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
|
||||
export function getNeedsContext(workflowContext: WorkflowContext): DescriptionDictionary {
|
||||
const d = new DescriptionDictionary();
|
||||
if (!workflowContext.job || !workflowContext.job.needs) {
|
||||
|
||||
const job = workflowContext.job || workflowContext.reusableWorkflowJob;
|
||||
|
||||
if (!job?.needs) {
|
||||
return d;
|
||||
}
|
||||
|
||||
for (const jobID of workflowContext.job.needs) {
|
||||
for (const jobID of job.needs) {
|
||||
const job = workflowContext.template?.jobs.find(job => job.id.value === jobID.value);
|
||||
d.add(jobID.value, needsJobContext(job));
|
||||
}
|
||||
@@ -22,7 +24,7 @@ function needsJobContext(job?: WorkflowJob): DescriptionDictionary {
|
||||
// https://docs.github.com/en/actions/learn-github-actions/contexts#needs-context
|
||||
const d = new DescriptionDictionary();
|
||||
|
||||
if (job && isJob(job)) {
|
||||
if (job) {
|
||||
d.add("outputs", jobOutputs(job));
|
||||
}
|
||||
|
||||
@@ -31,7 +33,7 @@ function needsJobContext(job?: WorkflowJob): DescriptionDictionary {
|
||||
return d;
|
||||
}
|
||||
|
||||
function jobOutputs(job?: Job): DescriptionDictionary {
|
||||
function jobOutputs(job?: WorkflowJob): DescriptionDictionary {
|
||||
const d = new DescriptionDictionary();
|
||||
if (!job?.outputs) {
|
||||
return d;
|
||||
|
||||
@@ -1,30 +1,5 @@
|
||||
import {convertWorkflowTemplate, parseWorkflow, WorkflowTemplate} from "@github/actions-workflow-parser";
|
||||
import {ActionStep, RunStep} from "@github/actions-workflow-parser/model/workflow-template";
|
||||
import {nullTrace} from "../nulltrace";
|
||||
import {getPositionFromCursor} from "../test-utils/cursor-position";
|
||||
import {findToken} from "../utils/find-token";
|
||||
import {getWorkflowContext, WorkflowContext} from "./workflow-context";
|
||||
|
||||
async function testGetWorkflowContext(input: string): Promise<WorkflowContext> {
|
||||
const [textDocument, pos] = getPositionFromCursor(input);
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
content: textDocument.getText(),
|
||||
name: "wf.yaml"
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
let template: WorkflowTemplate | undefined;
|
||||
|
||||
if (result.value) {
|
||||
template = await convertWorkflowTemplate(result.context, result.value);
|
||||
}
|
||||
|
||||
const {path} = findToken(pos, result.value);
|
||||
|
||||
return getWorkflowContext(textDocument.uri, template, path);
|
||||
}
|
||||
import {testGetWorkflowContext} from "../test-utils/test-workflow-context";
|
||||
|
||||
describe("getWorkflowContext", () => {
|
||||
it("context for workflow", async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {parseWorkflow} from "@github/actions-workflow-parser/.";
|
||||
import {parseWorkflow} from "@github/actions-workflow-parser";
|
||||
import {File} from "@github/actions-workflow-parser/workflows/file";
|
||||
import {nullTrace} from "../nulltrace";
|
||||
import {getPositionFromCursor} from "../test-utils/cursor-position";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {isString} from "@github/actions-workflow-parser/.";
|
||||
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";
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import {FileProvider} from "@github/actions-workflow-parser/workflows/file-provider";
|
||||
import {fileIdentifier} from "@github/actions-workflow-parser/workflows/file-reference";
|
||||
|
||||
export const testFileProvider: FileProvider = {
|
||||
getFileContent: async ref => {
|
||||
switch (fileIdentifier(ref)) {
|
||||
case "monalisa/octocat/workflow.yaml@main":
|
||||
return {
|
||||
name: "monalisa/octocat/workflow.yaml",
|
||||
content: `
|
||||
on: workflow_call
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
`
|
||||
};
|
||||
|
||||
case "monalisa/octocat/.github/workflows/non-reusable-workflow.yaml@main":
|
||||
return {
|
||||
name: "monalisa/octocat/.github/workflows/non-reusable-workflow.yaml",
|
||||
content: `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
`
|
||||
};
|
||||
|
||||
case "./reusable-workflow.yaml":
|
||||
return {
|
||||
name: "reusable-workflow.yaml",
|
||||
content: `
|
||||
on: workflow_call
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
`
|
||||
};
|
||||
|
||||
case "./reusable-workflow-with-inputs.yaml":
|
||||
return {
|
||||
name: "reusable-workflow-with-inputs.yaml",
|
||||
content: `
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
username:
|
||||
description: 'A username passed from the caller workflow'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
name:
|
||||
description: 'An optional name'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
`
|
||||
};
|
||||
|
||||
case "./reusable-workflow-with-outputs.yaml":
|
||||
return {
|
||||
name: "reusable-workflow-with-outputs.yaml",
|
||||
content: `
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
build_id:
|
||||
description: 'The resulting build ID'
|
||||
value: 123
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
`
|
||||
};
|
||||
|
||||
default:
|
||||
throw new Error("File not found");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import {convertWorkflowTemplate, parseWorkflow, WorkflowTemplate} from "@github/actions-workflow-parser";
|
||||
import {getWorkflowContext, WorkflowContext} from "../context/workflow-context";
|
||||
import {nullTrace} from "../nulltrace";
|
||||
import {findToken} from "../utils/find-token";
|
||||
import {getPositionFromCursor} from "./cursor-position";
|
||||
import {testFileProvider} from "./test-file-provider";
|
||||
|
||||
export async function testGetWorkflowContext(input: string): Promise<WorkflowContext> {
|
||||
const [textDocument, pos] = getPositionFromCursor(input);
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
content: textDocument.getText(),
|
||||
name: "wf.yaml"
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
let template: WorkflowTemplate | undefined;
|
||||
|
||||
if (result.value) {
|
||||
template = await convertWorkflowTemplate(result.context, result.value, undefined, testFileProvider, {
|
||||
fetchReusableWorkflowDepth: 1
|
||||
});
|
||||
}
|
||||
|
||||
const {path} = findToken(pos, result.value);
|
||||
|
||||
return getWorkflowContext(textDocument.uri, template, path);
|
||||
}
|
||||
@@ -118,15 +118,20 @@ async function additionalValidations(
|
||||
|
||||
// Allowed values coming from the schema have already been validated. Only check if
|
||||
// a value provider is defined for a token and if it is, validate the values match.
|
||||
if (config?.valueProviderConfig && token.range && validationDefinition) {
|
||||
if (token.range && validationDefinition) {
|
||||
const defKey = validationDefinition.key;
|
||||
if (defKey === "step-with") {
|
||||
// Action inputs should be validated already in validateAction
|
||||
continue;
|
||||
}
|
||||
|
||||
if (defKey === "workflow-job-with") {
|
||||
// Reusable workflow job inputs are validated by the parser
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try a custom value provider first
|
||||
let valueProvider = config.valueProviderConfig[defKey];
|
||||
let valueProvider = config?.valueProviderConfig?.[defKey];
|
||||
if (!valueProvider) {
|
||||
// fall back to default
|
||||
valueProvider = defaultValueProviders[defKey];
|
||||
|
||||
@@ -1,76 +1,7 @@
|
||||
import {FileProvider} from "@github/actions-workflow-parser/workflows/file-provider";
|
||||
import {fileIdentifier} from "@github/actions-workflow-parser/workflows/file-reference";
|
||||
import {createDocument} from "./test-utils/document";
|
||||
import {testFileProvider} from "./test-utils/test-file-provider";
|
||||
import {validate} from "./validate";
|
||||
|
||||
const testFileProvider: FileProvider = {
|
||||
getFileContent: async ref => {
|
||||
switch (fileIdentifier(ref)) {
|
||||
case "monalisa/octocat/workflow.yaml@main":
|
||||
return {
|
||||
name: "monalisa/octocat/workflow.yaml",
|
||||
content: `
|
||||
on: workflow_call
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
`
|
||||
};
|
||||
|
||||
case "monalisa/octocat/.github/workflows/non-reusable-workflow.yaml@main":
|
||||
return {
|
||||
name: "monalisa/octocat/.github/workflows/non-reusable-workflow.yaml",
|
||||
content: `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
`
|
||||
};
|
||||
|
||||
case "./reusable-workflow.yaml":
|
||||
return {
|
||||
name: "reusable-workflow.yaml",
|
||||
content: `
|
||||
on: workflow_call
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
`
|
||||
};
|
||||
|
||||
case "./reusable-workflow-with-inputs.yaml":
|
||||
return {
|
||||
name: "reusable-workflow-with-inputs.yaml",
|
||||
content: `
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
username:
|
||||
description: 'A username passed from the caller workflow'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
`
|
||||
};
|
||||
|
||||
default:
|
||||
throw new Error("File not found");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe("workflow references validation", () => {
|
||||
it("invalid workflow reference", async () => {
|
||||
const input = `
|
||||
|
||||
@@ -1,28 +1,35 @@
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
import {ValueProviderConfig, ValueProviderKind} from "./config";
|
||||
import {needs} from "./needs";
|
||||
import {reusableJobInputs} from "./reusable-job-inputs";
|
||||
import {stringsToValues} from "./strings-to-values";
|
||||
|
||||
export const DEFAULT_RUNNER_LABELS = [
|
||||
"ubuntu-latest",
|
||||
"ubuntu-22.04",
|
||||
"ubuntu-20.04",
|
||||
"ubuntu-18.04",
|
||||
"windows-latest",
|
||||
"windows-2022",
|
||||
"windows-2019",
|
||||
"macos-latest",
|
||||
"macos-12",
|
||||
"macos-11",
|
||||
"macos-10.15",
|
||||
"self-hosted"
|
||||
];
|
||||
|
||||
export const defaultValueProviders: ValueProviderConfig = {
|
||||
needs: {
|
||||
kind: ValueProviderKind.AllowedValues,
|
||||
get: needs
|
||||
},
|
||||
"workflow-job-with": {
|
||||
kind: ValueProviderKind.AllowedValues,
|
||||
get: async context => reusableJobInputs(context)
|
||||
},
|
||||
"runs-on": {
|
||||
kind: ValueProviderKind.SuggestedValues,
|
||||
get: async (_: WorkflowContext) =>
|
||||
stringsToValues([
|
||||
"ubuntu-latest",
|
||||
"ubuntu-18.04",
|
||||
"ubuntu-16.04",
|
||||
"windows-latest",
|
||||
"windows-2019",
|
||||
"windows-2016",
|
||||
"macos-latest",
|
||||
"macos-10.15",
|
||||
"macos-10.14",
|
||||
"macos-10.13",
|
||||
"self-hosted"
|
||||
])
|
||||
get: async (_: WorkflowContext) => stringsToValues(DEFAULT_RUNNER_LABELS)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token";
|
||||
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
|
||||
import {isMapping, isString} from "@github/actions-workflow-parser/templates/tokens/type-guards";
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
import {Value} from "./config";
|
||||
import {stringsToValues} from "./strings-to-values";
|
||||
|
||||
export function reusableJobInputs(context: WorkflowContext): Value[] {
|
||||
if (!context.reusableWorkflowJob?.["input-definitions"]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const values: Value[] = [];
|
||||
|
||||
for (const {key, value} of context.reusableWorkflowJob["input-definitions"]) {
|
||||
if (!isString(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
values.push({
|
||||
label: key.value,
|
||||
description: inputDescription(value)
|
||||
});
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
function inputDescription(inputDef: TemplateToken): string | undefined {
|
||||
if (!isMapping(inputDef)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const descriptionToken = inputDef.find("description");
|
||||
if (!descriptionToken || !isString(descriptionToken)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return descriptionToken.value;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@github/actions-workflow-parser",
|
||||
"version": "0.1.126",
|
||||
"version": "0.1.132",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"source": "./src/index.ts",
|
||||
@@ -40,7 +40,7 @@
|
||||
"watch": "tsc --build tsconfig.build.json --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@github/actions-expressions": "^0.1.126",
|
||||
"@github/actions-expressions": "^0.1.132",
|
||||
"cronstrue": "^2.21.0",
|
||||
"yaml": "^2.0.0-8"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {TemplateContext} from "../../templates/template-context";
|
||||
import {StringToken, MappingToken, BasicExpressionToken, TemplateToken, ScalarToken} from "../../templates/tokens";
|
||||
import {BasicExpressionToken, MappingToken, ScalarToken, StringToken, TemplateToken} from "../../templates/tokens";
|
||||
import {isSequence, isString} from "../../templates/tokens/type-guards";
|
||||
import {WorkflowJob, Step} from "../workflow-template";
|
||||
import {Step, WorkflowJob} from "../workflow-template";
|
||||
import {convertConcurrency} from "./concurrency";
|
||||
import {convertToJobContainer, convertToJobServices} from "./container";
|
||||
import {handleTemplateTokenErrors} from "./handle-errors";
|
||||
@@ -103,7 +103,7 @@ export function convertJob(context: TemplateContext, jobKey: StringToken, token:
|
||||
type: "reusableWorkflowJob",
|
||||
id: jobKey,
|
||||
name: jobName(name, jobKey),
|
||||
needs: needs ?? [],
|
||||
needs: needs || [],
|
||||
if: new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined),
|
||||
ref: workflowJobRef,
|
||||
"input-definitions": undefined,
|
||||
|
||||
@@ -606,9 +606,9 @@ class TemplateReader {
|
||||
this._fileId,
|
||||
token.range,
|
||||
`format('${format.join("")}'${args.join("")})`,
|
||||
token.definitionInfo,
|
||||
definitionInfo,
|
||||
expressionTokens,
|
||||
undefined
|
||||
raw
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "browser-playground",
|
||||
"version": "0.1.126",
|
||||
"version": "0.1.132",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@github/actions-languageserver": "^0.1.126",
|
||||
"@github/actions-languageserver": "^0.1.132",
|
||||
"monaco-editor-webpack-plugin": "^7.0.1",
|
||||
"monaco-editor-workers": "^0.34.2",
|
||||
"monaco-languageclient": "^4.0.3",
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.1.126"
|
||||
"version": "0.1.132"
|
||||
}
|
||||
|
||||
Generated
+17
-17
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"actions-expressions": {
|
||||
"name": "@github/actions-expressions",
|
||||
"version": "0.1.126",
|
||||
"version": "0.1.132",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.0.3",
|
||||
@@ -34,11 +34,11 @@
|
||||
},
|
||||
"actions-languageserver": {
|
||||
"name": "@github/actions-languageserver",
|
||||
"version": "0.1.126",
|
||||
"version": "0.1.132",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-languageservice": "^0.1.126",
|
||||
"@github/actions-workflow-parser": "^0.1.126",
|
||||
"@github/actions-languageservice": "^0.1.132",
|
||||
"@github/actions-workflow-parser": "^0.1.132",
|
||||
"@octokit/rest": "^19.0.7",
|
||||
"vscode-languageserver": "^8.0.2",
|
||||
"vscode-languageserver-textdocument": "^1.0.7",
|
||||
@@ -59,11 +59,11 @@
|
||||
},
|
||||
"actions-languageservice": {
|
||||
"name": "@github/actions-languageservice",
|
||||
"version": "0.1.126",
|
||||
"version": "0.1.132",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-expressions": "^0.1.126",
|
||||
"@github/actions-workflow-parser": "^0.1.126",
|
||||
"@github/actions-expressions": "^0.1.132",
|
||||
"@github/actions-workflow-parser": "^0.1.132",
|
||||
"vscode-languageserver-textdocument": "^1.0.7",
|
||||
"vscode-languageserver-types": "^3.17.2",
|
||||
"yaml": "^2.1.1"
|
||||
@@ -82,10 +82,10 @@
|
||||
},
|
||||
"actions-workflow-parser": {
|
||||
"name": "@github/actions-workflow-parser",
|
||||
"version": "0.1.126",
|
||||
"version": "0.1.132",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-expressions": "^0.1.126",
|
||||
"@github/actions-expressions": "^0.1.132",
|
||||
"cronstrue": "^2.21.0",
|
||||
"yaml": "^2.0.0-8"
|
||||
},
|
||||
@@ -105,10 +105,10 @@
|
||||
}
|
||||
},
|
||||
"browser-playground": {
|
||||
"version": "0.1.126",
|
||||
"version": "0.1.132",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-languageserver": "^0.1.126",
|
||||
"@github/actions-languageserver": "^0.1.132",
|
||||
"monaco-editor-webpack-plugin": "^7.0.1",
|
||||
"monaco-editor-workers": "^0.34.2",
|
||||
"monaco-languageclient": "^4.0.3",
|
||||
@@ -14570,8 +14570,8 @@
|
||||
"@github/actions-languageserver": {
|
||||
"version": "file:actions-languageserver",
|
||||
"requires": {
|
||||
"@github/actions-languageservice": "^0.1.126",
|
||||
"@github/actions-workflow-parser": "^0.1.126",
|
||||
"@github/actions-languageservice": "^0.1.132",
|
||||
"@github/actions-workflow-parser": "^0.1.132",
|
||||
"@octokit/rest": "^19.0.7",
|
||||
"@types/jest": "^29.0.3",
|
||||
"fetch-mock": "^9.11.0",
|
||||
@@ -14588,8 +14588,8 @@
|
||||
"@github/actions-languageservice": {
|
||||
"version": "file:actions-languageservice",
|
||||
"requires": {
|
||||
"@github/actions-expressions": "^0.1.126",
|
||||
"@github/actions-workflow-parser": "^0.1.126",
|
||||
"@github/actions-expressions": "^0.1.132",
|
||||
"@github/actions-workflow-parser": "^0.1.132",
|
||||
"@types/jest": "^29.0.3",
|
||||
"jest": "^29.0.3",
|
||||
"prettier": "^2.8.3",
|
||||
@@ -14604,7 +14604,7 @@
|
||||
"@github/actions-workflow-parser": {
|
||||
"version": "file:actions-workflow-parser",
|
||||
"requires": {
|
||||
"@github/actions-expressions": "^0.1.126",
|
||||
"@github/actions-expressions": "^0.1.132",
|
||||
"@types/jest": "^29.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
||||
"@typescript-eslint/parser": "^5.40.0",
|
||||
@@ -17562,7 +17562,7 @@
|
||||
"browser-playground": {
|
||||
"version": "file:browser-playground",
|
||||
"requires": {
|
||||
"@github/actions-languageserver": "^0.1.126",
|
||||
"@github/actions-languageserver": "^0.1.132",
|
||||
"css-loader": "^6.7.2",
|
||||
"monaco-editor-webpack-plugin": "^7.0.1",
|
||||
"monaco-editor-workers": "^0.34.2",
|
||||
|
||||
Reference in New Issue
Block a user