Context-aware autocomplete and validation for action.yml runs section (#289)
- Set main as required in node-runs-strict schema definition - Add validation for invalid key combinations based on using value - Add validation for missing required keys (main for node, steps for composite, image for docker) - Filter autocomplete suggestions based on using value - Prioritize 'using' in completions when not set yet Fixes context-aware autocomplete for action.yml files where different action types (node, composite, docker) have different valid keys under runs:
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
import {isMapping} from "@actions/workflow-parser";
|
||||
import {isActionStep} from "@actions/workflow-parser/model/type-guards";
|
||||
import {ErrorPolicy} from "@actions/workflow-parser/model/convert";
|
||||
import {MappingToken} from "@actions/workflow-parser/templates/tokens/mapping-token";
|
||||
import {SequenceToken} from "@actions/workflow-parser/templates/tokens/sequence-token";
|
||||
import {TemplateToken} from "@actions/workflow-parser/templates/tokens/template-token";
|
||||
import {File} from "@actions/workflow-parser/workflows/file";
|
||||
@@ -16,6 +17,31 @@ import {getOrConvertActionTemplate, getOrParseAction} from "./utils/workflow-cac
|
||||
import {validateActionReference} from "./validate-action-reference.js";
|
||||
import {ValidationConfig} from "./validate.js";
|
||||
|
||||
/**
|
||||
* Valid keys for each action type under the `runs:` section.
|
||||
* Source: https://github.com/actions/runner/blob/main/src/Runner.Worker/ActionManifestManager.cs
|
||||
*/
|
||||
const NODE_KEYS = new Set(["using", "main", "pre", "post", "pre-if", "post-if"]);
|
||||
const COMPOSITE_KEYS = new Set(["using", "steps"]);
|
||||
const DOCKER_KEYS = new Set([
|
||||
"using",
|
||||
"image",
|
||||
"args",
|
||||
"env",
|
||||
"entrypoint",
|
||||
"pre-entrypoint",
|
||||
"pre-if",
|
||||
"post-entrypoint",
|
||||
"post-if"
|
||||
]);
|
||||
|
||||
/**
|
||||
* Required keys for each action type (besides 'using').
|
||||
*/
|
||||
const NODE_REQUIRED_KEYS = ["main"];
|
||||
const COMPOSITE_REQUIRED_KEYS = ["steps"];
|
||||
const DOCKER_REQUIRED_KEYS = ["image"];
|
||||
|
||||
/**
|
||||
* Validates an action.yml file
|
||||
*
|
||||
@@ -57,6 +83,12 @@ export async function validateAction(textDocument: TextDocument, config?: Valida
|
||||
});
|
||||
}
|
||||
|
||||
// Validate runs key combinations based on using type
|
||||
if (result.value) {
|
||||
const runsKeyDiagnostics = validateRunsKeys(result.value);
|
||||
diagnostics.push(...runsKeyDiagnostics);
|
||||
}
|
||||
|
||||
// Validate composite action steps if we have a parsed result
|
||||
if (result.value) {
|
||||
const template = getOrConvertActionTemplate(result.context, result.value, textDocument.uri, {
|
||||
@@ -102,3 +134,103 @@ function findStepsSequence(root: TemplateToken): SequenceToken | undefined {
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the keys under `runs:` are valid for the specified `using:` type.
|
||||
*/
|
||||
function validateRunsKeys(root: TemplateToken): Diagnostic[] {
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
|
||||
// Find the runs mapping from the root
|
||||
let runsMapping: MappingToken | undefined;
|
||||
if (root instanceof MappingToken) {
|
||||
for (let i = 0; i < root.count; i++) {
|
||||
const {key, value} = root.get(i);
|
||||
if (key.toString().toLowerCase() === "runs" && value instanceof MappingToken) {
|
||||
runsMapping = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!runsMapping) {
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
// Get the using value from the runs mapping
|
||||
let usingValue: string | undefined;
|
||||
for (let i = 0; i < runsMapping.count; i++) {
|
||||
const {key, value} = runsMapping.get(i);
|
||||
if (key.toString().toLowerCase() === "using") {
|
||||
usingValue = value.toString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!usingValue) {
|
||||
return diagnostics; // No using value, let schema validation handle it
|
||||
}
|
||||
|
||||
// Determine allowed keys, required keys, and action type name
|
||||
let allowedKeys: Set<string>;
|
||||
let requiredKeys: string[];
|
||||
let actionType: string;
|
||||
|
||||
if (usingValue.match(/^node\d+$/i)) {
|
||||
allowedKeys = NODE_KEYS;
|
||||
requiredKeys = NODE_REQUIRED_KEYS;
|
||||
actionType = "Node.js";
|
||||
} else if (usingValue.toLowerCase() === "composite") {
|
||||
allowedKeys = COMPOSITE_KEYS;
|
||||
requiredKeys = COMPOSITE_REQUIRED_KEYS;
|
||||
actionType = "composite";
|
||||
} else if (usingValue.toLowerCase() === "docker") {
|
||||
allowedKeys = DOCKER_KEYS;
|
||||
requiredKeys = DOCKER_REQUIRED_KEYS;
|
||||
actionType = "Docker";
|
||||
} else {
|
||||
return diagnostics; // Unknown type, let schema validation handle it
|
||||
}
|
||||
|
||||
// Get all present keys
|
||||
const presentKeys = new Set<string>();
|
||||
for (let i = 0; i < runsMapping.count; i++) {
|
||||
const {key} = runsMapping.get(i);
|
||||
presentKeys.add(key.toString().toLowerCase());
|
||||
}
|
||||
|
||||
// Check for invalid keys
|
||||
for (let i = 0; i < runsMapping.count; i++) {
|
||||
const {key} = runsMapping.get(i);
|
||||
const keyStr = key.toString().toLowerCase();
|
||||
|
||||
if (!allowedKeys.has(keyStr)) {
|
||||
diagnostics.push({
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: mapRange(key.range),
|
||||
message: `'${key.toString()}' is not valid for ${actionType} actions (using: ${usingValue})`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check for missing required keys
|
||||
for (const requiredKey of requiredKeys) {
|
||||
if (!presentKeys.has(requiredKey)) {
|
||||
// Find the 'using' key to report the error location
|
||||
let usingKeyRange = runsMapping.range;
|
||||
for (let i = 0; i < runsMapping.count; i++) {
|
||||
const {key} = runsMapping.get(i);
|
||||
if (key.toString().toLowerCase() === "using") {
|
||||
usingKeyRange = key.range;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
diagnostics.push({
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: mapRange(usingKeyRange),
|
||||
message: `'${requiredKey}' is required for ${actionType} actions (using: ${usingValue})`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user