Skip to content

Add release-tag consistency guards to CI and release workflows#250

Merged
pengfei-threemoonslab merged 2 commits into
mainfrom
release-tag-guard
Jul 2, 2026
Merged

Add release-tag consistency guards to CI and release workflows#250
pengfei-threemoonslab merged 2 commits into
mainfrom
release-tag-guard

Conversation

@pengfei-threemoonslab

Copy link
Copy Markdown
Contributor

Why

Main claimed v0.14.0 as the latest release for a day before the tag existed: the README @v0.14.0 Action 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.py already keeps every public version reference in lockstep with pyproject.toml — but nothing checked the lockstep claim against release reality.

What

  • ci.yml — new release-tag-consistency job. On pushes to main only (never on pull_request, so a version-bump PR is never blocked): asserts v{pyproject version} exists as a tag on origin via git ls-remote --exit-code. A merged bump without a pushed tag turns main red with the exact git tag && git push command to finish the release.
  • release.yml — fail-fast tag/version assertion before the test/build/publish pipeline: the pushed tag must equal v{pyproject version}. PyPI uploads are immutable, so this is the last cheap moment to catch a mistagged release (e.g. a v0.15.0 tag 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 the VERSION_LITERAL_TARGETS pin in test_public_surface_contract.py still matches.

Verification

  • Guard shell logic exercised locally against real repo state: extraction from pyproject.toml0.14.0; git ls-remote finds refs/tags/v0.14.0 (passes); simulated v0.15.0 tag correctly rejected by the release assertion.
  • pytest tests/test_public_surface_contract.py tests/test_action_metadata.py — 204 passed.
  • Both workflow files validate as YAML.
  • shipgate check --agent claude-code --format codex-boundary-json on 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 in changed_files — consistent with the policy watching the shipgate gate workflow specifically, but worth a look for ci.yml/release.yml coverage.

🤖 Generated with Claude Code

pengfei-threemoonslab and others added 2 commits July 1, 2026 23:37
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>
@pengfei-threemoonslab

Copy link
Copy Markdown
Contributor Author

Addressed the P2 in b5e5c7d: the failure text now names the working retest path — pushing the tag triggers release.yml, not a retest of the failed run, so the recovery is "push the tag, then Re-run failed jobs" (the job re-checks origin's live tag list on re-run; it also self-heals on the next push to main). The job comment explains why a tags: trigger on this workflow was rejected: it would start a new run on the tag ref, leave the failed branch run red on the CI badge, and duplicate the full test job that release.yml already runs on every tag.

On the open question — yes, tag-only is intentional, and it's now stated in the job comment. The tag is what triggers release.yml, the canonical publisher, which fails loudly if the PyPI upload or the GitHub Release step breaks; so "tag exists + release run green" transitively covers /releases/latest and PyPI. Querying PyPI from this job would add an external dependency that lags publish by minutes (observed during the v0.14.0 cut: the /json index served 0.13.0 for a while after the version endpoint had 0.14.0) and could false-fail immediately after a legitimate release.

🤖 Generated with Claude Code

@pengfei-threemoonslab pengfei-threemoonslab merged commit 3fe7bd2 into main Jul 2, 2026
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.

1 participant