Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions actions/setup/js/interpolate_prompt.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,20 @@ function renderMarkdownTemplate(markdown) {
core.info(`[renderMarkdownTemplate] Starting template rendering`);
core.info(`[renderMarkdownTemplate] Input length: ${markdown.length} characters`);

// Preserve fenced code blocks to avoid processing {{#if}} markers inside them
const _codeBlocks = [];
const _FENCE_PH = "\x00FENCE\x00";
const _stripped = markdown.replace(/`{3,}[^\n]*\n[\s\S]*?\n`{3,}[ \t]*/g, m => {
Comment on lines +59 to +62
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as render_template.cjs: the fenced-code-block extraction regex can terminate an outer 4–6 backtick fence at an inner closing ``` line, which is common when embedding Markdown that itself contains triple-backtick code blocks. Use an opening-fence capture + backreference (and multiline line-start anchoring) so only a matching-length closing fence ends the preserved block.

Suggested change
// Preserve fenced code blocks to avoid processing {{#if}} markers inside them
const _codeBlocks = [];
const _FENCE_PH = "\x00FENCE\x00";
const _stripped = markdown.replace(/`{3,}[^\n]*\n[\s\S]*?\n`{3,}[ \t]*/g, m => {
// Preserve fenced code blocks to avoid processing {{#if}} markers inside them.
// Capture the opening fence and require the closing fence to use the same
// number of backticks, anchored at the start of a line, so outer 4+ backtick
// fences are not terminated by inner triple-backtick lines.
const _codeBlocks = [];
const _FENCE_PH = "\x00FENCE\x00";
const _stripped = markdown.replace(/^(`{3,})[^\n]*\n[\s\S]*?^\1[ \t]*$/gm, m => {

Copilot uses AI. Check for mistakes.
_codeBlocks.push(m);
return `${_FENCE_PH}${_codeBlocks.length - 1}${_FENCE_PH}`;
});
if (_codeBlocks.length > 0) {
core.info(`[renderMarkdownTemplate] Preserved ${_codeBlocks.length} fenced code block(s) from template processing`);
}

// Count conditionals before processing
const blockConditionals = (markdown.match(/(\n?)([ \t]*{{#if\s+([^}]*)}}[ \t]*\n)([\s\S]*?)([ \t]*{{\/if}}[ \t]*)(\n?)/g) || []).length;
const inlineConditionals = (markdown.match(/{{#if\s+([^}]*)}}([\s\S]*?){{\/if}}/g) || []).length - blockConditionals;
const blockConditionals = (_stripped.match(/(\n?)([ \t]*{{#if\s+([^}]*)}}[ \t]*\n)([\s\S]*?)([ \t]*{{\/if}}[ \t]*)(\n?)/g) || []).length;
const inlineConditionals = (_stripped.match(/{{#if\s+([^}]*)}}([\s\S]*?){{\/if}}/g) || []).length - blockConditionals;

core.info(`[renderMarkdownTemplate] Found ${blockConditionals} block conditional(s) and ${inlineConditionals} inline conditional(s)`);

Expand All @@ -68,7 +79,7 @@ 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)
let result = markdown.replace(/(\n?)([ \t]*{{#if\s+([^}]*)}}[ \t]*\n)([\s\S]*?)([ \t]*{{\/if}}[ \t]*)(\n?)/g, (match, leadNL, openLine, cond, body, closeLine, trailNL) => {
let result = _stripped.replace(/(\n?)([ \t]*{{#if\s+([^}]*)}}[ \t]*\n)([\s\S]*?)([ \t]*{{\/if}}[ \t]*)(\n?)/g, (match, leadNL, openLine, cond, body, closeLine, trailNL) => {
blockCount++;
const condTrimmed = cond.trim();
const truthyResult = isTruthy(cond);
Expand Down Expand Up @@ -126,6 +137,18 @@ function renderMarkdownTemplate(markdown) {
core.info(`[renderMarkdownTemplate] Cleaned up ${excessiveLines} excessive blank line sequence(s)`);
core.info(`[renderMarkdownTemplate] Length change from cleanup: ${beforeCleanup} -> ${result.length} characters`);
}
// Restore fenced code blocks
if (_codeBlocks.length > 0) {
result = result.replace(/\x00FENCE\x00(\d+)\x00FENCE\x00/g, (_, i) => _codeBlocks[+i]);
}

// Runtime assertion: number of fence markers must be the same before and after processing
const _inputFenceCount = (markdown.match(/`{3,}/g) || []).length;
const _outputFenceCount = (result.match(/`{3,}/g) || []).length;
if (_inputFenceCount !== _outputFenceCount) {
core.warning(`[renderMarkdownTemplate] Fence count mismatch: input had ${_inputFenceCount} fence marker(s), output has ${_outputFenceCount}`);
}

core.info(`[renderMarkdownTemplate] Final output length: ${result.length} characters`);

return result;
Expand Down
2 changes: 1 addition & 1 deletion actions/setup/js/interpolate_prompt.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { fileURLToPath } from "url";
const { ERR_CONFIG } = require("./error_codes.cjs");
const __filename = fileURLToPath(import.meta.url),
__dirname = path.dirname(__filename),
core = { info: vi.fn(), setFailed: vi.fn() };
core = { info: vi.fn(), warning: vi.fn(), setFailed: vi.fn() };
global.core = core;
const interpolatePromptScript = fs.readFileSync(path.join(__dirname, "interpolate_prompt.cjs"), "utf8"),
{ isTruthy } = require("./is_truthy.cjs"),
Expand Down
34 changes: 33 additions & 1 deletion actions/setup/js/interpolate_prompt_additional.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url),
__dirname = path.dirname(__filename),
core = { info: vi.fn(), setFailed: vi.fn() };
core = { info: vi.fn(), warning: vi.fn(), setFailed: vi.fn() };
global.core = core;
const { isTruthy } = require("./is_truthy.cjs"),
interpolatePromptScript = fs.readFileSync(path.join(__dirname, "interpolate_prompt.cjs"), "utf8"),
Expand Down Expand Up @@ -200,5 +200,37 @@ describe("renderMarkdownTemplate - Additional Edge Cases", () => {
const output = renderMarkdownTemplate("{{#if false}}\nContent\n{{/if}}Line after");
expect(output).toBe("Line after");
}));
}),
describe("fenced code blocks", () => {
(it("should preserve {{#if false}} markers inside a fenced code block (regression)", () => {
const input = "```js\n{{#if false}}\nHidden\n{{/if}}\n```";
const output = renderMarkdownTemplate(input);
expect(output).toBe(input);
}),
it("should preserve {{#if true}} markers inside a fenced code block", () => {
const input = "```js\n{{#if true}}\nVisible\n{{/if}}\n```";
const output = renderMarkdownTemplate(input);
expect(output).toBe(input);
}),
it("should process conditionals outside fenced blocks while preserving inside", () => {
const input = "{{#if false}}\nRemove this\n{{/if}}\n```js\n{{#if false}}\nKeep this\n{{/if}}\n```";
const output = renderMarkdownTemplate(input);
expect(output).toBe("```js\n{{#if false}}\nKeep this\n{{/if}}\n```");
}),
it("should preserve fence count (no fence markers lost or gained)", () => {
const input = "```js\n{{#if false}}\nHidden\n{{/if}}\n```";
const output = renderMarkdownTemplate(input);
expect((output.match(/`{3,}/g) || []).length).toBe((input.match(/`{3,}/g) || []).length);
}),
it("should preserve multiple fenced code blocks unchanged", () => {
const input = "```js\ncode 1\n```\n\n```py\ncode 2\n```";
const output = renderMarkdownTemplate(input);
expect(output).toBe(input);
}),
it("should handle fenced blocks adjacent to conditionals", () => {
const input = "{{#if true}}\nKeep\n{{/if}}\n```python\nprint('hello')\n```";
const output = renderMarkdownTemplate(input);
expect(output).toBe("Keep\n```python\nprint('hello')\n```");
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage: consider adding fenced-code-block cases for indented fences and for an outer 4–6 backtick fence containing an inner triple-backtick block. The current preservation regex can mis-handle nested fences, and a focused regression test here would catch it.

Suggested change
expect(output).toBe("Keep\n```python\nprint('hello')\n```");
expect(output).toBe("Keep\n```python\nprint('hello')\n```");
}),
it("should preserve indented fenced code blocks unchanged", () => {
const input = " ```js\n {{#if false}}\n Hidden\n {{/if}}\n ```";
const output = renderMarkdownTemplate(input);
expect(output).toBe(input);
}),
it("should preserve an outer 4-backtick fence containing an inner triple-backtick block", () => {
const input = "````md\n```js\n{{#if false}}\nHidden\n{{/if}}\n```\n````";
const output = renderMarkdownTemplate(input);
expect(output).toBe(input);

Copilot uses AI. Check for mistakes.
}));
}));
});
29 changes: 26 additions & 3 deletions actions/setup/js/render_template.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,20 @@ function renderMarkdownTemplate(markdown) {
core.info(`[renderMarkdownTemplate] Starting template rendering`);
core.info(`[renderMarkdownTemplate] Input length: ${markdown.length} characters`);

// Preserve fenced code blocks to avoid processing {{#if}} markers inside them
const _codeBlocks = [];
const _FENCE_PH = "\x00FENCE\x00";
const _stripped = markdown.replace(/`{3,}[^\n]*\n[\s\S]*?\n`{3,}[ \t]*/g, m => {
_codeBlocks.push(m);
return `${_FENCE_PH}${_codeBlocks.length - 1}${_FENCE_PH}`;
Comment on lines +27 to +32
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fenced-code-block extraction regex can break for fences longer than 3 backticks (e.g. ), because it will treat an inner closing ``` line as the end of the outer block. This can reintroduce corruption for prompts that wrap markdown in 4–6 backticks to embed nested triple-backtick blocks. Consider capturing the opening fence (and any leading indentation) and restoring only when you see a closing fence of the same length at the start of a line (e.g. via a multiline regex with a backreference).

This issue also appears on line 27 of the same file.

Suggested change
// Preserve fenced code blocks to avoid processing {{#if}} markers inside them
const _codeBlocks = [];
const _FENCE_PH = "\x00FENCE\x00";
const _stripped = markdown.replace(/`{3,}[^\n]*\n[\s\S]*?\n`{3,}[ \t]*/g, m => {
_codeBlocks.push(m);
return `${_FENCE_PH}${_codeBlocks.length - 1}${_FENCE_PH}`;
// Preserve fenced code blocks to avoid processing {{#if}} markers inside them.
// Match only a closing fence with the same indentation and backtick length as the opener,
// so outer 4+ backtick fences are not terminated by inner triple-backtick lines.
const _codeBlocks = [];
const _FENCE_PH = "\x00FENCE\x00";
const _FENCED_CODE_BLOCK_RE = /(^|\n)([ \t]*)(`{3,})[^\n]*\n[\s\S]*?\n\2\3[ \t]*(?=\n|$)/g;
const _stripped = markdown.replace(_FENCED_CODE_BLOCK_RE, (m, lineStart) => {
const block = m.slice(lineStart.length);
_codeBlocks.push(block);
return `${lineStart}${_FENCE_PH}${_codeBlocks.length - 1}${_FENCE_PH}`;

Copilot uses AI. Check for mistakes.
});
if (_codeBlocks.length > 0) {
core.info(`[renderMarkdownTemplate] Preserved ${_codeBlocks.length} fenced code block(s) from template processing`);
}

// 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;
const blockConditionals = (_stripped.match(/(\n?)([ \t]*{{#if\s+(.*?)\s*}}[ \t]*\n)([\s\S]*?)([ \t]*{{\/if}}[ \t]*)(\n?)/g) || []).length;
const inlineConditionals = (_stripped.match(/{{#if\s+(.*?)\s*}}([\s\S]*?){{\/if}}/g) || []).length - blockConditionals;

core.info(`[renderMarkdownTemplate] Found ${blockConditionals} block conditional(s) and ${inlineConditionals} inline conditional(s)`);

Expand All @@ -37,7 +48,7 @@ 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) => {
let result = _stripped.replace(/(\n?)([ \t]*{{#if\s+(.*?)\s*}}[ \t]*\n)([\s\S]*?)([ \t]*{{\/if}}[ \t]*)(\n?)/g, (match, leadNL, openLine, cond, body) => {
blockCount++;
const truthyResult = isTruthy(cond);

Expand Down Expand Up @@ -82,6 +93,18 @@ function renderMarkdownTemplate(markdown) {
// Clean up excessive blank lines (more than one blank line = 2 newlines)
result = result.replace(/\n{3,}/g, "\n\n");

// Restore fenced code blocks
if (_codeBlocks.length > 0) {
result = result.replace(/\x00FENCE\x00(\d+)\x00FENCE\x00/g, (_, i) => _codeBlocks[+i]);
}

// Runtime assertion: number of fence markers must be the same before and after processing
const _inputFenceCount = (markdown.match(/`{3,}/g) || []).length;
const _outputFenceCount = (result.match(/`{3,}/g) || []).length;
if (_inputFenceCount !== _outputFenceCount) {
core.warning(`[renderMarkdownTemplate] Fence count mismatch: input had ${_inputFenceCount} fence marker(s), output has ${_outputFenceCount}`);
}

core.info(`[renderMarkdownTemplate] Final output length: ${result.length} characters`);

return result;
Expand Down
32 changes: 32 additions & 0 deletions actions/setup/js/render_template.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,36 @@ describe("renderMarkdownTemplate", () => {
const output = renderMarkdownTemplate(" {{#if true}}\n Content\n {{/if}}\nNext line");
expect(output).toBe(" Content\nNext line");
}));
describe("fenced code blocks", () => {
it("should preserve {{#if false}} markers inside a fenced code block (regression)", () => {
const input = "```js\n{{#if false}}\nHidden\n{{/if}}\n```";
const output = renderMarkdownTemplate(input);
expect(output).toBe(input);
});
it("should preserve {{#if true}} markers inside a fenced code block", () => {
const input = "```js\n{{#if true}}\nVisible\n{{/if}}\n```";
const output = renderMarkdownTemplate(input);
expect(output).toBe(input);
});
it("should process conditionals outside fenced blocks while preserving inside", () => {
const input = "{{#if false}}\nRemove this\n{{/if}}\n```js\n{{#if false}}\nKeep this\n{{/if}}\n```";
const output = renderMarkdownTemplate(input);
expect(output).toBe("```js\n{{#if false}}\nKeep this\n{{/if}}\n```");
});
it("should preserve fence count (no fence markers lost or gained)", () => {
const input = "```js\n{{#if false}}\nHidden\n{{/if}}\n```";
const output = renderMarkdownTemplate(input);
expect((output.match(/`{3,}/g) || []).length).toBe((input.match(/`{3,}/g) || []).length);
});
it("should preserve multiple fenced code blocks unchanged", () => {
const input = "```js\ncode 1\n```\n\n```py\ncode 2\n```";
const output = renderMarkdownTemplate(input);
expect(output).toBe(input);
});
it("should handle fenced blocks with language tag and conditional outside", () => {
const input = "{{#if true}}\nKeep\n{{/if}}\n```python\nprint('hello')\n```";
const output = renderMarkdownTemplate(input);
expect(output).toBe("Keep\n```python\nprint('hello')\n```");
});
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage: these fenced-code-block tests cover only non-indented triple-backtick fences. Adding regression cases for (1) indented fences (up to 3 leading spaces) and (2) outer fences of 4–6 backticks containing an inner ``` block would help prevent reintroducing corruption with nested fenced blocks.

Suggested change
});
});
it("should preserve indented fenced code blocks unchanged", () => {
const input = " ```js\n{{#if false}}\nHidden\n{{/if}}\n ```";
const output = renderMarkdownTemplate(input);
expect(output).toBe(input);
});
it("should process conditionals outside indented fenced blocks while preserving inside", () => {
const input = "{{#if false}}\nRemove this\n{{/if}}\n ```js\n{{#if false}}\nKeep this\n{{/if}}\n ```";
const output = renderMarkdownTemplate(input);
expect(output).toBe(" ```js\n{{#if false}}\nKeep this\n{{/if}}\n ```");
});
it("should preserve outer fences of 4-6 backticks containing an inner triple-backtick block", () => {
const input = "````markdown\n```js\n{{#if false}}\nHidden\n{{/if}}\n```\n````";
const output = renderMarkdownTemplate(input);
expect(output).toBe(input);
});
it("should preserve fence count for nested fenced blocks with longer outer fences", () => {
const input = "`````\n```js\n{{#if true}}\nVisible\n{{/if}}\n```\n`````";
const output = renderMarkdownTemplate(input);
expect((output.match(/`{3,}/g) || []).length).toBe((input.match(/`{3,}/g) || []).length);
expect(output).toBe(input);
});

Copilot uses AI. Check for mistakes.
});
});
Loading