Skip to content

fix(post-release-bump): trigger on the tag, which is the only signal 5 of its 6 repos raise (backend#3681) - #474

Open
saadqbal wants to merge 1 commit into
developfrom
fix/3681-post-release-bump-tag-trigger
Open

fix(post-release-bump): trigger on the tag, which is the only signal 5 of its 6 repos raise (backend#3681)#474
saadqbal wants to merge 1 commit into
developfrom
fix/3681-post-release-bump-tag-trigger

Conversation

@saadqbal

@saadqbal saadqbal commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Refs backend#3681. Does not close it — the callers are the next PR.

post-release-bump.yml was driven by the release event. 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:

repo how a release appears on: release fires?
client train runs gh release create under a PAT (gh_release: true) yes
cli own release.yml, softprops/action-gh-release, default GITHUB_TOKEN no — GitHub does not start workflow runs from GITHUB_TOKEN events
data-ingestors own release-image.yml, gh release create under secrets.GITHUB_TOKEN no — same
tracebloc-py-package none — promote-repo.sh POSTs a bare tag ref no
design-system none — same no
design-system-v2 none — same no

So 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-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, since the train always creates it on the way to publishing. The release path still works, so client is unaffected.

The sharp edge: the prerelease test had to move

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's 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.

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.sh extracts both case blocks 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:

  ok    v1.2.3 is accepted
  FAIL  v1.2.3-rc.1 is skipped, not bumped
     expected skip, got accept (tag: v1.2.3-rc.1)
  FAIL  v1.9.115-rc.1 is skipped (client's real rc shape)
  ...
  ok    a non-version tag is refused
post-release-bump: 6 passed, 4 failed

The suite also caught 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.

Evidence

make selftests-cover passes with the new target wired (32 selftests, 18 mutation runners). make check is green except selftest-pipefail-early-close, which fails identically on unmodified develop here because gawk isn't installed on this machine167 passed, 1 failed both with and without this change. CI has gawk.

Inventory

repo-inventory.yml's post_release_bump_parked row 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-bump so it can run when callers use push: tags: ['v*'], because the release event only fires for one of six version-file repos; the rest only get tags.

The bump step now resolves TAG from github.event.release.tag_name or github.ref_name, so tag pushes and the existing client release 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 treat 1.2.3-rc.1 as a normal release.

Adds post-release-bump-selftest.sh, wired via make selftest-post-release-bump, which pulls the two case blocks from the workflow YAML so tag accept/skip/refuse logic cannot drift. repo-inventory.yml updates the parked post-release-bump exemption 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.

…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>
@saadqbal saadqbal self-assigned this Sep 11, 2026
@saadqbal

Copy link
Copy Markdown
Collaborator Author

CI: audit fails and gate fails because it waits on audit — so there is one failure, and it is not this PR's.

The audit reported 121 findings including backend: MISSING required caller for advance-deploy-env.yml on develop. That caller exists:

$ gh api "repos/tracebloc/backend/contents/.github/workflows?ref=develop" --jq '...'
advance-deploy-env.yml, bugbot-gate-caller.yml, code-quality-caller.yml

Same for client-runtime. The audit is rendering an unreadable/partial caller read as MISSING rather than UNKNOWN — filed as backend#3690, which also blocks every other .github contract PR until it is fixed.

This PR's diff cannot cause it: repo-inventory.yml changes only the prose inside the post_release_bump_parked folded scalar, and parsing both trees and diffing them shows the structure is identical to develop apart from that one string. The audit was green on develop at 2026-09-10 11:47 and first failed at 2026-09-11 10:36 on this branch.

The run did surface three findings that are genuine and unrelated to me — model-zoo-archive, quickstart and start-training-archive are active in the org and absent from the inventory. Those are captured in #3690 too rather than folded in here.

Everything else on this PR is green: both Analyze jobs, gate / gate, and all mutation targets.

@saadqbal

Copy link
Copy Markdown
Collaborator Author

The two red checks here aren't this PR's. audit fails on the caller-inventory sweep and gate only fails as a cascade of it — that's backend#3690.

Re-ran audit to separate transient from standing: it went 2 unreadable repos → 1, so the enumeration is partly flaky, but the residual one is real. It's model-zoo, which has been renamed to model-zoo-archive and made private (gh api repos/tracebloc/model-zootracebloc/model-zoo-archive; not in the org listing; repo-inventory.yml:1351 still declares model-zoo). Presumably part of backend#3582.

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 backend#3690.

@saadqbal

Copy link
Copy Markdown
Collaborator Author

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.

gate is red as a consequence — it polls for the audit's conclusion and its budget expired while the audit was re-running, so its verdict is downstream of the same thing.

This PR stays blocked until #3690 is fixed. Everything else on it is green (37 checks).

@saadqbal

Copy link
Copy Markdown
Collaborator Author

The two red checks are not caused by this PRgate only fails because audit did, and audit is failing closed on genuine fleet drift that landed today. Diagnosis, so nobody edits this branch trying to make it green:

caller-drift reports "2 declared repo(s) missing from the org listing". They are model-zoo and start-training, and both were renamed and privatised today at 09:25Z:

declared in repo-inventory.yml actual
tracebloc/model-zoo tracebloc/model-zoo-archiveprivate, not archived
tracebloc/start-training tracebloc/start-training-archiveprivate, archived

GET /repos/tracebloc/model-zoo still resolves because GitHub follows the rename redirect, but the org listing returns only the new names — so the inventory's declared names are genuinely absent, and the audit is right to refuse an all-clear. Every caller-drift run before today was green; the first failure is this branch at 10:36Z, ~1h after the renames.

This PR's only repo-inventory.yml change is prose — it extends a shared_reasons explanation and declares no repos — so it cannot have caused this. I re-ran the audit in case it was a transient org-enumeration blip; it failed identically, which confirms real drift.

What actually unblocks it: reconcile repo-inventory.yml with the renames (drop the two rows, or re-point them at the -archive names and mark them retired) — a separate change from this PR's purpose. Once audit is green, gate needs a re-run too: it polls caller-drift's conclusion per head SHA, so it stays red until re-run even after the audit passes.

Also worth flagging separately: the org's CLAUDE.md lists model-zoo and start-training among the public repos. Both are private as of today, so that line is now false and the public/private filing rule keys off it.

@saadqbal

Copy link
Copy Markdown
Collaborator Author

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 (model-zoomodel-zoo-archive + new public model-zoo) and #3595 (start-trainingstart-training-archive + new public quickstart). The archive halves landed today at 09:25Z; the replacement repos have not been created yet, so the org listing is genuinely missing both declared names and the audit is correctly refusing an all-clear on a mid-flight migration.

So my earlier "re-point the rows at the -archive names or drop them" was the wrong remedy. The right ones are either landing the replacement repos, or updating repo-inventory.yml as part of the epic — noting start-training becomes quickstart, a rename rather than a removal.

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 audit gates gate for all of them. Flagged on the epic (backend#3582) for @LukasWodka, who owns it.

@LukasWodka LukasWodka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 declared source_repo, its post-release-bump.yml is unchanged on main and develop (still TAG: ${{ github.event.release.tag_name }}, no rc arm), and nothing syncs the two — so a caller written against the declared host gets an empty TAG on every tag push. Thread on line 137.
  • repo-inventory.yml now cites backend#3681 and then says it cites no ticket; reason-citations runs inside the required selftests job 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 — and version_file.py cmp is what actually refuses them, with a message that blames the version file. cli's release.yml and 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 as refuse — reproduced: an unbound $REPO_FULL in the refusal message still yields 10 passed. Assert the specific refusal.

Design questions for the callers PR, so they're written down now:

  • On client the train's gh release create under a PAT raises both the tag push and the release event. If client's caller keeps release: published alongside push: tags, two runs race on chore/bump-$NEXT with no concurrency: block and an idempotency check that reads before either has pushed. Is the plan one trigger per caller, or a job-level concurrency keyed on the tag with cancel-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 }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]*) ;;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread repo-inventory.yml
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants