Add missing validation for action.yml (parity with workflow files) (#311)
* 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
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Shared validation utilities for `if` condition literal text detection.
|
||||
* Used by both workflow and action validation.
|
||||
*/
|
||||
|
||||
import {data} from "@actions/expressions";
|
||||
import {Expr, FunctionCall, Literal, Logical} from "@actions/expressions/ast";
|
||||
|
||||
/**
|
||||
* Checks if a format function contains literal text in its format string.
|
||||
* This indicates user confusion about how expressions work.
|
||||
*
|
||||
* Example: format('push == {0}', github.event_name)
|
||||
* The literal text "push == " will always evaluate to truthy.
|
||||
*
|
||||
* @param expr The expression to check
|
||||
* @returns true if the expression is a format() call with literal text
|
||||
*/
|
||||
export function hasFormatWithLiteralText(expr: Expr): boolean {
|
||||
// If this is a logical AND expression (from ensureStatusFunction wrapping)
|
||||
// check the right side for the format call
|
||||
if (expr instanceof Logical && expr.operator.lexeme === "&&" && expr.args.length === 2) {
|
||||
return hasFormatWithLiteralText(expr.args[1]);
|
||||
}
|
||||
|
||||
if (!(expr instanceof FunctionCall)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if this is a format function
|
||||
if (expr.functionName.lexeme.toLowerCase() !== "format") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the first argument is a string literal
|
||||
if (expr.args.length < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstArg = expr.args[0];
|
||||
if (!(firstArg instanceof Literal) || firstArg.literal.kind !== data.Kind.String) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the format string and trim whitespace
|
||||
const formatString = firstArg.literal.coerceString();
|
||||
const trimmed = formatString.trim();
|
||||
|
||||
// Check if there's literal text (non-replacement tokens) after trimming
|
||||
let inToken = false;
|
||||
for (let i = 0; i < trimmed.length; i++) {
|
||||
if (!inToken && trimmed[i] === "{") {
|
||||
inToken = true;
|
||||
} else if (inToken && trimmed[i] === "}") {
|
||||
inToken = false;
|
||||
} else if (inToken && trimmed[i] >= "0" && trimmed[i] <= "9") {
|
||||
// OK - this is a replacement token like {0}, {1}, etc.
|
||||
} else {
|
||||
// Found literal text
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user