diff --git a/actions/setup/js/runtime_import.cjs b/actions/setup/js/runtime_import.cjs index 8f3eb50f32e..5870887b5dc 100644 --- a/actions/setup/js/runtime_import.cjs +++ b/actions/setup/js/runtime_import.cjs @@ -650,20 +650,20 @@ function wrapExpressionsInTemplateConditionals(content) { return match; } + // Boolean/null literals are self-evaluating — the template renderer's isTruthy() + // handles them directly. Wrapping them would create __GH_AW_TRUE__/__GH_AW_FALSE__/__GH_AW_NULL__ + // placeholders that cannot be resolved at runtime (no corresponding env var is set), + // causing the placeholder validator to flag them as unsubstituted. + if (trimmed === "true" || trimmed === "false" || trimmed === "null") { + return match; + } + // Only wrap expressions that look like GitHub Actions expressions // GitHub Actions expressions typically start with a letter and contain dots - // (e.g., github.actor, github.event.issue.number) or specific keywords (true, false, null). + // (e.g., github.actor, github.event.issue.number). // Expressions starting with non-alphabetic characters (e.g., "...") are NOT GitHub expressions. const looksLikeGitHubExpr = - (/^[a-zA-Z]/.test(trimmed) && trimmed.includes(".")) || - trimmed === "true" || - trimmed === "false" || - trimmed === "null" || - trimmed.startsWith("github.") || - trimmed.startsWith("needs.") || - trimmed.startsWith("steps.") || - trimmed.startsWith("env.") || - trimmed.startsWith("inputs."); + (/^[a-zA-Z]/.test(trimmed) && trimmed.includes(".")) || trimmed.startsWith("github.") || trimmed.startsWith("needs.") || trimmed.startsWith("steps.") || trimmed.startsWith("env.") || trimmed.startsWith("inputs."); if (!looksLikeGitHubExpr) { // Not a GitHub Actions expression, leave as-is diff --git a/actions/setup/js/runtime_import.test.cjs b/actions/setup/js/runtime_import.test.cjs index 53c35c0b8f7..5ecf330eba5 100644 --- a/actions/setup/js/runtime_import.test.cjs +++ b/actions/setup/js/runtime_import.test.cjs @@ -1498,14 +1498,17 @@ describe("runtime_import", () => { it("should wrap {{#if steps.foo.outputs.bar}}", () => { expect(wrapExpressionsInTemplateConditionals("{{#if steps.foo.outputs.bar}}body{{/if}}")).toBe("{{#if ${{ steps.foo.outputs.bar }} }}body{{/if}}"); }); - it("should wrap {{#if true}}", () => { - expect(wrapExpressionsInTemplateConditionals("{{#if true}}body{{/if}}")).toBe("{{#if ${{ true }} }}body{{/if}}"); + }); + + describe("boolean/null literals — must be left unchanged for direct isTruthy() evaluation", () => { + it("should leave {{#if true}} unchanged", () => { + expect(wrapExpressionsInTemplateConditionals("{{#if true}}body{{/if}}")).toBe("{{#if true}}body{{/if}}"); }); - it("should wrap {{#if false}}", () => { - expect(wrapExpressionsInTemplateConditionals("{{#if false}}body{{/if}}")).toBe("{{#if ${{ false }} }}body{{/if}}"); + it("should leave {{#if false}} unchanged", () => { + expect(wrapExpressionsInTemplateConditionals("{{#if false}}body{{/if}}")).toBe("{{#if false}}body{{/if}}"); }); - it("should wrap {{#if null}}", () => { - expect(wrapExpressionsInTemplateConditionals("{{#if null}}body{{/if}}")).toBe("{{#if ${{ null }} }}body{{/if}}"); + it("should leave {{#if null}} unchanged", () => { + expect(wrapExpressionsInTemplateConditionals("{{#if null}}body{{/if}}")).toBe("{{#if null}}body{{/if}}"); }); });