Add release-tag consistency guards to CI and release workflows#250
Conversation
Close the gap that let main claim v0.14.0 as the latest release for a
day before the tag existed (README's @v0.14.0 Action pin 404'd for
anyone copying it; ROADMAP said "Latest release: v0.14.0" while PyPI
was at 0.13.0).
Public surfaces already move in lockstep with pyproject.toml
(tests/test_public_surface_contract.py), but nothing checked the
lockstep claim against release reality. Two guards:
- ci.yml release-tag-consistency job (pushes to main only, never PRs):
the tag v{pyproject version} must exist on origin, so a merged
version bump without a pushed tag turns main red with the exact
command to cut the release.
- release.yml fail-fast step: the pushed tag must equal
v{pyproject version} before the test/build/publish pipeline runs —
PyPI uploads are immutable, so this is the last cheap moment to
catch a mistagged release.
ROADMAP's latest-release line now links to the GitHub /releases/latest
page (no version in the URL, so nothing new to bump) and names the CI
job that enforces it. Phrasing is unchanged so the existing
VERSION_LITERAL_TARGETS pin in test_public_surface_contract.py still
matches.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review finding on #250: the failure text told maintainers to push the missing tag, but a tag push triggers release.yml — it does not retest the failed main run, which stays red until re-run or the next main push. Name the working recovery explicitly ("Re-run failed jobs" re-checks origin's live tag list) in both the error output and the job comment, and explain why a tag trigger on this workflow would not help (new run on the tag ref, badge stays red, duplicates the full test job release.yml already runs on tags). Also make the guard's tag-only scope explicit: the tag triggers release.yml, the canonical publisher, which fails loudly if the PyPI upload or GitHub Release step breaks — so tag-exists plus a green release run covers /releases/latest and PyPI. Querying PyPI here would add an external dependency that lags publish by minutes and can false-fail right after a legitimate release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Addressed the P2 in b5e5c7d: the failure text now names the working retest path — pushing the tag triggers On the open question — yes, tag-only is intentional, and it's now stated in the job comment. The tag is what triggers 🤖 Generated with Claude Code |
Why
Main claimed
v0.14.0as the latest release for a day before the tag existed: the README@v0.14.0Action pin 404'd for anyone copying it, and ROADMAP said "Latest release:v0.14.0" while PyPI was at 0.13.0 (surfaced in the 2026-07-01 repo review; the release has now been cut).tests/test_public_surface_contract.pyalready keeps every public version reference in lockstep withpyproject.toml— but nothing checked the lockstep claim against release reality.What
ci.yml— newrelease-tag-consistencyjob. On pushes tomainonly (never onpull_request, so a version-bump PR is never blocked): assertsv{pyproject version}exists as a tag on origin viagit ls-remote --exit-code. A merged bump without a pushed tag turns main red with the exactgit tag && git pushcommand to finish the release.release.yml— fail-fast tag/version assertion before the test/build/publish pipeline: the pushed tag must equalv{pyproject version}. PyPI uploads are immutable, so this is the last cheap moment to catch a mistagged release (e.g. av0.15.0tag on a 0.14.0 tree).ROADMAP.md— the latest-release line now links to/releases/latest(no version in the URL, nothing new to bump) and names the CI job that enforces it. Phrasing is unchanged, so theVERSION_LITERAL_TARGETSpin intest_public_surface_contract.pystill matches.Verification
pyproject.toml→0.14.0;git ls-remotefindsrefs/tags/v0.14.0(passes); simulatedv0.15.0tag correctly rejected by the release assertion.pytest tests/test_public_surface_contract.py tests/test_action_metadata.py— 204 passed.shipgate check --agent claude-code --format codex-boundary-jsonon this diff:decision: allow,completion_allowed: true(CI is strengthened, not weakened). Note in passing: the boundary policy reported "No Codex boundary changes require action" despite two workflow files inchanged_files— consistent with the policy watching the shipgate gate workflow specifically, but worth a look forci.yml/release.ymlcoverage.🤖 Generated with Claude Code