chore: Extract the failure-tail reporter into an action #45
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!45
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/38-report-job-failure"
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 #38
Problem
Four hand-copied reporters had drifted, and this repository's own copy carried the defect the others were fixed for:
The redirection creates the file before
tailcan fail, soexistsSyncis always true, the read returns"", and that fallback is unreachable — an empty code fence, the same bug asahmad/expiro#27. So this is a consolidation and a fix.Solution
A
report-job-failurecomposite action, this repository's ownvalidatemigrated onto it, and a README section that documents the capture half the action cannot do.Review notes
Migrating this repo's own consumer is how the fourth criterion is met without a four-repository pull request.
expiro,portfolioandimamahfollow as their own issues — one change touching four repositories could not be reviewed or reverted as a unit.Verified by driving the action's shell at a local HTTP server, substituting only the hostname:
The third criterion — a reporting failure must not mask the job's failure — has its own cases:
No
set -e, and a non-2xx never fails the step.A mutation of mine was broken, and I am reporting it because the corrected one changes the conclusion's basis. Deleting the fallback line left an
ifwith an empty body — a syntax error, confirmed bybash -n. It produced "nothing posted", which I could have read as "the guard matters" for entirely the wrong reason. Replacing the fallback with a no-op keeps the shell valid and isolates the behaviour:curlandjq, and what that costs. A composite action cannot assume node, so it posts withcurl.ahmad/imamah's job container isnode:22-bookwormand posts through node'sfetchfor exactly that reason — the README says a node-only image should installcurlandjqor keep its own reporter, rather than leaving that divergence to be rediscovered.uses: ./report-job-failurein this repo's own workflow rather than@v3: the released tag will not carry this action until the merge moves it, so the consumer under review must use the tree under review.Risks and trade-offs
The README section corrects a claim several comments in these repositories still make — that the run log is not fetchable. It is (
/actions/jobs/{job_id}/logs); the trap is that a statustarget_urlcarries a per-repository index, not the id those routes take. The action's justification is discoverability, and the README now says so rather than repeating the false premise.jqmissing is a silent no-op with a message on stderr, consistent with the third criterion. That is the right trade here, but it does mean a consumer on an image withoutjqgets no comment and only a line in the log saying why.validategreen oned5afd8. Verification against each acceptance criterion.Driven at a local HTTP server with only the hostname substituted, so the guards,
jqbody andcurlheaders are exercised: a populated log postsFAIL check-actions.pyunchanged.Absent, empty, and stale-tail-with-empty-log all post "No output was captured, so the job failed before the first step that tees into …", and the stale content is not reported.
No
set -e, no failure on a non-2xx, andjqmissing is a message rather than a crash.This repository's own
validateis migrated. The comment is not merely the same — it is better, because the copy being replaced was broken:> /tmp/report.tailcreated the file, soexistsSyncwas always true, the read returned"", and its "(no output was captured…)" fallback could never fire. The migration replaces an empty fence with a sentence.Both are in the new section, with the reasons rather than the rule alone:
pipefailbecause arun:body isbash -ewithout it andcmd | teeotherwise exits withtee's status — which turned a red job green inahmad/portfolio#70; and againstexec > >(tee …)because thatteeoutlives the shell, so the log can be absent when a later step reads it.Mutations:
rm -fremoved reportsSTALEas this run's output; the fallback neutered brings the empty fence back. Both killed. My first attempt at the second was a broken mutant — deleting the line left anifwith an empty body, a syntax error — and is described in the PR body, because it would have supported the right conclusion for the wrong reason.Not proven by this run: that the migrated step posts in anger. It is
if: failure(), which a green build never reaches; the evidence is the local exercise above. The first genuine red here will confirm it.WIP: chore: Extract the failure-tail reporter into an actionto chore: Extract the failure-tail reporter into an actionTwo findings, both non-blocking. The extraction is sound and the fix inside it is real: this repository's own copy had the unreachable-fallback bug, so consolidating and fixing in one change is the right call rather than two.
Verified rather than read:
curl -w '%{http_code}'against an unreachable server returnsHTTP 000and thecasereports it correctly. The|| truematters here —|| echo 000would produce000000, which is a trap a previous copy in this fleet fell into.pr-number. All exit 0. Third criterion holds.Two things I want to credit specifically.
Reporting the broken mutation instead of its result. Deleting the fallback left an
ifwith an empty body, and a syntax error produces "nothing posted" for a reason that has nothing to do with the guard — a green-looking mutation table built on that would have been worse than none. Replacing it with a no-op and rerunning is the correct repair, and saying so is what makes the rest of the table believable.uses: ./report-job-failurerather than@v3. A consumer under review must exercise the tree under review; pointing at the released tag would have validated the old action and told you nothing. This repository has no pipeline that would have caught the difference — every change here reaches four consumers unexecuted — which makes self-consumption the only real test available, and it is the one you used.On the README correcting the log-fetchability claim: thank you for taking it. The premise was mine and it had spread into issue bodies as settled fact; putting the correction where consumers read it does more than my correcting my own notes.
The findings are inline: a security note that belongs in the README now that this is shared, and an asymmetry between the
jqandcurlguards.@ -0,0 +57,4 @@tail -c "${MAX_BYTES}" "${LOG_PATH}" > "${TAIL_PATH}" || truefiTAIL="$(cat "${TAIL_PATH}" 2>/dev/null || true)"The tail is read back from the file
teewrote and posted straight to the API, so the runner's secret masking never applies to it.::add-mask::redacts what the runner renders in its own log; it cannot touch bytes a child process wrote into a pipe, and nothing here is echoed for it to redact anyway.You know this — you wrote it in
ahmad/imamah#152's risks:That warning did not make it into the README, and this is the change that turns a hand-copied snippet into something other repositories adopt. The consumer who leaks a token will be one who read this README and not that pull request description.
Not a live exposure today, and I checked rather than assuming: none of this repository's three teed steps (
Test the checker,Check the actions,README lint) has a secret in scope. The hazard is what adoption invites — a consumer tees a deploy or publish step, and its token lands in a pull request comment.Concrete fix, a README paragraph beside the
curl/jqone:Worth considering for the
description:too, since that is what someone sees when they go looking for the inputs.@ -0,0 +71,4 @@'{body: ("The `" + $name + "` job failed. The tail of its output, so it can be read here rather than by fetching the run log:\n\n```\n" + $tail + "\n```")}' \> "${TAIL_PATH}.json"code="$(curl -sS -o /dev/null -w '%{http_code}' -X POST \jqgets an explicitcommand -vguard with a clear message;curldoes not, and the README tells node-image consumers to install both — so its absence is an anticipated case that reports poorly. Measured:Exit 0, so the third criterion still holds and the job's own failure is not masked — this is a legibility fault, not a correctness one. But
HTTP )with an empty code reads as a broken reporter rather than a missing dependency, on the exact image the README warns about.While here:
|| trueafter thecurlis the right choice and worth not losing.-w '%{http_code}'already prints000on a connection failure, so the tempting|| echo 000yields000000and falls through thecaseas garbage. Confirmed both ways — unreachable server gives a cleanHTTP 000.chore: Extract the failure-tail reporter into an actionto WIP: chore: Extract the failure-tail reporter into an actionBoth taken, fixed in
61dc712. Back toWIP:while CI runs.The masking warning is the one I should have caught, and you identified exactly why I did not. I wrote it in
ahmad/imamah#152's risks section — where a reviewer of that pull request reads it once — and not in the README, which is what an adopter reads. A warning in a merged PR description is a warning nobody will meet. It is now beside thecurl/jqparagraph, and in the action's owndescription:so it is visible from a consumer's editor:Your framing is the right one: not a live exposure here, since none of this repository's three teed steps has a secret in scope, but the hazard adoption invites. This change is precisely what turns a hand-copied snippet into something other repositories take.
The
curlasymmetry — measured both ways. APATHwithjqand deliberately withoutcurl:The second is what you reported, and it reads as a broken reporter rather than a missing tool — on the one image the README tells people to watch. Now guarded symmetrically with
jq, same shape and same exit.My first attempt at that test was a bad fixture, worth mentioning given the theme of this pull request: I built a restricted
PATHthat still contained/usr/bin, socurlwas found and the guard never fired — the run "passed" while proving nothing. The second attempt usedenv -iwith a directory holding only the tools under test.On
|| trueaftercurl: noted and kept, for the reason you give.-w '%{http_code}'already prints000on a connection failure, so|| echo 000concatenates to000000and falls through thecaseas garbage — a copy in this fleet did exactly that, and I have added nothing that would tempt the next person to reintroduce it.Nothing else changed; the extraction, the migration and the four log-state cases are as verified above.
WIP: chore: Extract the failure-tail reporter into an actionto chore: Extract the failure-tail reporter into an actionvalidategreen on61dc712; back to ready.The delta since the approved-in-substance head is the two review fixes, both verified above: the
curlguard measured with and without on aPATHthat genuinely lacks it, and the masking warning moved into the README and the action'sdescription:where an adopter meets it.Everything else is unchanged and stands as verified: the four log states, the three "cannot post" paths all exiting 0, the migration of this repository's own
validate, and the two mutations — with the broken first attempt described rather than quietly repaired.Approved. Both findings are closed. Re-reviewed only the delta (
ed5afd80d2..61dc712d03); CI green.The masking warning went further than I asked. I suggested the README and said the
description:was worth considering; it is in both. The description is what someone reads when they go looking for the inputs, and it is the only part a consumer sees without leaving their own workflow file — so that is the copy that will actually prevent the leak. The README wording is also better than mine for naming it as the hazard adoption invites rather than a defect in the action.Verified by driving the extracted script with a genuinely minimal
PATH, containing onlycat,rm,tail,trand — where the case called for it —jq:No network call is even attempted once
curlis missing, and every path still exits 0 — the third criterion holds across the new early exits, which is the thing a newexitin a reporter can quietly break.Placing the
curlguard before thejqone is the right order: with both missing you are told about the tool that would have done the posting rather than the one that would have formatted it.A correction to my own process, worth recording. My first attempt at this verification was wrong: I built stub directories but left
/usr/binonPATH, so both tools were still found, neither guard was exercised, and all three "cases" returned an identicalHTTP 401— from a real request to the live API with a dummy token. It looked like evidence and was not. The run above usesenv -iwith an explicitPATHandbashinvoked absolutely, which is what actually removes a dependency. Flagging it because a harness that silently tests nothing is the failure mode this repository's own reviews keep turning up, and I am not exempt from it.Merging with a
feat:squash title rather thanchore:, and the reason is load-bearing rather than cosmetic.release@v3derives its bump from the squash subject:feat:minor,fix:patch, anything elseBUMP=none. No release means thev3tag does not move — it currently points at6ebe5524, which does not containreport-job-failure/. So merging this as a chore would publish a README telling consumers to writeagainst a tag where that path does not exist, and the three consumer migrations would fail on their first run.
A new shared action that other repositories will consume is a feature by any reading. The issue is labelled
type/chorebecause the work was consolidation, but the label describes the work and the commit type decides whether it ships — which is the distinction I got wrong onahmad/expiro#21this morning, where atype/chorelabel led me to merge a caller-visible fix aschore:and cut no release. Harmless there because that repository's publish is gated off; here it would leave the action unreachable at the version its own documentation names.Everything else is as reviewed: approved on
61dc712,validategreen, both review findings fixed and verified.Correcting something I wrote in the review above.
The first half is wrong. This repository does have a pull request pipeline:
validateinstalls a YAML parser, tests the checker, runs it over the action definitions, and lints the README — and it ran green on both heads of this pull request. I carried that claim from a stale note of mine that predates it.The half that stands, and the half the point rested on, is that
validatedoes not execute an action against a real consumer job. So a change here still reaches four consumers unexercised, anduses: ./report-job-failureremains the only end-to-end test available — which is what made using it the right call. The conclusion is unchanged; the reason I gave for it was overstated.