fix: Let a notify job survive an unreachable vault #67
No reviewers
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
amtronics/platform-actions!67
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/65-notify-non-blocking"
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 #65.
Problem
ahmad/triprun 2462:versionsucceeded,publishanddeploywere skipped, and the run is recorded as a failure — becausenotifycould not reach a secret. A release that was fine reads as broken, which is the opposite of what a notification is for.The immediate cause of that run (a dead apt suite, then a missing
jq) is already fixed by2bccd5e. The shape is not: everynotifyjob reaches OpenBao before it can say anything, and any failure along that path fails the run it is reporting on.Solution
openbao-fetchgainsrequired, default"true". Withrequired: falsea failed fetch writes an empty output file, emits a::warning::naming the reason, and exits 0. The template'snotifyjob passes it;notify-telegramalready treats an empty token as a skip.Review notes
Why the default is fatal, with the survey rather than the instinct. Making the fetch non-fatal everywhere would have satisfied criteria 2 and 5 with no consumer edit at all, so I checked whether it was safe. It is not: I looked at what follows every use of this action across the fleet, and the
deployjob ofahmad/trip,ahmad/portfolio,bayan/kidpoints,bayan/stem,suhba/khitta,suhba/suhba-webandahmad/imamahhas no guard — an empty.envwould start containers with noDATABASE_URLand report success. Onlyexpiroandlardafail loudly at the point of use. So the safe default is the one that stops.shell: bashruns with-ealready set, and that nearly shipped as a bug. Writingset -uo pipefaildoes not turn-eoff; the script now saysset +eexplicitly. Without it the first failing command substitution exits beforefailis reached, sorequired: falsewould have done nothing at all. Mutation-checked: removingset +efails three behaviour cases.Tested against stubbed commands, not just parsed. Six cases in the behaviour suite drive the real
run:body with acurlthat refuses everything and one that answers: the default stops with the reason on stderr,required: falsewarns and exits 0, and the value never appears on stdout or stderr in either outcome. Both halves mutation-checked — droppingset +efails three, makingfail()ignorerequiredfails three.The harness found a dependency I had added without noticing. My first version used
mktempfor the atomic write; the minimal PATH refused it, which is exactly the question worth asking in a repo that has been removing runtime dependencies (jq, the apt install). It writes to"$OB_OUT.partial"instead and moves it into place.Criterion 5, stated rather than claimed. The central change reaches every consumer through the
@v3tag. The tolerance half needs each consumer's copiednotifyjob to gain one line, because the job is copied and not called — I checked again that no consumer references a reusable workflow. Eight repositories have such a job. I have not edited them here; that is a fleet propagation and it belongs in its own change, listed on the follow-up.Verified
check-actions, its own test suite, the behaviour suite and the README lint all exit 0.Risks and trade-offs
A job that sets
required: falseand then genuinely needs the secrets will now fail later and less clearly than it would have. The README says where the flag belongs and the action's own input description repeats it, because this is a foot-gun that reads as a convenience.The
notifyjobs in eight repositories keep today's behaviour until they are updated. That is not a regression — it is the current failure, unfixed for them — but it does mean this PR alone does not close the issue's fifth criterion for existing consumers, and I would rather say so than let the merge imply otherwise.WIP: fix: Let a notify job survive an unreachable vaultto fix: Let a notify job survive an unreachable vaultVerification on
51d52d7.PR / validategreen — which here meanscheck-actions, its own test suite, the behaviour suite and the README lint.Criterion 1 — Telegram unreachable leaves the conclusion unchanged. Already true before this change, and I checked rather than assumed:
notify-telegramruns withoutset -e, treats an empty token as a skip, and turns a delivery failure into::warning::plusexit 0. Nothing in this PR touches it.Criterion 2 — a failed secret fetch leaves the conclusion unchanged, with the reason visible. Driven against a stubbed
curlthat refuses everything:required: falseexits 0 with::warning::openbao-fetch: AppRole login failedon stdout, and the default still exits 1 with the reason on stderr. Both are cases in the behaviour suite rather than claims here.Criterion 3 —
jqabsent is not fatal. Already satisfied by2bccd5e, which replaced both parses withnode. I asserted the opposite in an earlier comment on this issue, from a worktree four commits stale, and corrected it there.Criterion 4 — a genuinely failed build still reports failure. Untouched: the status the notifier sends is computed from
needs.*.result. A tolerant fetch cannot make a failed build look green, because the build's own jobs are what fail the run.Criterion 5 — no consumer edit. Honestly: not fully met, and it cannot be by a change to this repository alone. The
notifyjob is copied into each consumer rather than called, sorequired: falsereaches a repository when its own workflow gains the line. Eight repositories are affected; every one is listed, with its exact workflows, on the follow-up issue below. What this PR does deliver to all of them through@v3is the removal of the failure mode that has actually been firing.What I checked before choosing the default, since it decides whether criterion 5 could have been met the easy way: the
deployjob oftrip,portfolio,kidpoints,stem,khitta,suhba-webandimamahhas no guard after the fetch. A tolerant default would have started containers with noDATABASE_URLand reported success — a worse failure than the one being fixed.Mutation-checked in two places. Removing
set +efails three cases; makingfail()ignorerequiredfails three. The harness also caught anmktempI had introduced without noticing, in a repository that has been deliberately shedding runtime dependencies.The propagation is filed as #68, with the eight repositories and their exact workflows.
Approved. The reasoning behind the default is the substance of this change and it holds up — I re-ran your mutations and then widened your survey mechanically, which made the argument stronger rather than weaker.
The mutations, reproduced:
set +e→ 3 failures, exactly as described. That is the one that nearly shipped as a no-op, and it is worth restating why:token=$(curl … | node …)under-eexits on the substitution's status, sofailis never reached andrequired: falsewould have done nothing at all while looking correct.fail()always stop → 3 failures, your figure.fail()always warn — a default silently flipping to non-fatal — fails one case,required by default: an unreachable vault stops the job. The predicate is pinned in both directions, but that single guard is carrying the whole weight of the decision below, and it is the direction whose failure is silent rather than loud.Your survey is right and short by two. Walking every repository the API lists rather than a hand-kept list: eleven repositories use
openbao-fetch, across thirty workflow files.ahmad/nameyandsuhba/suhba-docsare not in the PR's list, and both have adeploy.ymlcalling the action with no-zguard anywhere in the file — so the unguarded population is nine, not seven. Nothing about the conclusion changes; the safe default is even safer than the description argues.And the action is called from more than
deployandrelease.monitoring.ymlinahmad/portfolio,ahmad/trip,ahmad/imamah,suhba/khittaandBayan/kidpoints;pr.ymlinahmad/nameyandahmad/portfolio;docs-build.ymlinsuhba/suhba-docs;cms-pr.ymlinsuhba/suhba-web. A monitoring workflow going red because the vault blinked is the same shape as the notify case this PR exists for, so those are candidates for the follow-up propagation alongside the eightnotifyjobs — worth naming there so the next pass does not have to rediscover them.The rest, checked:
[ "${OB_REQUIRED}" != "false" ]fails closed: any value that is not exactlyfalse— a typo,False,0, an empty string — is treated as required. That is the right polarity for a flag whose wrong answer is a silent deploy.grep "^$k=" .env 2>/dev/null | cut … || truegenuinely tolerates an absent or empty file underset -euo pipefail; the|| truecovers the whole pipeline inside the substitution, sopipefailcannot resurrect it.OB_PATH, never a value, and the "never echoed" case asserts that on both streams."$OB_OUT.partial"instead ofmktempis the right call for the reason you give, and the harness catching it is the harness doing its job.On not propagating here: agreed, and saying plainly that this alone does not close criterion 5 for existing consumers is worth more than a merge that implies otherwise.
@ -28,0 +34,4 @@notification. Leave it at the default for anything that deploys or buildswith the values.required: falsedefault: "true"The survey behind this default is the load-bearing part of the change, so I walked it mechanically rather than checking the names given.
Eleven repositories use
openbao-fetch, across thirty workflow files — the description's list is nine. The two missing areahmad/nameyandsuhba/suhba-docs, and both have adeploy.ymlthat calls the action with no-zguard anywhere in the file. So the unguarded population is nine, not seven, and the case fordefault: "true"is stronger than the PR argues rather than weaker.The action is also called outside
deployandrelease:monitoring.yml(portfolio, trip, imamah, khitta, kidpoints),pr.yml(namey, portfolio),docs-build.yml(suhba-docs) andcms-pr.yml(suhba-web). Several of those are the same shape as the notify case — a workflow whose whole purpose is reporting or checking, failed by a vault it only needed for one step — so they belong on the follow-up beside the eightnotifyjobs.Nothing to change here. Recording it because the follow-up will otherwise be scoped to a list of eight, and because a claim of the form "every consumer does X" is only as wide as the enumeration behind it.
Merged as
512a234. Both of your additions are right, and the second one changed a number I had put in a table as though it were settled.On the survey being short by two — it was short by three. You walked the fleet properly and found
ahmad/nameyandsuhba/suhba-docsmissing from my list. I had surveyed my local checkouts, which can only see repositories I happen to have cloned and current, and that is not an enumeration of anything. Re-running it against the API — every repository the server lists, every file under.forgejo/workflows/, eachopenbao-fetch@attributed to its enclosing job — gives 12 repositories across 35 workflow files.Bayan/bayan-webis in neither of our lists.So the unguarded population is larger again, and the conclusion is unchanged in direction and stronger in degree. I have replaced the table on #68 with the mechanically-derived one and written down the method, because "every consumer does X" is only as wide as the enumeration behind it and mine was not wide.
Your point about
monitoring.ymlandpr.ymlis the more useful half. Classifying by whether the job's only use of the secrets is a notification: 26 jobs across 10 repositories, and they are not all inrelease.yml—pr.ymlinportfolio,nameyandbayan-web,monitoring.ymlin five repositories. A pull-request check going red because the vault blinked is the same failure as a release doing it and lands somewhere more annoying. All 26 are now listed on the follow-up; the 57 jobs that genuinely need the secrets are listed too, so the next pass has both halves and does not have to re-derive which is which.On the thin guard. You are right that
required by default: an unreachable vault stops the jobis carrying the whole weight of the default, and that its failure direction is the silent one. I have left it as the single case rather than padding it — the predicate is one comparison and the case does exercise it — but it is the assertion I would want re-run first if that comparison is ever touched, and it is worth having said so here rather than assuming the next reader infers it.