Repository navigation
Refuse to publish anything but a tagged release to PyPI - #560
Merged
Merged
Conversation
publish.yml built and uploaded whatever ref it was dispatched from, and the GitHub UI defaults 'Use workflow from' to main. While cutting 0.10.0 it was dispatched from main and uploaded 0.10.1.dev0 to PyPI in place of the release. PyPI never accepts a version twice, so the bad upload can only be yanked. A new first step in the build job runs scripts/check_release_ref.py, which fails the run when publishing to pypi unless it was dispatched from a tag, that tag is exactly v<__version__>, and the version is a final X.Y.Z with no .dev, pre-release, post-release or local segment. The version is read from src/canarchy/__init__.py, the same file hatch builds from. Both publish jobs need build, so a refused run uploads nothing. testpypi is unrestricted, since the runbook uses it to rehearse workflow changes. The input reaches the script through env rather than being interpolated into the shell line. Logic lives in a stdlib-only script so it is unit-tested, including the exact main/0.10.1.dev0 dispatch that caused this; a workflow test fails if the step is removed or moved after the build. fixes #559 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195Fkk63xics9zBvjFgYz8T
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.
Summary
publish.ymlbuilt and uploaded whatever ref it was dispatched from. The GitHub UI's "Use workflow from" control defaults tomain, so while cutting 0.10.0 the workflow was dispatched frommainand uploaded0.10.1.dev0to production PyPI in place of the release (run 36806107125). PyPI never accepts a version twice, so that upload can only be yanked.This makes the workflow refuse any PyPI publish that isn't from the matching release tag, before anything is built.
Closes #559
Changes
scripts/check_release_ref.py, a new stdlib-only guard. When the target ispypiit fails unless all three hold:v<__version__>X.Y.Z, with no.dev,rc/a/b,.post, or+localsegmentThe version is read from
src/canarchy/__init__.py, which is the same file hatch builds the package version from. Failures print as::error::annotations that name the problem and the fix.publish.yml: the guard is a new step inbuild, ahead ofuv build. Both publish jobsneed: build, so a refused run uploads nothing.testpypiis unrestricted. The runbook uses it to rehearse workflow changes, which may need to run from a branch.The workflow input reaches the script through
env, not${{ }}interpolation into the shell line. It's achoiceinput today, but this way it stays safe if that changes.What it says on the dispatch that caused this
Test plan
uv run pytest -q: 1833 passed (18 new), 1 skipped, 1 deselected, 306 subtestsuv run ruff check/ruff format --check: cleanpublish.ymlparses as YAMLtests/test_release_ref.pycovers:branch/main/0.10.1.dev0) → refused with both reasons0.10.0tag → each refused.dev,rc,a,b,.postand+localversions → refused even from a matching tagtestpypifrommain→ allowedread_versionagrees withcanarchy.__version__build. Removing the step frompublish.ymlmakes this fail ('Verify release ref' is not in list), so the protection can't be dropped silently.Documentation
CHANGELOG.md:[Unreleased] / Fixedentrydocs/release.md: step 8 now says to publish from the tag, "How to use it" says to pick the tag under Use workflow from > Tags and gives thegh workflow run --refequivalent, "Workflow behavior" documents the enforced rule, and a note records whydocs/design//docs/tests/: none cover publishing, and AGENTS.md reserves new specs for command-surface, data-model or module-boundary changes. A CI guard is none of those.AGENTS.md/docs/agents.md: no agent-facing surface changedLimitation
GitHub runs the workflow file from the dispatched ref, so the guard applies to refs that contain it:
mainafter this merges, and every future release tag. Dispatching from an older ref, such as thev0.10.0tag, runs the old unguarded workflow. That's fine for publishing 0.10.0 itself, and every earlier tag is already on PyPI.Safety
No active-bus behaviour. This adds a refusal path to the release workflow and changes nothing else about a correctly dispatched publish.
🤖 Generated with Claude Code
https://claude.ai/code/session_0195Fkk63xics9zBvjFgYz8T
Generated by Claude Code