Skip to content

ci: re-anchor the next -rc.N on tag cut + backward-version guard (#308) - #315

Merged
StefanSteiner merged 9 commits into
tableau:mainfrom
StefanSteiner:fix/release-please-rc-anchor-308
Sep 8, 2026
Merged

ci: re-anchor the next -rc.N on tag cut + backward-version guard (#308)#315
StefanSteiner merged 9 commits into
tableau:mainfrom
StefanSteiner:fix/release-please-rc-anchor-308

Conversation

@StefanSteiner

Copy link
Copy Markdown
Contributor

What & why

release-please miscomputes the next -rc.N after a manual-tag release cut. Under
this repo's gated flow (skip-github-release: true), merging the release PR does
not tag — a maintainer hand-cuts the vX.Y.Z tag + Release afterward. On the
release-PR merge push the new tag doesn't exist yet, so release-please falls
back to scanning full history, where a stale Release-As: 1.0.0-rc.2 footer in an
ancestor commit drags the next version backward to an already-published rc
(#306 proposed 1.0.0-rc.2 after 1.0.0-rc.3 shipped). Publishing a backward
version is unrecoverable — crates.io never frees a version number.

Closes #308.

The fix (defense in depth)

  1. Primary — re-anchor on tag cut. Add release: types: [published] to
    release-please.yml's triggers. gh release create now re-runs release-please
    with the tag present, so it anchors on the just-cut tag, scans only post-tag
    commits (excluding the stale footer), and regenerates the release PR with the
    correct next -rc.N. skip-github-release: true means this re-run creates no
    Release, so there is no self-trigger loop; the constant concurrency: release-please group serializes it against the merge-push run.

  2. Backstop — backward-version guard. New verify-release-pr-version.yml +
    verify-release-pr-version.py (stdlib-only, full semver-2.0 precedence) fail a
    release PR whose proposed manifest version is behind main. The job always runs
    (reports success on non-release PRs, so it is safe to require); the comparison
    is gated to the release-please--branches--* PR. Its 12-case regression suite
    runs on every PR.

  3. Runbook reorder. The manual-tag runbook now promotes the release PR's
    autorelease: tagged label before gh release create, so the
    release: published re-run cannot abort on "untagged, merged release PR
    outstanding."

The fix survives multiple more rc cuts and the eventual 1.0.0 graduation:
1.0.0 sorts above every 1.0.0-rc.N (a forward move), so neither the re-run nor
the guard obstructs graduating.

Docs

docs/GITHUB_OPERATIONS.md, CONTRIBUTING.md, and AGENTS.md are reconciled to
tell one consistent story (trigger, label-before-tag ordering, what the guard
catches vs. doesn't); a new "The backward-version guard" subsection documents the
check; the implementation plan is committed under docs/superpowers/plans/.

⚠️ Maintainer action required for the backstop to be airtight

Mark verify-release-pr-version as a required status check on main
(Settings → Branches). Until then the guard is advisory — a red X a human can
click past. The always-run job design makes it safe to require: it reports
success on every non-release PR and only fails a release PR that regressed the
version.

Verification

CI-and-docs-only change — no Rust / library code touched, so no cargo gate
applies.

…uard

Document the release: published re-run that re-anchors the next -rc.N on
the freshly cut tag, the backward-version guard that backstops it, and the
runbook reorder that promotes the release PR's autorelease: tagged label
before cutting the tag so the re-run cannot abort.

- GITHUB_OPERATIONS.md: workflow table row + trigger; publish-mechanism
  fix (maintainer publishes the Release, not release-please); rc-line
  self-bump now notes the anchoring precondition and re-trigger; new
  'The backward-version guard' subsection; graduation section clarifies
  the guard catches the opposite (backward) failure; branch-protection
  and re-verifying cross-references.
- CONTRIBUTING.md: step 4 notes the same event re-anchors the next rc.
- AGENTS.md: release-please re-runs on release: published to re-anchor.
`python3 -m unittest .github/scripts/test_verify_release_pr_version.py`
fails: the leading dot in `.github` is parsed as a relative-module
reference ("Empty module name"). Point both the docstring and the
GITHUB_OPERATIONS.md reference at `python3 <path>`, which runs from the
repo root.
Final-sweep review found the comparator's 12-case regression suite was
never invoked in CI — only manually or implicitly when a real release PR
fires the guard, i.e. at the exact high-stakes moment the guard exists to
protect. Since the version-forward job already always runs, make checkout
unconditional and add an always-run step executing
test_verify_release_pr_version.py, so a regression to the semver logic is
caught on every PR.

Also document two assumptions the review surfaced: the jq '."."' read
depends on the single-'.'-package (include-component-in-tag: false)
layout, and the PR-only cancel-in-progress expression is intentional
forward-safety, not a dead conditional.
Align Step 1's docstring with the shipped test file: the
`python3 -m unittest <path>` form fails on the dot-leading .github path.
@StefanSteiner
StefanSteiner merged commit 5a12884 into tableau:main Sep 8, 2026
17 checks passed
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.

release-please miscomputes the next rc after a manual-tag cut (#287's footer-free flow left rc.3 unrecognized; #306 regressed to rc.2)

1 participant