diff --git a/actions/setup/js/render_template.cjs b/actions/setup/js/render_template.cjs index ae3630e9cc8..d9f65a06fc2 100644 --- a/actions/setup/js/render_template.cjs +++ b/actions/setup/js/render_template.cjs @@ -5,24 +5,12 @@ // Single-function Markdown → Markdown postprocessor for GitHub Actions. // Processes only {{#if }} ... {{/if}} blocks after ${{ }} evaluation. -const { getErrorMessage } = require("./error_helpers.cjs"); +require("./shim.cjs"); +const { getErrorMessage } = require("./error_helpers.cjs"); const fs = require("fs"); -const { ERR_API, ERR_CONFIG, ERR_VALIDATION } = require("./error_codes.cjs"); - -/** - * Determines if a value is truthy according to template logic - * @param {string} expr - The expression to evaluate - * @returns {boolean} - Whether the expression is truthy - */ -function isTruthy(expr) { - const v = expr.trim().toLowerCase(); - const result = !(v === "" || v === "false" || v === "0" || v === "null" || v === "undefined"); - if (typeof core !== "undefined") { - core.info(`[isTruthy] Evaluating "${expr}" (trimmed: "${v}") -> ${result}`); - } - return result; -} +const { ERR_API, ERR_CONFIG } = require("./error_codes.cjs"); +const { isTruthy } = require("./is_truthy.cjs"); /** * Renders a Markdown template by processing {{#if}} conditional blocks. @@ -33,18 +21,14 @@ function isTruthy(expr) { * @returns {string} - The processed markdown content */ function renderMarkdownTemplate(markdown) { - if (typeof core !== "undefined") { - core.info(`[renderMarkdownTemplate] Starting template rendering`); - core.info(`[renderMarkdownTemplate] Input length: ${markdown.length} characters`); - } + core.info(`[renderMarkdownTemplate] Starting template rendering`); + core.info(`[renderMarkdownTemplate] Input length: ${markdown.length} characters`); // Count conditionals before processing const blockConditionals = (markdown.match(/(\n?)([ \t]*{{#if\s+(.*?)\s*}}[ \t]*\n)([\s\S]*?)([ \t]*{{\/if}}[ \t]*)(\n?)/g) || []).length; const inlineConditionals = (markdown.match(/{{#if\s+(.*?)\s*}}([\s\S]*?){{\/if}}/g) || []).length - blockConditionals; - if (typeof core !== "undefined") { - core.info(`[renderMarkdownTemplate] Found ${blockConditionals} block conditional(s) and ${inlineConditionals} inline conditional(s)`); - } + core.info(`[renderMarkdownTemplate] Found ${blockConditionals} block conditional(s) and ${inlineConditionals} inline conditional(s)`); let blockCount = 0; let keptBlocks = 0; @@ -53,37 +37,24 @@ function renderMarkdownTemplate(markdown) { // First pass: Handle blocks where tags are on their own lines // Captures: (leading newline)(opening tag line)(condition)(body)(closing tag line)(trailing newline) // Uses .*? (non-greedy) with \s* to handle expressions with or without trailing spaces - let result = markdown.replace(/(\n?)([ \t]*{{#if\s+(.*?)\s*}}[ \t]*\n)([\s\S]*?)([ \t]*{{\/if}}[ \t]*)(\n?)/g, (match, leadNL, openLine, cond, body, closeLine, trailNL) => { + let result = markdown.replace(/(\n?)([ \t]*{{#if\s+(.*?)\s*}}[ \t]*\n)([\s\S]*?)([ \t]*{{\/if}}[ \t]*)(\n?)/g, (match, leadNL, openLine, cond, body) => { blockCount++; - const condTrimmed = cond.trim(); const truthyResult = isTruthy(cond); - const bodyPreview = body.substring(0, 60).replace(/\n/g, "\\n"); - if (typeof core !== "undefined") { - core.info(`[renderMarkdownTemplate] Block ${blockCount}: condition="${condTrimmed}" -> ${truthyResult ? "KEEP" : "REMOVE"}`); - core.info(`[renderMarkdownTemplate] Body preview: "${bodyPreview}${body.length > 60 ? "..." : ""}"`); - } + core.info(`[renderMarkdownTemplate] Block ${blockCount}: condition="${cond.trim()}" -> ${truthyResult ? "KEEP" : "REMOVE"}`); if (truthyResult) { // Keep body with leading newline if there was one before the opening tag keptBlocks++; - if (typeof core !== "undefined") { - core.info(`[renderMarkdownTemplate] Action: Keeping body with leading newline=${!!leadNL}`); - } return leadNL + body; } else { // Remove entire block completely - the line containing the template is removed removedBlocks++; - if (typeof core !== "undefined") { - core.info(`[renderMarkdownTemplate] Action: Removing entire block`); - } return ""; } }); - if (typeof core !== "undefined") { - core.info(`[renderMarkdownTemplate] First pass complete: ${keptBlocks} kept, ${removedBlocks} removed`); - } + core.info(`[renderMarkdownTemplate] First pass complete: ${keptBlocks} kept, ${removedBlocks} removed`); let inlineCount = 0; let keptInline = 0; @@ -93,14 +64,9 @@ function renderMarkdownTemplate(markdown) { // Uses .*? (non-greedy) with \s* to handle expressions with or without trailing spaces result = result.replace(/{{#if\s+(.*?)\s*}}([\s\S]*?){{\/if}}/g, (_, cond, body) => { inlineCount++; - const condTrimmed = cond.trim(); const truthyResult = isTruthy(cond); - const bodyPreview = body.substring(0, 40).replace(/\n/g, "\\n"); - if (typeof core !== "undefined") { - core.info(`[renderMarkdownTemplate] Inline ${inlineCount}: condition="${condTrimmed}" -> ${truthyResult ? "KEEP" : "REMOVE"}`); - core.info(`[renderMarkdownTemplate] Body preview: "${bodyPreview}${body.length > 40 ? "..." : ""}"`); - } + core.info(`[renderMarkdownTemplate] Inline ${inlineCount}: condition="${cond.trim()}" -> ${truthyResult ? "KEEP" : "REMOVE"}`); if (truthyResult) { keptInline++; @@ -111,22 +77,12 @@ function renderMarkdownTemplate(markdown) { } }); - if (typeof core !== "undefined") { - core.info(`[renderMarkdownTemplate] Second pass complete: ${keptInline} kept, ${removedInline} removed`); - } + core.info(`[renderMarkdownTemplate] Second pass complete: ${keptInline} kept, ${removedInline} removed`); // Clean up excessive blank lines (more than one blank line = 2 newlines) - const beforeCleanup = result.length; - const excessiveLines = (result.match(/\n{3,}/g) || []).length; result = result.replace(/\n{3,}/g, "\n\n"); - if (typeof core !== "undefined") { - if (excessiveLines > 0) { - core.info(`[renderMarkdownTemplate] Cleaned up ${excessiveLines} excessive blank line sequence(s)`); - core.info(`[renderMarkdownTemplate] Length change from cleanup: ${beforeCleanup} -> ${result.length} characters`); - } - core.info(`[renderMarkdownTemplate] Final output length: ${result.length} characters`); - } + core.info(`[renderMarkdownTemplate] Final output length: ${result.length} characters`); return result; } @@ -136,91 +92,40 @@ function renderMarkdownTemplate(markdown) { */ function main() { try { - if (typeof core !== "undefined") { - core.info("========================================"); - core.info("[main] Starting render_template processing"); - core.info("========================================"); - } + core.info("[render_template] Starting template rendering"); const promptPath = process.env.GH_AW_PROMPT; if (!promptPath) { - if (typeof core !== "undefined") { - core.setFailed(`${ERR_CONFIG}: GH_AW_PROMPT environment variable is not set`); - } + core.setFailed(`${ERR_CONFIG}: GH_AW_PROMPT environment variable is not set`); process.exit(1); } - if (typeof core !== "undefined") { - core.info(`[main] Prompt path: ${promptPath}`); - } + core.info(`[render_template] Prompt path: ${promptPath}`); - // Read the prompt file - if (typeof core !== "undefined") { - core.info(`[main] Reading prompt file...`); - } const markdown = fs.readFileSync(promptPath, "utf8"); - const originalLength = markdown.length; + core.info(`[render_template] Read ${markdown.length} characters`); - if (typeof core !== "undefined") { - core.info(`[main] Original content length: ${originalLength} characters`); - core.info(`[main] First 200 characters: ${markdown.substring(0, 200).replace(/\n/g, "\\n")}`); - } - - // Check if there are any conditional blocks const hasConditionals = /{{#if\s+[^}]+}}/.test(markdown); if (!hasConditionals) { - if (typeof core !== "undefined") { - core.info("No conditional blocks found in prompt, skipping template rendering"); - core.info("========================================"); - core.info("[main] Processing complete - SKIPPED"); - core.info("========================================"); - } + core.info("No conditional blocks found in prompt, skipping template rendering"); process.exit(0); } const conditionalMatches = markdown.match(/{{#if\s+[^}]+}}/g) || []; - if (typeof core !== "undefined") { - core.info(`[main] Processing ${conditionalMatches.length} conditional template block(s)`); - } + core.info(`[render_template] Processing ${conditionalMatches.length} conditional template block(s)`); - // Render the template - const beforeRendering = markdown.length; const rendered = renderMarkdownTemplate(markdown); - const afterRendering = rendered.length; - - // Write back to the same file - if (typeof core !== "undefined") { - core.info("\n========================================"); - core.info("[main] Writing Output"); - core.info("========================================"); - core.info(`[main] Writing processed content back to: ${promptPath}`); - core.info(`[main] Final content length: ${afterRendering} characters`); - core.info(`[main] Total length change: ${beforeRendering} -> ${afterRendering} (${afterRendering > beforeRendering ? "+" : ""}${afterRendering - beforeRendering})`); - } + core.info(`[render_template] Writing back to ${promptPath} (${rendered.length} characters)`); fs.writeFileSync(promptPath, rendered, "utf8"); - if (typeof core !== "undefined") { - core.info(`[main] Last 200 characters: ${rendered.substring(Math.max(0, rendered.length - 200)).replace(/\n/g, "\\n")}`); - core.info("========================================"); - core.info("[main] Processing complete - SUCCESS"); - core.info("========================================"); - } + core.info("[render_template] Processing complete"); } catch (error) { - if (typeof core !== "undefined") { - core.info("========================================"); - core.info("[main] Processing failed - ERROR"); - core.info("========================================"); - const err = error instanceof Error ? error : new Error(String(error)); - core.info(`[main] Error type: ${err.constructor.name}`); - core.info(`[main] Error message: ${err.message}`); - if (err.stack) { - core.info(`[main] Stack trace:\n${err.stack}`); - } - core.setFailed(`${ERR_API}: ${getErrorMessage(error)}`); - } else { - throw error; + const err = error instanceof Error ? error : new Error(String(error)); + if (err.stack) { + core.info(`[render_template] Stack trace:\n${err.stack}`); } + core.setFailed(`${ERR_API}: ${getErrorMessage(error)}`); } } diff --git a/actions/setup/js/render_template.test.cjs b/actions/setup/js/render_template.test.cjs index c7f0f01e19e..08140c40386 100644 --- a/actions/setup/js/render_template.test.cjs +++ b/actions/setup/js/render_template.test.cjs @@ -6,111 +6,84 @@ const __filename = fileURLToPath(import.meta.url), __dirname = path.dirname(__filename), core = { info: vi.fn(), warning: vi.fn(), setFailed: vi.fn(), summary: { addHeading: vi.fn().mockReturnThis(), addRaw: vi.fn().mockReturnThis(), write: vi.fn() } }; global.core = core; -const renderTemplateScript = fs.readFileSync(path.join(__dirname, "render_template.cjs"), "utf8"), - isTruthyMatch = renderTemplateScript.match(/function isTruthy\(expr\)\s*{[\s\S]*?return[\s\S]*?;[\s\S]*?}/), +const { isTruthy } = require("./is_truthy.cjs"), + renderTemplateScript = fs.readFileSync(path.join(__dirname, "render_template.cjs"), "utf8"), renderMarkdownTemplateMatch = renderTemplateScript.match(/function renderMarkdownTemplate\(markdown\)\s*{[\s\S]*?return result;[\s\S]*?}/); -if (!isTruthyMatch || !renderMarkdownTemplateMatch) throw new Error("Could not extract functions from render_template.cjs"); -const isTruthy = eval(`(${isTruthyMatch[0]})`), - renderMarkdownTemplate = eval(`(${renderMarkdownTemplateMatch[0]})`); -(describe("isTruthy", () => { - (it("should return false for empty string", () => { - expect(isTruthy("")).toBe(!1); +if (!renderMarkdownTemplateMatch) throw new Error("Could not extract renderMarkdownTemplate function from render_template.cjs"); +const renderMarkdownTemplate = eval(`(${renderMarkdownTemplateMatch[0]})`); +describe("renderMarkdownTemplate", () => { + (it("should keep content in truthy blocks", () => { + const output = renderMarkdownTemplate("{{#if true}}\nHello\n{{/if}}"); + expect(output).toBe("Hello\n"); }), - it('should return false for "false"', () => { - (expect(isTruthy("false")).toBe(!1), expect(isTruthy("FALSE")).toBe(!1), expect(isTruthy("False")).toBe(!1)); + it("should remove content in falsy blocks", () => { + const output = renderMarkdownTemplate("{{#if false}}\nHello\n{{/if}}"); + expect(output).toBe(""); }), - it('should return false for "0"', () => { - expect(isTruthy("0")).toBe(!1); + it("should process multiple blocks", () => { + const output = renderMarkdownTemplate("{{#if true}}\nKeep this\n{{/if}}\n{{#if false}}\nRemove this\n{{/if}}"); + expect(output).toBe("Keep this\n"); }), - it('should return false for "null"', () => { - (expect(isTruthy("null")).toBe(!1), expect(isTruthy("NULL")).toBe(!1)); + it("should handle nested content", () => { + const output = renderMarkdownTemplate("# Title\n\n{{#if true}}\n## Section 1\nThis should be kept.\n{{/if}}\n\n{{#if false}}\n## Section 2\nThis should be removed.\n{{/if}}\n\n## Section 3\nThis is always visible."); + expect(output).toBe("# Title\n\n## Section 1\nThis should be kept.\n\n## Section 3\nThis is always visible."); }), - it('should return false for "undefined"', () => { - (expect(isTruthy("undefined")).toBe(!1), expect(isTruthy("UNDEFINED")).toBe(!1)); + it("should leave content without conditionals unchanged", () => { + const input = "# Normal Markdown\n\nNo conditionals here.", + output = renderMarkdownTemplate(input); + expect(output).toBe(input); }), - it('should return true for "true"', () => { - (expect(isTruthy("true")).toBe(!0), expect(isTruthy("TRUE")).toBe(!0)); + it("should handle conditionals with various expressions", () => { + (expect(renderMarkdownTemplate("{{#if 1}}\nKeep\n{{/if}}")).toBe("Keep\n"), + expect(renderMarkdownTemplate("{{#if 0}}\nRemove\n{{/if}}")).toBe(""), + expect(renderMarkdownTemplate("{{#if null}}\nRemove\n{{/if}}")).toBe(""), + expect(renderMarkdownTemplate("{{#if undefined}}\nRemove\n{{/if}}")).toBe("")); }), - it("should return true for any non-falsy string", () => { - (expect(isTruthy("yes")).toBe(!0), expect(isTruthy("1")).toBe(!0), expect(isTruthy("hello")).toBe(!0)); + it("should preserve markdown formatting inside blocks", () => { + const output = renderMarkdownTemplate("{{#if true}}\n## Header\n- List item 1\n- List item 2\n\n```javascript\nconst x = 1;\n```\n{{/if}}"); + expect(output).toBe("## Header\n- List item 1\n- List item 2\n\n```javascript\nconst x = 1;\n```\n"); }), - it("should trim whitespace", () => { - (expect(isTruthy(" false ")).toBe(!1), expect(isTruthy(" true ")).toBe(!0)); - })); -}), - describe("renderMarkdownTemplate", () => { - (it("should keep content in truthy blocks", () => { - const output = renderMarkdownTemplate("{{#if true}}\nHello\n{{/if}}"); - expect(output).toBe("Hello\n"); + it("should handle whitespace in conditionals", () => { + (expect(renderMarkdownTemplate("{{#if true }}\nKeep\n{{/if}}")).toBe("Keep\n"), expect(renderMarkdownTemplate("{{#if\ttrue\t}}\nKeep\n{{/if}}")).toBe("Keep\n")); + }), + it("should clean up multiple consecutive empty lines", () => { + const output = renderMarkdownTemplate("# Title\n\n{{#if false}}\n## Hidden Section\nThis should be removed.\n{{/if}}\n\n## Visible Section\nThis is always visible."); + expect(output).toBe("# Title\n\n## Visible Section\nThis is always visible."); + }), + it("should collapse multiple false blocks without excessive empty lines", () => { + const output = renderMarkdownTemplate("Start\n\n{{#if false}}\nBlock 1\n{{/if}}\n\n{{#if false}}\nBlock 2\n{{/if}}\n\n{{#if false}}\nBlock 3\n{{/if}}\n\nEnd"); + (expect(output).not.toMatch(/\n{3,}/), expect(output).toContain("Start"), expect(output).toContain("End")); + }), + it("should preserve leading spaces with truthy block", () => { + const output = renderMarkdownTemplate(" {{#if true}}\n Content with leading spaces\n {{/if}}"); + expect(output).toBe(" Content with leading spaces\n"); + }), + it("should remove leading spaces when block is falsy", () => { + const output = renderMarkdownTemplate(" {{#if false}}\n Content that should be removed\n {{/if}}"); + expect(output).toBe(""); }), - it("should remove content in falsy blocks", () => { - const output = renderMarkdownTemplate("{{#if false}}\nHello\n{{/if}}"); - expect(output).toBe(""); - }), - it("should process multiple blocks", () => { - const output = renderMarkdownTemplate("{{#if true}}\nKeep this\n{{/if}}\n{{#if false}}\nRemove this\n{{/if}}"); - expect(output).toBe("Keep this\n"); - }), - it("should handle nested content", () => { - const output = renderMarkdownTemplate("# Title\n\n{{#if true}}\n## Section 1\nThis should be kept.\n{{/if}}\n\n{{#if false}}\n## Section 2\nThis should be removed.\n{{/if}}\n\n## Section 3\nThis is always visible."); - expect(output).toBe("# Title\n\n## Section 1\nThis should be kept.\n\n## Section 3\nThis is always visible."); - }), - it("should leave content without conditionals unchanged", () => { - const input = "# Normal Markdown\n\nNo conditionals here.", - output = renderMarkdownTemplate(input); - expect(output).toBe(input); - }), - it("should handle conditionals with various expressions", () => { - (expect(renderMarkdownTemplate("{{#if 1}}\nKeep\n{{/if}}")).toBe("Keep\n"), - expect(renderMarkdownTemplate("{{#if 0}}\nRemove\n{{/if}}")).toBe(""), - expect(renderMarkdownTemplate("{{#if null}}\nRemove\n{{/if}}")).toBe(""), - expect(renderMarkdownTemplate("{{#if undefined}}\nRemove\n{{/if}}")).toBe("")); - }), - it("should preserve markdown formatting inside blocks", () => { - const output = renderMarkdownTemplate("{{#if true}}\n## Header\n- List item 1\n- List item 2\n\n```javascript\nconst x = 1;\n```\n{{/if}}"); - expect(output).toBe("## Header\n- List item 1\n- List item 2\n\n```javascript\nconst x = 1;\n```\n"); - }), - it("should handle whitespace in conditionals", () => { - (expect(renderMarkdownTemplate("{{#if true }}\nKeep\n{{/if}}")).toBe("Keep\n"), expect(renderMarkdownTemplate("{{#if\ttrue\t}}\nKeep\n{{/if}}")).toBe("Keep\n")); - }), - it("should clean up multiple consecutive empty lines", () => { - const output = renderMarkdownTemplate("# Title\n\n{{#if false}}\n## Hidden Section\nThis should be removed.\n{{/if}}\n\n## Visible Section\nThis is always visible."); - expect(output).toBe("# Title\n\n## Visible Section\nThis is always visible."); - }), - it("should collapse multiple false blocks without excessive empty lines", () => { - const output = renderMarkdownTemplate("Start\n\n{{#if false}}\nBlock 1\n{{/if}}\n\n{{#if false}}\nBlock 2\n{{/if}}\n\n{{#if false}}\nBlock 3\n{{/if}}\n\nEnd"); - (expect(output).not.toMatch(/\n{3,}/), expect(output).toContain("Start"), expect(output).toContain("End")); - }), - it("should preserve leading spaces with truthy block", () => { - const output = renderMarkdownTemplate(" {{#if true}}\n Content with leading spaces\n {{/if}}"); - expect(output).toBe(" Content with leading spaces\n"); - }), - it("should remove leading spaces when block is falsy", () => { - const output = renderMarkdownTemplate(" {{#if false}}\n Content that should be removed\n {{/if}}"); - expect(output).toBe(""); - }), - it("should handle mixed indentation levels", () => { - const output = renderMarkdownTemplate("{{#if true}}\nNo indent\n{{/if}}\n {{#if true}}\n Two space indent\n {{/if}}\n {{#if true}}\n Four space indent\n {{/if}}"); - expect(output).toBe("No indent\n Two space indent\n Four space indent\n"); - }), - it("should preserve indentation in content when using leading spaces", () => { - const output = renderMarkdownTemplate("# Header\n\n {{#if true}}\n ## Indented subsection\n This content has two leading spaces\n {{/if}}\n\nNormal content"); - expect(output).toBe("# Header\n\n ## Indented subsection\n This content has two leading spaces\n\nNormal content"); - }), - it("should handle tabs as leading characters", () => { - const output = renderMarkdownTemplate("\t{{#if true}}\n\tContent with tab\n\t{{/if}}"); - expect(output).toBe("\tContent with tab\n"); - }), - it("should handle realistic linter-formatted markdown", () => { - const inputWithValue = "# Analysis\n\n {{#if github.event.issue.number}}\n ## Issue Analysis\n \n Analyzing issue #123\n \n - Check description\n - Review labels\n {{/if}}\n\nContinue with other tasks".replace( - "github.event.issue.number", - "123" - ), - output = renderMarkdownTemplate(inputWithValue); - expect(output).toBe("# Analysis\n\n ## Issue Analysis\n \n Analyzing issue #123\n \n - Check description\n - Review labels\n\nContinue with other tasks"); - }), - it("should preserve closing tag indentation", () => { - const output = renderMarkdownTemplate(" {{#if true}}\n Content\n {{/if}}\nNext line"); - expect(output).toBe(" Content\nNext line"); - })); - })); + it("should handle mixed indentation levels", () => { + const output = renderMarkdownTemplate("{{#if true}}\nNo indent\n{{/if}}\n {{#if true}}\n Two space indent\n {{/if}}\n {{#if true}}\n Four space indent\n {{/if}}"); + expect(output).toBe("No indent\n Two space indent\n Four space indent\n"); + }), + it("should preserve indentation in content when using leading spaces", () => { + const output = renderMarkdownTemplate("# Header\n\n {{#if true}}\n ## Indented subsection\n This content has two leading spaces\n {{/if}}\n\nNormal content"); + expect(output).toBe("# Header\n\n ## Indented subsection\n This content has two leading spaces\n\nNormal content"); + }), + it("should handle tabs as leading characters", () => { + const output = renderMarkdownTemplate("\t{{#if true}}\n\tContent with tab\n\t{{/if}}"); + expect(output).toBe("\tContent with tab\n"); + }), + it("should handle realistic linter-formatted markdown", () => { + const inputWithValue = "# Analysis\n\n {{#if github.event.issue.number}}\n ## Issue Analysis\n \n Analyzing issue #123\n \n - Check description\n - Review labels\n {{/if}}\n\nContinue with other tasks".replace( + "github.event.issue.number", + "123" + ), + output = renderMarkdownTemplate(inputWithValue); + expect(output).toBe("# Analysis\n\n ## Issue Analysis\n \n Analyzing issue #123\n \n - Check description\n - Review labels\n\nContinue with other tasks"); + }), + it("should preserve closing tag indentation", () => { + const output = renderMarkdownTemplate(" {{#if true}}\n Content\n {{/if}}\nNext line"); + expect(output).toBe(" Content\nNext line"); + })); +});