33291f0f8d
* Add missing validation for action.yml (parity with workflow files)
- Add uses format validation for composite action steps
- Validates owner/repo@ref format
- Supports docker:// and ./ local references
- Warns about shortened SHA refs (security concern)
- Detects reusable workflow references in wrong context
- Add if literal text detection for composite action steps
- Detects literal text outside ${{ }} that makes conditions always truthy
- Works for both plain string and mixed expression formats
- Uses shared hasFormatWithLiteralText() utility
- Add pre-if/post-if validation for node and docker actions
- Errors on explicit ${{ }} syntax (runner only supports implicit expressions)
- Literal text detection for implicit expressions
- New runs-if schema type with proper context (runner, github, job, env, inputs, status functions)
- Validates only in strict schema used by language services
- Add format() function validation for all expressions
- Validates format string syntax in all expression contexts
- Checks argument count matches placeholders
- Fix env and matrix context providers to return complete=false
- Prevents false positive 'unknown context' errors
- Matches behavior of other dynamic contexts (secrets, vars, etc.)
- Refactor validation utilities into utils/validate-uses.ts and utils/validate-if.ts
- Shared between workflow and action validation
- Consistent error messages and codes
* Add strategy and matrix contexts to runs-if definition
Based on runner source code analysis (actions/runner):
- ExecutionContext.InitializeJob() populates ExpressionValues from message.ContextData
- strategy and matrix are part of message.ContextData, available before any steps run
- StepsRunner evaluates all steps (pre, main, post) using the same code path
Did NOT add:
- steps: empty at pre-if time (no steps completed yet)
- hashFiles: workspace files don't exist at pre-step time
119 lines
3.6 KiB
TypeScript
119 lines
3.6 KiB
TypeScript
/**
|
|
* Shared validation utilities for step `uses` field format.
|
|
* Used by both workflow and action validation.
|
|
*/
|
|
|
|
import {Diagnostic, DiagnosticSeverity} from "vscode-languageserver-types";
|
|
import {StringToken} from "@actions/workflow-parser/templates/tokens/string-token";
|
|
import {mapRange} from "./range.js";
|
|
|
|
// Matches a short SHA (7-8 hex characters) that looks like it should be a full SHA
|
|
const SHORT_SHA_PATTERN = /^[0-9a-f]{7,8}$/i;
|
|
const SHORT_SHA_DOCS_URL =
|
|
"https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions";
|
|
|
|
/**
|
|
* Checks if a ref looks like a short SHA and adds a warning if so.
|
|
* Returns true if a warning was added.
|
|
*/
|
|
export function warnIfShortSha(diagnostics: Diagnostic[], token: StringToken, ref: string): boolean {
|
|
if (SHORT_SHA_PATTERN.test(ref)) {
|
|
diagnostics.push({
|
|
message: `The provided ref '${ref}' may be a shortened commit SHA. If so, please use the full 40-character commit SHA instead, as short SHAs are not supported.`,
|
|
severity: DiagnosticSeverity.Warning,
|
|
range: mapRange(token.range),
|
|
code: "short-sha-ref",
|
|
codeDescription: {
|
|
href: SHORT_SHA_DOCS_URL
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Validates the format of a step's `uses` field.
|
|
*
|
|
* Valid formats:
|
|
* - docker://image:tag
|
|
* - ./local/path
|
|
* - .\local\path (Windows)
|
|
* - {owner}/{repo}@{ref}
|
|
* - {owner}/{repo}/{path}@{ref}
|
|
*/
|
|
export function validateStepUsesFormat(diagnostics: Diagnostic[], token: StringToken): void {
|
|
const uses = token.value;
|
|
|
|
// Empty uses value
|
|
if (!uses) {
|
|
diagnostics.push({
|
|
message: "'uses' value in action cannot be blank",
|
|
severity: DiagnosticSeverity.Error,
|
|
range: mapRange(token.range),
|
|
code: "invalid-uses-format"
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Docker image reference - always valid format
|
|
if (uses.startsWith("docker://")) {
|
|
return;
|
|
}
|
|
|
|
// Local action path - always valid format
|
|
if (uses.startsWith("./") || uses.startsWith(".\\")) {
|
|
return;
|
|
}
|
|
|
|
// Remote action: must be {owner}/{repo}[/path]@{ref}
|
|
const atSegments = uses.split("@");
|
|
|
|
// Must have exactly one @
|
|
if (atSegments.length !== 2) {
|
|
addStepUsesFormatError(diagnostics, token);
|
|
return;
|
|
}
|
|
|
|
const [repoPath, gitRef] = atSegments;
|
|
|
|
// Ref cannot be empty
|
|
if (!gitRef) {
|
|
addStepUsesFormatError(diagnostics, token);
|
|
return;
|
|
}
|
|
|
|
// Split by / or \ to get path segments
|
|
const pathSegments = repoPath.split(/[\\/]/);
|
|
|
|
// Must have at least owner and repo (both non-empty)
|
|
if (pathSegments.length < 2 || !pathSegments[0] || !pathSegments[1]) {
|
|
addStepUsesFormatError(diagnostics, token);
|
|
return;
|
|
}
|
|
|
|
// Check if this is a reusable workflow reference (should be at job level, not step)
|
|
// Path would be like: owner/repo/.github/workflows/file.yml
|
|
if (pathSegments.length >= 4 && pathSegments[2] === ".github" && pathSegments[3] === "workflows") {
|
|
diagnostics.push({
|
|
message: "Reusable workflows should be referenced at the top-level `jobs.<job_id>.uses` key, not within steps",
|
|
severity: DiagnosticSeverity.Error,
|
|
range: mapRange(token.range),
|
|
code: "invalid-uses-format"
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Warn if ref looks like a short SHA
|
|
warnIfShortSha(diagnostics, token, gitRef);
|
|
}
|
|
|
|
function addStepUsesFormatError(diagnostics: Diagnostic[], token: StringToken): void {
|
|
diagnostics.push({
|
|
message: `Expected format {owner}/{repo}[/path]@{ref}. Actual '${token.value}'`,
|
|
severity: DiagnosticSeverity.Error,
|
|
range: mapRange(token.range),
|
|
code: "invalid-uses-format"
|
|
});
|
|
}
|