fix(post-release-bump): trigger on the tag, which is the only signal 5 of its 6 repos raise (backend#3681) - #474
Conversation
…5 of its 6 repos raise (backend#3681)
post-release-bump.yml was driven by the `release` event, on the assumption that
callers would trigger on `release: published`. Measured 2026-09-11 across the six
repos that declare a version_file in release-train/repos.yml, that event reaches
exactly ONE of them:
client gh_release: true -- the train runs `gh release create`
under a PAT, so the event fires. FIRES
cli, data-ingestors their own publish workflows create the Release under
GITHUB_TOKEN, and GitHub does not start workflow runs
from events raised by GITHUB_TOKEN. NEVER FIRES
tracebloc-py-package no GitHub Release is created at all -- promote-repo.sh
design-system POSTs a bare tag ref for every repo that does not set
design-system-v2 gh_release. NEVER FIRES
So the caller rollout this was parked for would have installed five workflows
that can never run. Worse, the three raising no release event at all are exactly
the repos the reusable was written for: design-system after v1.3.0 and v1.4.0,
and design-system-v2 at v0.1.0, all cited in its own header.
Callers now trigger on `push: tags: ['v*']`. The tag is the one signal all six
produce, because the train always creates it on the way to publishing. The
release path still works, so `client` is unaffected.
THE PRERELEASE TEST HAD TO MOVE, AND IT IS THE SHARP EDGE HERE. It was a job-level
`if:` on `github.event.release.prerelease`; a tag push has no such field, and the
caller's `v*` glob matches every rc the train cuts -- once per hop per repo. The
shape test already in the script does NOT catch them: it is a glob,
`[0-9]*.[0-9]*.[0-9]*`, and `1.2.3-rc.1` satisfies it, because `*` matches the
suffix too. Dropping the field without adding an explicit rc test would not have
failed loudly; it would have bumped the version on every rc tag. An rc is now a
green no-op, checked before the shape test.
That trap is why this lands with a selftest rather than on inspection.
scripts/tests/post-release-bump-selftest.sh extracts BOTH `case` blocks out of the
YAML by anchor and runs them, so the workflow cannot drift from what the suite
checks -- a selftest carrying its own copy of the logic is the inert-verification
shape backend#1729 catalogued. Mutation-proven: neutering the rc arm turns exactly
the four rc assertions red and leaves accept/refuse green.
The suite also found a false green in its own first draft -- the three "refuse"
cases were passing on an `unbound variable` abort rather than on the shape test,
and would have kept passing with that test deleted. Noted in the file where the
fix is.
repo-inventory.yml's `post_release_bump_parked` row is updated rather than deleted:
the callers are still pending, so the exemption still holds, but it now records WHY
the rollout did not simply happen. The row still goes when the callers land.
Refs backend#3681.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
CI: The audit reported 121 findings including Same for This PR's diff cannot cause it: The run did surface three findings that are genuine and unrelated to me — Everything else on this PR is green: both |
|
The two red checks here aren't this PR's. Re-ran Not fixing it here — it's unrelated to the tag trigger and the inventory is org-governance data, so whether that entry gets dropped or renamed-with-archived-status is a call for whoever did the archival. Leaving this PR parked on |
|
Re-ran the audit to check whether the failure was transient. It is not — but the numbers moved between the two runs of the same head: 121 → 130 drift findings, 2 → 1 missing repos, 2 → 1 failed reads. Same commit, same inventory, different answer. That rules out this PR as the cause more firmly than the diff argument did, and points at a degraded/rate-limited credential on the audit rather than real drift. Detail on backend#3690.
This PR stays blocked until #3690 is fixed. Everything else on it is green (37 checks). |
|
The two red checks are not caused by this PR —
This PR's only What actually unblocks it: reconcile Also worth flagging separately: the org's |
|
Correcting my own recommendation above, having found the context. These are not accidental drift — they are planned work under epic backend#3582 (Public repository restructuring), specifically #3594 ( So my earlier "re-point the rows at the Either way it is not this PR's work, and this branch should not be edited to go green. It also blocks #472, #471 and #447 identically, since |
LukasWodka
left a comment
There was a problem hiding this comment.
Requesting changes. Two things are structural, the rest is tightening.
CI is red on the required gate, and it is not this PR's text: the gate arms because repo-inventory.yml is a guarded contract file, and it fails because caller-drift.yml reports 130 findings — all "MISSING required caller" on repos whose callers already resolve to tracebloc/org-config, i.e. drift from the reusables migration, plus org-config's own rows. None mention post-release-bump. Dropping the inventory hunk from this PR makes the gate short-circuit ("does not touch the repo contract") and is also the right call on its own merits (thread on repo-inventory.yml:362).
Blockers, one per line:
- The fix lands in
.github's copy, but org-config is the declaredsource_repo, itspost-release-bump.ymlis unchanged onmainanddevelop(stillTAG: ${{ github.event.release.tag_name }}, no rc arm), and nothing syncs the two — so a caller written against the declared host gets an emptyTAGon every tag push. Thread on line 137. repo-inventory.ymlnow cites backend#3681 and then says it cites no ticket;reason-citationsruns inside the requiredselftestsjob and reddens on a closed citation, and the callers PR is what closes #3681. Thread on line 362.
Tightening (threads inline):
- Job-level
if: github.event.release.prerelease != true(line 98) is still present although the comments say the test moved; one rule, please. - The shape glob accepts
v1.2.3-beta.1,v1.2.3-hotfix,v1.2.3.4,v1.2.3rc1— reproduced through the extracted blocks — andversion_file.py cmpis what actually refuses them, with a message that blames the version file. cli'srelease.ymland the train's promote script define stable as exactly^v[0-9]+\.[0-9]+\.[0-9]+$; the same anchored rule here would make the rc arm and the shape test one decision. - The rc no-op is evaluated after the token mint, two checkouts and the PAT guard; hoist it or filter rcs in the caller.
- Selftest
verdict()classifies any non-zero exit asrefuse— reproduced: an unbound$REPO_FULLin the refusal message still yields10 passed. Assert the specific refusal.
Design questions for the callers PR, so they're written down now:
- On
clientthe train'sgh release createunder a PAT raises both the tag push and the release event. If client's caller keepsrelease: publishedalongsidepush: tags, two runs race onchore/bump-$NEXTwith noconcurrency:block and an idempotency check that reads before either has pushed. Is the plan one trigger per caller, or a job-levelconcurrencykeyed on the tag withcancel-in-progress: false? - Given org-config is the host the fleet resolves to, should this reusable and its selftest move there now rather than land here and be ported later?
What holds up: the measurement in the header and body is right (five of six repos raise only the tag), and the mutation proof reproduces exactly — neutering the rc arm reddens the four rc assertions and nothing else.
| # BOTH EVENTS, because the release path stays supported for `client`, | ||
| # which is the one repo whose release event does fire. On a tag push | ||
| # `github.event.release` is absent and `github.ref_name` is the tag. | ||
| TAG: ${{ github.event.release.tag_name || github.ref_name }} |
There was a problem hiding this comment.
This fix lands in .github's copy, but repo-inventory.yml in tracebloc/org-config declares source_repo: org-config, the fleet's callers already resolve there (e.g. set-pr-status.yml in backend and design-system), and org-config's post-release-bump.yml is unchanged on both main and develop: still TAG: ${{ github.event.release.tag_name }}, no rc arm. Nothing syncs the two, and .github stays a valid host only while org-config's transition_sources lists it. So a caller written against org-config gets an empty TAG on a tag push and refuses every real release. Should this land in org-config instead (with the selftest), with .github's copy either mirrored or retired?
| fi | ||
|
|
||
| # AN rc CONSUMES NOTHING (backend#3681). On the release event this was the | ||
| # job-level `if: github.event.release.prerelease != true`; a tag push has |
There was a problem hiding this comment.
The comment says the prerelease test moved into the script, but the job-level if: github.event.release.prerelease != true on line 98 is still there. On a tag push it's null != true (always true), so it only does anything on client's release path, where it duplicates this rc case. One rule or the other — I'd delete line 98 and let the case be the single decision, which is also what the selftest can see.
|
|
||
| RELEASED="${TAG#v}" | ||
| case "$RELEASED" in | ||
| [0-9]*.[0-9]*.[0-9]*) ;; |
There was a problem hiding this comment.
Probed the two extracted case blocks: v1.2.3-beta.1, v1.2.3-hotfix, v1.2.3.4, v1.2.3-RC.1, v1.2.3rc1 all pass both and reach version_file.py cmp, which exits 1 with "'1.2.3-beta.1' is not a plain X.Y.Z" — red, but blaming the version file rather than the tag, so line 199's "Bump by hand" message is never the one shown. cli's release.yml and the train's promote script both define stable as exactly ^v[0-9]+\.[0-9]+\.[0-9]+$ and anything else as prerelease. Could the shape test use that anchored regex (*-* → prerelease no-op, anything else non-matching → refuse) instead of the glob, and the selftest gain a v1.2.3-beta.1 case?
| # since `*` matches the suffix too. The prerelease field was doing this | ||
| # work, so removing the field without adding this check would have bumped | ||
| # the version on every rc tag. | ||
| case "$TAG" in |
There was a problem hiding this comment.
The rc no-op is the most frequent outcome (once per staging hop per repo) and the cheapest to decide, but it runs after the App-token mint, both checkouts and the PAT guard. A caller missing SYNC_PR_AUTHOR_TOKEN will therefore go red on every rc, not just on the release that needs a PR. Two options: hoist this case to the top of the step (it needs only $TAG), or filter in the caller with tags: ['v*', '!v*-rc*'] so the reusable never runs for an rc. Either keeps the selftest extraction intact.
| release at all. Five callers would have been dead workflows, and the three with no | ||
| release event are the repos the reusable was written for. The trigger now accepts a | ||
| tag push, which is the one signal all six produce; the callers are the next PR and | ||
| this row still goes when they land. Deliberately cites NO ticket number: the ticket this reusable was filed under |
There was a problem hiding this comment.
This sentence now sits nine lines after UPDATED backend#3681: — the row cites a ticket and then says it cites none. Not cosmetic: reason-citations parses every #NNNN in shared_reasons and treats a CLOSED issue as a finding, and it runs as a step inside the required selftests job. #3681 is open today, and the thing that closes it is the callers PR this row says will also delete it — so the day the callers land, every PR here goes red until this row is gone. Simplest: drop the inventory hunk from this PR entirely (it's also what arms the red gate), and let the row's deletion ride with the callers.
| eval "$shape_case" | ||
| echo "__ACCEPTED__" | ||
| ) ; rc=$? | ||
| if [ $rc -ne 0 ]; then echo refuse |
There was a problem hiding this comment.
Reproduced: adding $REPO_FULL to the refusal echo on workflow line 199 prints REPO_FULL: unbound variable three times and the suite still says 10 passed, 0 failed — the unbound-var abort is non-zero, so verdict() calls it refuse. This is the same false-green the header says was fixed for VERSION_FILE; setting one variable closed one instance, not the class. Two cheap hardenings: (a) classify refuse only when the output contains ::error::release tag, otherwise report broken; (b) set every variable the bodies interpolate (REPO_FULL too) and assert the extracted blocks have no unbound references. Assert the specific failure, not "non-zero".
Refs backend#3681. Does not close it — the callers are the next PR.
post-release-bump.ymlwas driven by thereleaseevent. Measured 2026-09-11 across the six repos that declare aversion_fileinrelease-train/repos.yml, that event reaches exactly one of them:on: releasefires?gh release createunder a PAT (gh_release: true)release.yml,softprops/action-gh-release, defaultGITHUB_TOKENGITHUB_TOKENeventsrelease-image.yml,gh release createundersecrets.GITHUB_TOKENpromote-repo.shPOSTs a bare tag refSo the caller rollout this reusable was parked for would have installed five workflows that can never run. The three raising no release event at all are exactly the repos it was written for —
design-systemafterv1.3.0andv1.4.0, anddesign-system-v2atv0.1.0, all cited in its own header.Callers now trigger on
push: tags: ['v*']. The tag is the one signal all six produce, since the train always creates it on the way to publishing. The release path still works, soclientis unaffected.The sharp edge: the prerelease test had to move
It was a job-level
if:ongithub.event.release.prerelease. A tag push has no such field, and the caller'sv*glob matches every rc the train cuts — once per hop per repo.The shape test already in the script does not catch them. It's a glob —
[0-9]*.[0-9]*.[0-9]*— and1.2.3-rc.1satisfies it, because*matches the suffix too. Dropping the field without adding an explicit rc test would not have failed loudly; it would have bumped the version on every rc tag. An rc is now a green no-op, checked before the shape test.Why this lands with a selftest
That trap is not something a reviewer should have to catch by eye.
scripts/tests/post-release-bump-selftest.shextracts bothcaseblocks out of the YAML by anchor and runs them, so the workflow can't drift from what the suite checks — a selftest carrying its own copy of the logic is the inert-verification shape backend#1729 catalogued.Mutation-proven. Neutering the rc arm turns exactly the four rc assertions red and leaves accept/refuse green:
The suite also caught a false green in its own first draft: the three
refusecases were passing on anunbound variableabort rather than on the shape test, and would have kept passing with that test deleted. Noted in the file where the fix is.Evidence
make selftests-coverpasses with the new target wired (32 selftests, 18 mutation runners).make checkis green exceptselftest-pipefail-early-close, which fails identically on unmodifieddevelophere becausegawkisn't installed on this machine —167 passed, 1 failedboth with and without this change. CI has gawk.Inventory
repo-inventory.yml'spost_release_bump_parkedrow is updated, not deleted — the callers are still pending so the exemption still holds, but it now records why the rollout didn't simply happen. The row still goes when the callers land, as its own text demands.Note
Medium Risk
Changes when post-release version bumps run and adds tag-string RC detection; a mistake could skip real releases or bump on every RC tag, though the new selftest pins the decision logic.
Overview
Fixes
post-release-bumpso it can run when callers usepush: tags: ['v*'], because thereleaseevent only fires for one of six version-file repos; the rest only get tags.The bump step now resolves
TAGfromgithub.event.release.tag_nameorgithub.ref_name, so tag pushes and the existingclientrelease path both work. Prerelease handling moves into the shell: RC tags (*-rc.*,*-rc[0-9]*) exit as a green no-op before the version-shape glob, which would otherwise treat1.2.3-rc.1as a normal release.Adds
post-release-bump-selftest.sh, wired viamake selftest-post-release-bump, which pulls the twocaseblocks from the workflow YAML so tag accept/skip/refuse logic cannot drift.repo-inventory.ymlupdates the parkedpost-release-bumpexemption text for backend#3681; caller rollout is still a follow-up PR.Reviewed by Cursor Bugbot for commit f53a2fd. Bugbot is set up for automated code reviews on this repo. Configure here.