-
Notifications
You must be signed in to change notification settings - Fork 0
fix(ci): re-run hygiene check on every bump-version push retry (#678) #713
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
6bab170
test: reproduce #678 — hygiene not re-run on bump-version push retry
logbie e60b8c9
fix(ci): re-run hygiene check on every bump-version push retry (#678)
logbie 1d37016
docs: dev diary for #678 bump-version hygiene-on-retry fix
logbie 7b9f062
fix(ci): skip the pointless re-bump after the final push rejection (#…
logbie 6c33cb3
Merge branch 'main' into fix/678-bump-version-hygiene-on-retry
logbie 590611f
Merge branch 'main' into fix/678-bump-version-hygiene-on-retry
logbie File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
101 changes: 101 additions & 0 deletions
101
History/dev-diary/2026/2026-08-14-issue-678-bump-version-hygiene-on-retry.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| # 2026-08-14 — `bump-version` pushed to `main` unchecked on any push retry (#678) | ||
|
|
||
| ## Symptom | ||
|
|
||
| None observed in production — and that is the point. The gap needs a genuine | ||
| race to surface: another commit must land on `main` between the `bump-version` | ||
| job's checkout and its push. Rare enough to go unnoticed, and the place it | ||
| surfaces is the branch the check exists to protect. | ||
|
|
||
| Split out of the #674 review, where CodeRabbit raised it. Pre-existing; #674 did | ||
| not touch it. | ||
|
|
||
| ## Root cause | ||
|
|
||
| `.github/workflows/ci.yml` ran the hygiene gate exactly once, as its own step, | ||
| with a comment stating the requirement plainly: | ||
|
|
||
| ```yaml | ||
| # This job writes directly to main, so it must prove hygiene and version | ||
| # agreement itself, immediately before pushing (REPOSITORY_HYGIENE.md §8). | ||
| - name: Static hygiene and version check before push | ||
| run: python scripts/check_repo_hygiene.py --mode static | ||
| ``` | ||
|
|
||
| The `Push changes` step then retried up to five times, and each retry rebuilt | ||
| the commit from scratch: | ||
|
|
||
| ```bash | ||
| git fetch origin "$BRANCH" | ||
| git reset --hard "origin/$BRANCH" | ||
| python scripts/bump_version.py --update-all | ||
| ``` | ||
|
|
||
| That is a **new commit on a new tree** — the concurrent merge's content plus a | ||
| fresh bump — and it went out without the gate running again. The step's own | ||
| comment was true only of the first attempt. A concurrent merge carrying a | ||
| hygiene violation, or a bump that drifted against the new tree, reached `main` | ||
| unchecked and was caught only by the *next* run, after it had landed. | ||
|
|
||
| ## The fix | ||
|
|
||
| The retry loop moved to `scripts/push_version_bump.sh`, which re-runs the | ||
| hygiene check on the re-bumped tree before each subsequent push. A violation | ||
| **aborts**: | ||
|
|
||
| ```bash | ||
| if ! $HYGIENE_CMD; then | ||
| echo "::error::Static hygiene check failed on the re-bumped tree (attempt $attempt); refusing to push." | ||
| exit 1 | ||
| fi | ||
| ``` | ||
|
|
||
| Aborting, not skipping to the next attempt — retrying past a violation would | ||
| defeat the gate. Aborting leaves `main` un-bumped, which the next push to `main` | ||
| recovers. The non-retry path is unchanged: still exactly one check, in the | ||
| workflow step, before the first attempt. | ||
|
|
||
| ## Why it moved out of the YAML | ||
|
|
||
| Inline workflow shell cannot be tested — it only ever executes in production, on | ||
| pushes to `main`, on the rare retry path. That is the same hazard | ||
| `scripts/test_publish_spaces.sh` exists to close for the release scripts, and it | ||
| gets the same treatment here. | ||
|
|
||
| `scripts/test_push_version_bump.sh` runs the real script against a **real git | ||
| repository and a real bare remote**, with genuine non-fast-forward rejections | ||
| produced by a second clone committing to the remote mid-flight. Only the two | ||
| shelled-out commands are stubbed — `BUMP_CMD` and `HYGIENE_CMD`, as recording | ||
| fakes that log the HEAD each one saw — because the production versions mutate | ||
| the working repo and run Cargo. `git push` is never stubbed; the assertions are | ||
| made against where the remote tip actually moved. | ||
|
|
||
| ## Risk class | ||
|
|
||
| **R3** — release controls (root `testing.md` §5), which requires negative and | ||
| failure-path coverage. The failure-path case asserts the *absence* of the write: | ||
| when hygiene fails on a retry, the script exits non-zero **and** the remote tip | ||
| is byte-identical to where it started. | ||
|
|
||
| ## Red evidence | ||
|
|
||
| Against the faithful extraction of the buggy loop, 7 of 19 assertions failed. | ||
| The one that matters: | ||
|
|
||
| ```text | ||
| FAIL nothing was pushed - the remote tip is exactly where it was | ||
| expected: 0e721debb32938a8cf5da424cc1b7bb75d3f0f02 | ||
| actual: b169625d9ecf277cb4acd3d316750132847f2a32 | ||
| ``` | ||
|
|
||
| The old loop pushed `b169625` — the re-bumped tree carrying the concurrent | ||
| merge — to the remote despite hygiene rejecting it. That is the defect, observed | ||
| rather than argued. | ||
|
|
||
| Red: `6bab170`. Green: `e60b8c9`. 19/19 after the fix. | ||
|
|
||
| ## Coverage added | ||
|
|
||
| `Release Script Tests` now runs `scripts/test_push_version_bump.sh` alongside | ||
| `scripts/test_publish_spaces.sh`, so the retry path is exercised on every PR | ||
| rather than only during a real race on `main`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| #!/usr/bin/env bash | ||
| # Push the post-merge version-bump commit to a branch, re-bumping on rejection. | ||
| # | ||
| # Extracted from the `bump-version` job in .github/workflows/ci.yml so the retry | ||
| # behaviour can be tested (inline workflow shell cannot be). | ||
| # | ||
| # Usage: scripts/push_version_bump.sh <branch> | ||
| # | ||
| # The two commands this shells out to are overridable so the tests can record | ||
| # and control them; the defaults are what CI runs: | ||
| # BUMP_CMD - re-derives the next version from the fresh tip and commits it | ||
| # HYGIENE_CMD - proves the tree about to be pushed is hygienic | ||
| set -eu | ||
|
|
||
| BRANCH="${1:-${GITHUB_REF_NAME:-}}" | ||
| [ -n "$BRANCH" ] || { echo "usage: $0 <branch>" >&2; exit 64; } | ||
|
|
||
| BUMP_CMD="${BUMP_CMD:-python scripts/bump_version.py --update-all}" | ||
| HYGIENE_CMD="${HYGIENE_CMD:-python scripts/check_repo_hygiene.py --mode static}" | ||
|
|
||
| # The bump commit must fast-forward main. When another commit lands on main | ||
| # during this run (concurrent merges), the first push is rejected as | ||
| # non-fast-forward. Re-base the bump onto the new tip and retry. Because two | ||
| # concurrent runs can compute the same next version, we reset to the fresh tip | ||
| # and re-run the bump so the version is derived from the true current version | ||
| # instead of replaying a stale bump. | ||
| ATTEMPTS=5 | ||
| for attempt in 1 2 3 4 5; do | ||
| if git push origin HEAD:"$BRANCH"; then | ||
| echo "Pushed version bump on attempt $attempt" | ||
| exit 0 | ||
| fi | ||
| # After the final rejection there is no further push, so re-bumping would | ||
| # build a commit nothing will ever send - and the real bump shells out to | ||
| # Cargo, so that is not free. Give up here instead. | ||
| if [ "$attempt" -eq "$ATTEMPTS" ]; then | ||
| break | ||
| fi | ||
| echo "Push rejected on attempt $attempt (main advanced); re-bumping from latest tip..." | ||
| git fetch origin "$BRANCH" | ||
| git reset --hard "origin/$BRANCH" | ||
| # shellcheck disable=SC2086 # intentional word splitting: command + args | ||
| $BUMP_CMD | ||
| # The re-bump produced a new commit on a new tree - whatever landed on | ||
| # `$BRANCH` concurrently, plus a fresh bump. The check the job ran before the | ||
| # first attempt says nothing about that tree, so hygiene and version agreement | ||
| # must be re-proven here, immediately before this push | ||
| # (REPOSITORY_HYGIENE.md §8). | ||
| # shellcheck disable=SC2086 # intentional word splitting: command + args | ||
| if ! $HYGIENE_CMD; then | ||
| # Abort rather than retry: retrying past a violation would defeat the gate. | ||
| # This leaves the branch un-bumped, which the next push to it recovers. | ||
| echo "::error::Static hygiene check failed on the re-bumped tree (attempt $attempt); refusing to push." | ||
| exit 1 | ||
| fi | ||
| done | ||
| echo "::error::Failed to push version bump after 5 attempts (main kept advancing)." | ||
| exit 1 |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔍 Sibling versioning.yml push path still has no retry or re-check
.github/workflows/versioning.yml:44performs a plaingit push origin HEAD:${{ github.ref_name }}with a single preceding hygiene check and no retry loop. The extraction here does not change that workflow, so the two version-bump paths now differ in retry semantics. Worth confirming that is deliberate (versioning.yml presumably runs manually/scheduled where a concurrent-merge race is less likely) rather than an oversight of this refactor.(Refers to lines 704-715)
Was this helpful? React with 👍 or 👎 to provide feedback.