Skip to content

Commit b7cc564

Browse files
committed
fix(observability-map): let the renderer write its own comment file
1 parent 82bb097 commit b7cc564

5 files changed

Lines changed: 83 additions & 12 deletions

File tree

.github/workflows/observability-map.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,15 +300,21 @@ jobs:
300300
COMPARE_URL: ${{ github.server_url }}/${{ github.repository }}/compare/${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}
301301
run: |
302302
rm -f /tmp/comment.md
303+
# `--out` rather than a stdout redirect, for the reason the scan steps above give, and with a
304+
# worse failure mode than theirs: the marker has to be the comment's first line for the
305+
# lookup to find it, so a line printed ahead of the document makes every push post a new
306+
# comment instead of updating the one already there. Held by
307+
# `it("let the renderer write its own comment rather than capturing stdout")`.
303308
render() {
304309
pnpm --filter @internal/observability-map exec tsx src/report/prCommentCli.ts \
305-
--commit-sha="$HEAD_SHA" --commit-url="$COMPARE_URL" "$@"
310+
--commit-sha="$HEAD_SHA" --commit-url="$COMPARE_URL" --out=/tmp/comment.md.partial "$@"
306311
}
307312
308313
# Every write goes through this, so a renderer that exits non-zero never leaves a 0-byte
309314
# comment.md for the upsert to skip in silence.
310315
emit() {
311-
if render "$@" > /tmp/comment.md.partial; then
316+
rm -f /tmp/comment.md.partial
317+
if render "$@"; then
312318
mv /tmp/comment.md.partial /tmp/comment.md
313319
return 0
314320
fi

internal-packages/observability-map/INTERNALS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,12 @@ because
489489
`JSON.parse` and degrades the workflow to the stale-report comment permanently. What is asserted is
490490
the shape that cannot have the bug rather than the pinned 10.33.2 that happens not to.
491491

492+
The render step writes through `--out` for the same reason, and its failure mode is the worse of the
493+
two. `renderPrComment` puts the marker on the first line and the lookup finds the comment with
494+
`startswith` on it, so a line printed ahead of the document does not degrade the comment, it hides it:
495+
the next push finds no id and posts a second comment, and no later run can reconcile either. The scan
496+
steps degrade to a stale report, which at least stays one comment.
497+
492498
The corpus runs on the package's own paths and on a schedule rather than on every route pull request,
493499
because it measures the tool's resistance to laundering, which only an edit to the tool can weaken,
494500
and it costs four and a half minutes. The nightly covers tree drift late rather than not at all.

internal-packages/observability-map/src/integration.test.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,18 @@ describe("the report workflow reconciles a comment the paths no longer reach", (
177177

178178
/**
179179
* Asserts the shape that cannot have the stdout-capture bug rather than the pnpm version that happens
180-
* not to. Why, and why the render step is left alone: INTERNALS.md, "Tests, timeouts and CI".
180+
* not to. Why: INTERNALS.md, "Tests, timeouts and CI".
181181
*/
182-
describe("the report workflow's two scan steps", () => {
182+
describe("the report workflow's scan and render steps", () => {
183+
it("let the renderer write its own comment rather than capturing stdout", () => {
184+
const render = steps(read(REPORT)).find((step) => step.startsWith("📝 Render"))!;
185+
expect(render).toBeDefined();
186+
expect(render).toMatch(/--out=\S+\.md\S*/);
187+
// The marker has to be the comment's first line for the lookup to find it, so a redirect that
188+
// could put a package-manager banner ahead of the document costs the upsert, not just tidiness.
189+
expect(render).not.toMatch(/render[^\n]*>\s*\S*\.md/);
190+
});
191+
183192
it("let the scanner write its own report rather than capturing stdout", () => {
184193
const scans = steps(read(REPORT)).filter((step) => step.startsWith("🔎 Scan"));
185194
expect(scans).toHaveLength(2);

internal-packages/observability-map/src/report/prCommentCli.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
1+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
22
import { tmpdir } from "node:os";
33
import { join } from "node:path";
44
import { main, type Io } from "./prCommentCli.js";
@@ -197,6 +197,43 @@ describe("prCommentCli", () => {
197197
expect(r.err).not.toContain(" at ");
198198
});
199199

200+
describe("--out", () => {
201+
it("writes the comment to the file and leaves stdout empty", () => {
202+
const outPath = join(dir, "out-delta.md");
203+
const r = run(headPath, basePath, `--out=${outPath}`);
204+
expect(r.code).toBe(0);
205+
expect(r.out).toBe("");
206+
207+
const written = readFileSync(outPath, "utf8");
208+
expect(written.split("\n")[0]).toBe("<!-- observability-map-report -->");
209+
});
210+
211+
it("writes an empty file when there is nothing to post, rather than no file", () => {
212+
const outPath = join(dir, "out-nothing.md");
213+
const r = run(unchangedPath, unchangedPath, `--out=${outPath}`);
214+
expect(r.code).toBe(0);
215+
expect(existsSync(outPath)).toBe(true);
216+
expect(readFileSync(outPath, "utf8")).toBe("");
217+
});
218+
219+
it("writes the resolved and scan-failed comments too", () => {
220+
for (const mode of ["--resolved", "--scan-failed"]) {
221+
const outPath = join(dir, `out${mode}.md`);
222+
expect(run(mode, `--out=${outPath}`).code).toBe(0);
223+
expect(readFileSync(outPath, "utf8").split("\n")[0]).toBe(
224+
"<!-- observability-map-report -->"
225+
);
226+
}
227+
});
228+
229+
it("truncates a file left by an earlier run", () => {
230+
const outPath = join(dir, "out-stale.md");
231+
writeFileSync(outPath, "stale content from a previous push\n");
232+
expect(run(unchangedPath, unchangedPath, `--out=${outPath}`).code).toBe(0);
233+
expect(readFileSync(outPath, "utf8")).toBe("");
234+
});
235+
});
236+
200237
it("exits 1 with a one-line message when base.json is malformed", () => {
201238
const malformedBasePath = join(dir, "malformed-base.json");
202239
writeFileSync(malformedBasePath, "not json at all");

internal-packages/observability-map/src/report/prCommentCli.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { readFileSync } from "node:fs";
1+
import { readFileSync, writeFileSync } from "node:fs";
22
import { resolve } from "node:path";
33
import { fileURLToPath } from "node:url";
44
import type { MapReport } from "../score.js";
@@ -72,6 +72,21 @@ export function main(argv: string[], io: Io = processIo): number {
7272
const headPath = positional[0];
7373
const basePath = positional[1];
7474

75+
/**
76+
* The marker has to be the document's first line or the workflow's lookup cannot find the comment it
77+
* left, and every later push posts a new one instead of updating it. `--out` keeps stdout free for
78+
* whatever a tool decides to announce, the same reason `src/cli.ts` has it. An empty write is a real
79+
* outcome and not a failure: it is how "post nothing" reaches the workflow's `-s` check.
80+
*/
81+
const outPath = flag(args, "out");
82+
const write = (text: string) => {
83+
if (outPath === undefined) {
84+
if (text) io.out(text);
85+
return;
86+
}
87+
writeFileSync(outPath, text);
88+
};
89+
7590
let commit: CommitContext | undefined;
7691
try {
7792
commit = commitFrom(args);
@@ -81,12 +96,12 @@ export function main(argv: string[], io: Io = processIo): number {
8196
}
8297

8398
if (scanFailed) {
84-
io.out(`${renderScanFailedComment(commit)}\n`);
99+
write(`${renderScanFailedComment(commit)}\n`);
85100
return 0;
86101
}
87102

88103
if (resolved) {
89-
io.out(`${renderResolvedComment(commit)}\n`);
104+
write(`${renderResolvedComment(commit)}\n`);
90105
return 0;
91106
}
92107

@@ -106,12 +121,10 @@ export function main(argv: string[], io: Io = processIo): number {
106121
}
107122

108123
if (hasDelta(head, base)) {
109-
io.out(`${renderPrComment(head, base, commit)}\n`);
124+
write(`${renderPrComment(head, base, commit)}\n`);
110125
return 0;
111126
}
112-
if (existingComment) {
113-
io.out(`${renderResolvedComment(commit)}\n`);
114-
}
127+
write(existingComment ? `${renderResolvedComment(commit)}\n` : "");
115128
return 0;
116129
}
117130

0 commit comments

Comments
 (0)