Summary
The Action writes its multiline report output using a heredoc whose framing depends on two unstated assumptions.
packages/action/src/index.ts:
const delimiter = `fixmap_${Date.now()}`;
appendFileSync(process.env.GITHUB_OUTPUT, `report<<${delimiter}\n${output}${delimiter}\n`);
1. No newline before the closing delimiter. The string is ...${output}${delimiter}\n — this only produces a valid heredoc because both renderMarkdownReport and renderJsonReport happen to end with \n. Nothing enforces that. A renderer that ever stops emitting a trailing newline turns the last report line into ...text fixmap_1753…, and the Actions runner then fails the step with an unterminated-heredoc error whose cause is nowhere near this file.
2. A predictable delimiter. GitHub's documentation specifically calls for a randomly generated delimiter. Date.now() is guessable to the second, and the runner hard-fails the step if any line of the value equals the delimiter.
Reaching case 2 from untrusted PR content looks impractical today — report text is either repo-derived file paths or alphanumeric tokens (TOKEN_SPLIT strips _), and every rendered line is prefixed with - — so this is hardening, not a live vulnerability. It is cheap to close properly.
Related, same function
GITHUB_STEP_SUMMARY has a 1 MiB cap; appendFileSync of a large report on a big monorepo will fail the step with no explanation from FixMap.
- The three
appendFileSync calls are separate, non-atomic writes to the same file.
Suggested fix
import { randomUUID } from "node:crypto";
const delimiter = `fixmap_${randomUUID()}`;
const body = output.endsWith("\n") ? output : `${output}\n`;
appendFileSync(process.env.GITHUB_OUTPUT,
`report<<${delimiter}\n${body}${delimiter}\n` +
`context-count=${report.contextFiles.length}\n` +
`test-route-count=${report.testRoutes.length}\n`
);
and truncate the step-summary write with an explicit "report truncated, see the report output" footer when it would exceed the limit.
Tests asserting the exact bytes written to GITHUB_OUTPUT would pin all of this down — see the entry-point coverage issue.
Summary
The Action writes its multiline
reportoutput using a heredoc whose framing depends on two unstated assumptions.packages/action/src/index.ts:1. No newline before the closing delimiter. The string is
...${output}${delimiter}\n— this only produces a valid heredoc because bothrenderMarkdownReportandrenderJsonReporthappen to end with\n. Nothing enforces that. A renderer that ever stops emitting a trailing newline turns the last report line into...text fixmap_1753…, and the Actions runner then fails the step with an unterminated-heredoc error whose cause is nowhere near this file.2. A predictable delimiter. GitHub's documentation specifically calls for a randomly generated delimiter.
Date.now()is guessable to the second, and the runner hard-fails the step if any line of the value equals the delimiter.Reaching case 2 from untrusted PR content looks impractical today — report text is either repo-derived file paths or alphanumeric tokens (
TOKEN_SPLITstrips_), and every rendered line is prefixed with-— so this is hardening, not a live vulnerability. It is cheap to close properly.Related, same function
GITHUB_STEP_SUMMARYhas a 1 MiB cap;appendFileSyncof a large report on a big monorepo will fail the step with no explanation from FixMap.appendFileSynccalls are separate, non-atomic writes to the same file.Suggested fix
and truncate the step-summary write with an explicit "report truncated, see the
reportoutput" footer when it would exceed the limit.Tests asserting the exact bytes written to
GITHUB_OUTPUTwould pin all of this down — see the entry-point coverage issue.