fix: Build the comment body without requiring jq #53
No reviewers
Labels
No labels
priority/P0
priority/P1
priority/P2
priority/P3
size/L
size/M
size/S
size/XL
size/XS
state/done
state/in-progress
state/in-review
state/needs-refinement
state/ready
type/bug
type/chore
type/feature
type/refactor
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
amtronics/platform-actions!53
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/52-report-without-jq"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Issue
Closes #52
Problem
report-job-failurebuilt its comment body withjqand refused to post without it —command -v jq || { echo …; exit 0; }.jqis absent fromnode:22-bookworm, which is the image every consumer of this action runs in. This repository already knew that:release.ymlsays so in a comment and reaches fornodeinstead, for exactly this reason.Because the guard exits 0, the failure is invisible. The action reports success and posts nothing.
That is not hypothetical — this repository's own
validatehas been a silent no-op since #38 migrated it. It simply has not failed since, so nothing revealed it. The inline reporter it replaced usednode, notcurl+jq, precisely because of this image.Solution
Prefer
jq, fall back tonode'sJSON.stringify, and only give up when neither exists.Both are real JSON encoders, which is the property that matters:
jq --argwas chosen over hand-built JSON because a tail containing a quote, a backslash or a newline breaks string concatenation.JSON.stringifygives the same guarantee.Review notes
Driven by extracting the
run:body and running it underenv -iwith aPATHcontaining only the stubbed tools, so "not installed" is real rather than simulated, and with a stubcurlthat captures the payload that would have been POSTed. The log fixture contains a double quote, a backslash, a backtick and newlines.jq+nodenodeonlyNeither jq nor node is installed…The middle row is the fix; against the previous code that case posted nothing.
On "identical body", stated precisely. The raw JSON differs in whitespace —
jq -npretty-prints,JSON.stringifyis compact — but the decodedbodystring is equal, and the tail round-trips intact through both paths:So the comment the API receives is the same; the bytes on the wire are not. I would rather say that than claim byte-identity.
The
curlguard is unchanged —curlis present in these images.check-actions.pypasses, its own suite is 31/31, README lint is clean.Risks and trade-offs
An image with neither
jqnornodestill posts nothing, and still exits 0. That is deliberate and unchanged: a reporter must not fail a job that already failed. It is now the only way to reach that branch, where before it was the normal case.Once this releases, #46 can proceed — migrating
ahmad/portfolio,ahmad/imamahandahmad/expiroonto the action becomes safe, where today it would silently disable three working reporters.WIP: fix: Build the comment body without requiring jqto fix: Build the comment body without requiring jqMarked ready on
bb2c9c3.validategreen.Verified against each acceptance criterion of #52, by extracting the
run:body and running it underenv -iwith aPATHcontaining only stubbed tools — so "not installed" is genuinely absent rather than simulated — and a stubcurlthat captures the payload that would have been POSTed. The log fixture contains a double quote, a backslash, a backtick and newlines, because quoting is where this breaks.nodebut nojq→ the comment is posted.exit 0, payload captured, 181 bytes. Against the previous code this case posted nothing, which is the kill.jqpresent → posted, body identical to thejq-built one. Posted, 187 bytes. The decodedbodystrings are equal; the raw JSON differs in whitespace only, becausejq -npretty-prints andJSON.stringifyis compact. The API receives the same body; the bytes on the wire are not identical, and I would rather state that than claim they are.'```\nline1 "quoted"\nback\\slash and `tick`\nlast\n```'.jqnornode→ says so and exits 0 without failing the job.exit 0, nothing posted,Neither jq nor node is installed, so the failure comment cannot be built.Criterion 5 — a comment appearing when this repository's own
validatefails — cannot be checked here: it needs a releasedv3and a genuinely failingvalidate. It is the one claim I am not making from this branch.check-actions.pypasses on all six action files, its own suite is 31/31, README lint clean.A note on how the earlier version passed review. #38 migrated this repository's
validateonto the action and was accepted as proving equivalence. It did not:validateruns innode:22-bookworm, the action neededjq, and the guard exits 0 — so the migration replaced a workingnodereporter with one that could not run there, and everypr.ymlrun since has passed, so nothing surfaced it. The equivalence that was demonstrated was of the output, not of the runtime. Worth stating because the same shape — a guard that exits 0 on a missing dependency — will read as success anywhere it appears.Approved. CI green. And this one found a real defect that I approved and should have caught.
The premise is correct, and I verified it from the image definitions rather than taking it.
node:22-bookwormderives frombuildpack-deps:bookworm→scm→curl→debian. The complete package list across that chain isca-certificates curl gnupg netbase sq wget git mercurial openssh-client subversion procpsplus the compiler/-dev set — nojq. And this repository's ownvalidatejob runscontainer: node:22-bookwormand calls./report-job-failureonif: failure(). So the reporter has been unable to build a body here since#38, exiting 0 with a message on stderr that nobody reads.That is my miss, and worth naming precisely because the shape recurs. I reviewed
#45, and in the follow-up I raised thejq/curlasymmetry and asked for a symmetriccurlguard — I even quoted the README line about a node-only image needing both installed. I tested that thejqguard behaved correctly: exits 0, prints its message, never posts. What I never asked was which side of that guard the actual consumer was on. I treated "no jq" as a hypothetical about future adopters while it was already true of the only consumer in the repository, sitting three files away inpr.yml.Verifying that a guard works is not the same as verifying that the branch it guards is the unreached one. A guard whose fallback path is the live path is a no-op wearing a guard's clothes.
The fix, driven from this branch's own YAML under
env -iwith only the named tools onPATH:Your care about "identical body" was the right call. The encodings differ —
jq -npretty-prints,JSON.stringifyis compact, hence 187 against 181 — but I decoded both and thebodystrings are equal, with the tail surviving intact through both paths including a double quote, a backslash, a backtick and newlines:Claiming byte-identity there would have been wrong and would have been believed. Saying the decoded bodies match, and why the bytes do not, is the accurate claim.
Keeping both encoders rather than dropping
jqis also right: both are real JSON encoders, which is the property that matters, and hand-built JSON would break on exactly the characters your fixture contains.