fix: A failed notification must not fail the release #65
Labels
No labels
priority/P0
priority/P1
priority/P2
priority/P3
size/L
size/M
size/S
size/XL
size/XS
state/blocked
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
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
amtronics/platform-actions#65
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Context
A release notification that cannot be delivered currently fails the release.
ahmad/trip's run 2462 (2026-09-07) is the example:versionsucceeded, but thenotifyjob exited 127 and the whole run is recorded as a failure, so a release that was fine reads as broken.The chain is
notify→openbao-fetch→ an on-demandapt-get install curl jqin the job container →jqmissing →curl … | jqexits 127. The immediate cause of the failed install was bullseye's dead security suite and is fixed (amtronics/infrastructure-vps#420), but the shape remains: every notify job reaches the network to install a tool before it can read a secret, and any failure along that path — a slow mirror, an OpenBao blip, a Telegram outage — is reported as a failed release.The conventions state that notification delivery is non-blocking and that an outage at the provider must never fail a build or a release. Today it does.
Scope
jqwhere it can be avoided.Acceptance criteria
jq, when the notify path runs, then it either does not needjqor its absence is not fatal.Picking this up. Plan, and one tension in the criteria I want stated before I write code rather than discovered in review.
What the log actually says. I pulled the job log for
ahmad/triprun 2462 (job 6322) instead of reasoning from the description.curlis present in that container — the(23) Failed writing bodyis curl noticing its output pipe died — andjqis not:1-composite-0.sh: line 16: jq: command not found, thenexitcode '127'. Line 16 isopenbao-fetch's AppRole login. The apt install above it printed the expired-bullseye-securityerror and did not produce ajq. So the failing dependency isjqalone, and it isopenbao-fetch's, not the notifier's.notify-telegramis already non-blocking — noset -e, an empty token is a skip, a delivery failure is a::warning::andexit 0. It never neededjq. Criterion 1 holds today, and I will show that rather than assume it.The approach.
jqfromopenbao-fetch, and the apt install with it.nodeis present in every job container on this fleet — act runs JS actions likeactions/checkout@v4withdocker exec cmd=[node …]inside the job container, and that step succeeded in this very run — so the JSON parsing moves tonode -e. That removes the network round-trip before a secret can be read, which is the "slow mirror" half of the problem as well as thejqhalf. It reaches every consumer with no edit, because they all pinopenbao-fetch@v3andv3tracks this branch.requiredinput, defaulting totrue. Whenfalse, a fetch failure writes an empty output file, emits a::warning::with the reason, and exits 0. The template's notify job passesrequired: false;notify-telegramthen sees an empty token and skips, which is the behaviour it already documents.Why the default is
trueand notfalse. Making the fetch non-fatal everywhere would be smaller and I am not doing it.larda's deploy reads.env.secretswith awhile … done < .env.secretsunderset -e, so a missing file fails loudly there — but that is one repository's guard, and a deploy that proceeds with empty secrets because the fetcher shrugged is a worse failure than the one this issue is about. A secret fetcher stays fatal by default.The tension, stated plainly. Criterion 5 asks that no consumer repository need an edit. Item 1 meets that. Item 2 cannot, because the notify job is copied into each consumer, not called — I checked, and no consumer references a reusable workflow. So
required: falsereaches a repository only when itsrelease.ymlis refreshed. My reading: criterion 5 rules out fixing this by hand-editing eleven repositories, and is satisfied by the central fix carrying the cause that has actually fired; the residual tolerance for a genuine OpenBao outage arrives with the template. I will list the consumers carrying the old copy and open one follow-up, the way #66's fourth criterion asks. If you meant criterion 5 more strictly than that, say so and I will stop at item 1.Verification. The pipeline here only parses YAML and runs
bash -n, so it cannot see whether the node reader is correct. I will test that myself against the failure mode already recorded for this substitution: a multi-byte value split across a read boundary silently becoming replacement characters withoutsetEncoding('utf8'), and non-scalar values rendering as[object Object]wherejqwrote JSON. A corrupted password reaching.envfails at the application, far from the step that broke it, so this is the part worth measuring rather than reading.Files:
openbao-fetch/action.yml,templates/release.yml,README.md.Correction to the plan above, and I am putting the issue back rather than carrying on — the work has been redirected to
ahmad/larda.Item 1 of that plan is already done, and I read a stale tree. I described
openbao-fetchas still installingjq. It does not:2bccd5e("fix: Parse OpenBao responses with node, not jq", #58) landed onmainand both parses arenode -ewithsetEncoding("utf8"), with the apt fallback reduced to curl-only. My worktree was four commits behindorigin/mainand I quoted it as current. The run-2462 evidence stands — that failure predates2bccd5e— but the conclusion I drew from it about what still needs doing was wrong, and I would have written a PR removing something already removed.So criterion 3 is already satisfied on
main, and what remains for this issue is narrower than the plan says: therequiredinput and the template's notify job, which is criteria 2 and 5, plus showing that criteria 1 and 4 hold as they stand.One thing I noticed and am not folding in. The node renderer writes
key + "=" + value, so a non-scalar field renders[object Object]wherejqwrote{"a":1}. Every value insecret/<app>is a string today, so nothing is broken now;typeof value === "string" ? value : JSON.stringify(value)restores parity. That is a different defect from this one and belongs in its own issue rather than in a notify fix.Unassigning and putting this back to
state/ready. Nothing was pushed and no branch remains.Resuming this, and the design question I left open above now has data behind it rather than a judgement.
I checked whether
openbao-fetchcould simply be non-fatal by default, which would have satisfied both criterion 2 and criterion 5 with no consumer edit at all. It cannot. I surveyed every use of the action across the fleet and looked at what follows it:ahmad/expirodeploy,ahmad/lardadeploy,suhba/suhba-docspublish and validate,suhba/suhba-webcms-pr,ahmad/imamahversion.deployjob ofahmad/trip,ahmad/portfolio,bayan/kidpoints,bayan/stem,suhba/khitta,suhba/suhba-webandahmad/imamah.So a non-fatal default would let a deploy proceed with an empty
.envon seven repositories: containers started with noDATABASE_URL, no error, a green run. That is a worse failure than the one this issue is about, and it is the "trades a loud failure for a quiet one" shape the fleet has been bitten by before. The fetcher stays fatal by default.The blast radius of the actual problem, since I now have the numbers. A
notifyjob that fetches from OpenBao exists in eight repositories —ahmad/portfolio,ahmad/trip,ahmad/imamah,bayan/kidpoints,bayan/stem,suhba/khitta,suhba/suhba-docs,suhba/suhba-web— and inportfolioit is onpr.ymlandmonitoring.ymlas well. Every one of those turns an OpenBao blip into a failed run for a build that was fine.What I am building, unchanged from the plan above except that item 1 is already done by
2bccd5e:openbao-fetchgainsrequired, default"true". Withrequired: falsea failure writes an empty output file, emits a::warning::naming the reason, and exits 0.templates/release.yml's notify job passesrequired: false.notify-telegramalready treats an empty token as a skip, so the run continues and says why.Criterion 5, stated plainly rather than claimed. The central change reaches every consumer through
@v3for the jq half, which is what has actually been failing. The tolerance half needs each consumer's copiednotifyjob to gain one line, because the job is copied rather than called — I verified again that no consumer references a reusable workflow. I will list them on a follow-up issue rather than edit eight repositories inside this PR.