fix: fail the release when the computed version already exists on the registry - #897
Merged
Merged
Conversation
Contributor
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
… registry PyPI never allows reusing a version number. If this repo's git history is ever rewritten away from what was published, python-semantic-release can later compute an already-published version, and that failed in one of two bad ways: PSR sees a matching git tag, reports the version "already released", sets released=false, and every downstream publish job is silently skipped (nobody notices); or no tag exists, and `uv publish` dies with an opaque 409 deep inside the pipeline, after PSR has already created a git tag and GitHub Release. Adds a dry-run PSR step (no_operation_mode: true, same pinned action/SHA and same prerelease inputs as the real release step) purely to learn the version PSR would publish, followed by a guard step gated on steps.dryrun.outputs.released == 'true' that queries PyPI for that exact version and classifies the result: HTTP 200 is a collision (fail loudly, explain the recovery), 404 is free (proceed), and anything else (network error, rate limit, outage) is unknown and is deliberately treated as fatal rather than as "free" -- a transient error read as "free" would let a real collision slip through, which is worse than the bug this guard prevents. Both fire before any tag or Release exists, converting the two silent/opaque failure modes above into one explicit, early, actionable error.
n24q02m
force-pushed
the
fix/cd-guard-version-already-published
branch
from
July 10, 2026 02:29
46b7f94 to
30897e0
Compare
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Problem
PyPI never allows reusing a version number. If this repo's git history is ever rewritten away from what was published, python-semantic-release (PSR) can later compute an already-published version. Two bad outcomes follow:
released=false, and the downstreampublish-pypijob is silently skipped. Nobody notices.uv publishdies with an opaque 409 deep inside the pipeline, after PSR has already created a git tag and GitHub Release.Neither is acceptable. This adds a loud, early, actionable failure instead.
Change (
.github/workflows/cd.ymlonly)Placed in the
releasejob, before the real semantic-release step, so it fires before any tag or Release is created:Dry-run step (
id: dryrun) -- the exact same pinned PSR action (350c48fcb3ffcdfd2e0a235206bc2ecea6b69df0) withno_operation_mode: true(--noop) and the sameprerelease/prerelease_tokeninputs as the realid: releasestep. This repo's real step passes noconfig_file, so the dry-run doesn't either -- both compute the version identically against the same untouched checkout. No new action or dependency.Guard step -- gated on
steps.dryrun.outputs.released == 'true'(mirrors the existingneeds.release.outputs.released == 'true'gate onpublish-pypi, so a "no releasable commits" dispatch doesn't false-fail on the current already-published version). Queries PyPI forqwen3-embed==<computed version>(name verbatim frompyproject.toml[project].name) and classifies:curl -o /dev/null -w '%{http_code}'200::error::+exit 1404::error::+exit 1, never treated as freeI confirmed myself that PyPI normalizes the version string in the URL path:
https://pypi.org/pypi/qwen3-embed/1.12.1-beta.1/jsonandhttps://pypi.org/pypi/qwen3-embed/1.12.1b1/jsonboth return200for the same release, so PSR's raw semverversionoutput can go straight into the URL without reformatting.A transient error is deliberately treated as fatal rather than "free" -- reading it as free would let a real collision slip through, which is worse than the bug this guard prevents.
The
releasejob's outputs (released/tag/version/is_prerelease) still come from the realid: releasestep and are unchanged, sopublish-pypi,notify-downstream,create-downstream-issuesconsume them exactly as before.Verification
YAML parses:
Guard's shell classification logic exercised live against the real PyPI registry, using the exact
curlinvocation from the guard, three ways:Commit created with pre-commit hooks running normally (the Python-only hooks -- ruff/ty/pytest -- correctly skip a YAML-only change).
Do not merge.