0fe31c6656
* Setup CodeActions and add quickfix for missing inputs * PR feedback * Update languageservice/src/code-actions/quickfix/add-missing-inputs.ts Co-authored-by: Salman Chishti <salmanmkc@GitHub.com> * Fix indentSize detection for code actions after rebase - Add indentSize to MissingInputsDiagnosticData interface - Pass indentSize parameter from validate.ts to validateActionReference - Detect indentSize from workflow structure (jobs key to first child) - Fall back to detecting from with: block children when available * update typescript * formatting * linting * Gate missing inputs quickfix behind feature flag * Address PR review: rename files, move position calculation to quickfix - Rename index.ts files to follow repo patterns: - code-actions/index.ts → code-actions/code-actions.ts - code-actions/quickfix/index.ts → quickfix/quickfix-providers.ts - Move position calculation from validation to quickfix: - MissingInputsDiagnosticData now passes raw token ranges - Quickfix computes insertion position and indentation at code action time - detectIndentSize moved from validate.ts to validate-action-reference.ts * wip * Remove pointless comment --------- Co-authored-by: Salman Chishti <salmanmkc@GitHub.com>
421 lines
10 KiB
TypeScript
421 lines
10 KiB
TypeScript
import {DiagnosticSeverity} from "vscode-languageserver-types";
|
|
import {ActionMetadata, ActionReference} from "./action.js";
|
|
import {registerLogger} from "./log.js";
|
|
import {createDocument} from "./test-utils/document.js";
|
|
import {TestLogger} from "./test-utils/logger.js";
|
|
import {validate, ValidationConfig} from "./validate.js";
|
|
import {ValueProviderKind} from "./value-providers/config.js";
|
|
import {clearCache} from "./utils/workflow-cache.js";
|
|
|
|
registerLogger(new TestLogger());
|
|
|
|
beforeEach(() => {
|
|
clearCache();
|
|
});
|
|
|
|
const validationConfig: ValidationConfig = {
|
|
actionsMetadataProvider: {
|
|
fetchActionMetadata: (ref: ActionReference) => {
|
|
let metadata: ActionMetadata | undefined = undefined;
|
|
switch (ref.owner + "/" + ref.name + "@" + ref.ref) {
|
|
case "actions/checkout@v3":
|
|
metadata = {
|
|
name: "Checkout",
|
|
description: "Checkout a Git repository at a particular version",
|
|
inputs: {
|
|
repository: {
|
|
description: "Repository name with owner",
|
|
default: "${{ github.repository }}"
|
|
}
|
|
}
|
|
};
|
|
break;
|
|
case "actions/setup-node@v1":
|
|
metadata = {
|
|
name: "Setup Node.js environment",
|
|
description:
|
|
"Setup a Node.js environment by adding problem matchers and optionally downloading and adding it to the PATH.",
|
|
inputs: {
|
|
version: {
|
|
description: "Deprecated. Use node-version instead. Will not be supported after October 1, 2019",
|
|
deprecationMessage:
|
|
"The version property will not be supported after October 1, 2019. Use node-version instead"
|
|
}
|
|
}
|
|
};
|
|
break;
|
|
case "actions/deploy-pages@main":
|
|
metadata = {
|
|
name: "Deploy GitHub Pages site",
|
|
description: "A GitHub Action to deploy an artifact as a GitHub Pages site",
|
|
inputs: {
|
|
token: {
|
|
required: true,
|
|
description: "token to use",
|
|
default: "${{ github.token }}"
|
|
}
|
|
}
|
|
};
|
|
break;
|
|
case "actions/cache@v1":
|
|
metadata = {
|
|
name: "Cache",
|
|
description: "Cache artifacts like dependencies and build outputs to improve workflow execution time",
|
|
inputs: {
|
|
path: {
|
|
description: "A directory to store and save the cache",
|
|
required: true
|
|
},
|
|
key: {
|
|
description: "An explicit key for restoring and saving the cache",
|
|
required: true
|
|
},
|
|
"restore-keys": {
|
|
description: "An ordered list of keys to use for restoring the cache if no cache hit occurred for key",
|
|
required: false
|
|
}
|
|
}
|
|
};
|
|
break;
|
|
case "actions/action-no-input@v1":
|
|
metadata = {
|
|
name: "Action with no inputs",
|
|
description: "An action with no inputs"
|
|
};
|
|
}
|
|
return Promise.resolve(metadata);
|
|
}
|
|
}
|
|
};
|
|
|
|
describe("validate action steps", () => {
|
|
it("valid action reference", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input), validationConfig);
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("no actionsMetadataProvider", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/does-not-exist@v3
|
|
`;
|
|
const config: ValidationConfig = {};
|
|
const result = await validate(createDocument("wf.yaml", input), config);
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("action does not exist", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/does-not-exist@v3
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input), validationConfig);
|
|
|
|
expect(result).toEqual([
|
|
{
|
|
message: "Unable to resolve action `actions/does-not-exist@v3`, repository or version not found",
|
|
range: {
|
|
end: {
|
|
character: 41,
|
|
line: 6
|
|
},
|
|
start: {
|
|
character: 16,
|
|
line: 6
|
|
}
|
|
},
|
|
severity: DiagnosticSeverity.Error
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("action does not define inputs", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/action-no-input@v1
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input), validationConfig);
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("invalid input", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
with:
|
|
notanoption: true
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input), validationConfig);
|
|
|
|
expect(result).toEqual([
|
|
{
|
|
message: "Invalid action input 'notanoption'",
|
|
range: {
|
|
end: {
|
|
character: 19,
|
|
line: 8
|
|
},
|
|
start: {
|
|
character: 8,
|
|
line: 8
|
|
}
|
|
},
|
|
severity: DiagnosticSeverity.Error
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("deprecated input", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/setup-node@v1
|
|
with:
|
|
version: 10
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input), validationConfig);
|
|
|
|
expect(result).toEqual([
|
|
{
|
|
message: "The version property will not be supported after October 1, 2019. Use node-version instead",
|
|
range: {
|
|
end: {
|
|
character: 15,
|
|
line: 8
|
|
},
|
|
start: {
|
|
character: 8,
|
|
line: 8
|
|
}
|
|
},
|
|
severity: DiagnosticSeverity.Warning
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("missing required input", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/cache@v1
|
|
with:
|
|
key: \${{ runner.os }}-node-\${{ hashFiles('**/package-lock.json') }}
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input), validationConfig);
|
|
|
|
expect(result).toEqual([
|
|
{
|
|
message: "Missing required input `path`",
|
|
range: {
|
|
end: {
|
|
character: 10,
|
|
line: 7
|
|
},
|
|
start: {
|
|
character: 6,
|
|
line: 7
|
|
}
|
|
},
|
|
severity: DiagnosticSeverity.Error,
|
|
code: "missing-required-inputs",
|
|
data: {
|
|
action: {
|
|
name: "cache",
|
|
owner: "actions",
|
|
ref: "v1"
|
|
},
|
|
missingInputs: [
|
|
{
|
|
default: undefined,
|
|
name: "path"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("required input with default value", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/deploy-pages@main
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input), validationConfig);
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("multiple missing required inputs", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/cache@v1
|
|
with:
|
|
restore-keys: \${{ runner.os }}-node-
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input), validationConfig);
|
|
|
|
expect(result).toEqual([
|
|
{
|
|
message: "Missing required inputs: `path`, `key`",
|
|
range: {
|
|
end: {
|
|
character: 10,
|
|
line: 7
|
|
},
|
|
start: {
|
|
character: 6,
|
|
line: 7
|
|
}
|
|
},
|
|
severity: DiagnosticSeverity.Error,
|
|
code: "missing-required-inputs",
|
|
data: {
|
|
action: {
|
|
name: "cache",
|
|
owner: "actions",
|
|
ref: "v1"
|
|
},
|
|
missingInputs: [
|
|
{
|
|
default: undefined,
|
|
name: "path"
|
|
},
|
|
{
|
|
default: undefined,
|
|
name: "key"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("missing required inputs without a `with` key", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/cache@v1
|
|
`;
|
|
const result = await validate(createDocument("wf.yaml", input), validationConfig);
|
|
|
|
expect(result).toEqual([
|
|
{
|
|
message: "Missing required inputs: `path`, `key`",
|
|
range: {
|
|
end: {
|
|
character: 0,
|
|
line: 7
|
|
},
|
|
start: {
|
|
character: 6,
|
|
line: 6
|
|
}
|
|
},
|
|
severity: DiagnosticSeverity.Error,
|
|
code: "missing-required-inputs",
|
|
data: {
|
|
action: {
|
|
name: "cache",
|
|
owner: "actions",
|
|
ref: "v1"
|
|
},
|
|
missingInputs: [
|
|
{
|
|
default: undefined,
|
|
name: "path"
|
|
},
|
|
{
|
|
default: undefined,
|
|
name: "key"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("skips extra validation from custom value provider", async () => {
|
|
const input = `
|
|
on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
with:
|
|
notanoption: true
|
|
`;
|
|
const config = validationConfig;
|
|
config.valueProviderConfig = {
|
|
"step-with": {
|
|
kind: ValueProviderKind.AllowedValues,
|
|
get: () => {
|
|
return Promise.resolve([{label: "repository", description: "Repository name with owner."}]);
|
|
}
|
|
}
|
|
};
|
|
const result = await validate(createDocument("wf.yaml", input), config);
|
|
|
|
expect(result).toEqual([
|
|
{
|
|
message: "Invalid action input 'notanoption'",
|
|
range: {
|
|
end: {
|
|
character: 19,
|
|
line: 8
|
|
},
|
|
start: {
|
|
character: 8,
|
|
line: 8
|
|
}
|
|
},
|
|
severity: DiagnosticSeverity.Error
|
|
}
|
|
]);
|
|
});
|
|
});
|