Repository navigation
fix: publish only reviewed commits that passed CI - #18
Merged
Merged
Conversation
The pypi environment had no deployment policy, and the dispatch input was checked out as given, so any branch could publish any ref under the library's name. setup_repo.py now makes the environment deployable from the default branch only. Both publish triggers already run with master as their ref, so the code that decides what to publish is always master's own. The tag input stays, as data: it has to be an existing release tag whose commit is on master, it reaches the shell through env, and the publish job checks out the commit check resolved rather than the tag name. Publish used to run as soon as Release Please finished, minutes before CI on the same commit, and rewrote __version__.py from the tag. check now waits for the push CI run on the released commit and stops unless it succeeds, and it fails when the tag and the committed version disagree instead of patching the version in at build time. id-token: write moves from the workflow to the publish job. Closes #10 Closes #11
This was referenced Oct 16, 2026
This was referenced Oct 17, 2026
Merged
Merged
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.
The problem
Two issues, one job. #10: any branch could publish any ref. The
pypienvironment had no deployment policy, and the dispatchtaginput was checked out as given, whether it was a branch, a SHA or any tag. #11: publish started when Release Please finished, minutes before CI on the same commit was done, and it rewrote__version__.pyfrom the tag. A tag that disagreed with the committed version therefore shipped under the tag's version without any error. Both come down to thecheckjob deciding what gets published, so they share this PR.The decision
The boundary is the environment, and it admits
masteronly.setup_repo.pynow createspypiwith a custom deployment policy holding one branch policy for the default branch, and removes any other policy it finds, the same way it replaces the ruleset as a whole. Both triggers already run withmasteras their ref:workflow_runalways runs on the default branch, and a dispatch runs frommaster. So the workflow file that decides what reaches PyPI is always the reviewed one onmaster. A dispatch from another branch is refused the environment by GitHub, whatever that branch'spublish.ymlsays. This is a repository setting. The YAML also rejects a non-master dispatch incheck, but only to give a readable error; it does not protect anything.The
taginput stays, but it is only data. It reaches the shell throughenv:. It has to match the release tag pattern, exist as a tag, and point at a commit onmaster. The publish job checks out the SHA thatcheckresolved, so moving the tag between the two jobs changes nothing.CI on the released commit gates the release. When there is a release tag,
checkfinds thepushrun ofci.ymlfor that SHA, waits for it, and fails unless it endssuccess. If there is no CI run, that is an error. A dispatch goes through the same wait. Ordinary pushes tomasterwithout a tag skip the wait.The version comes from the commit. The "Set version from release tag" step is gone.
checkcompares the tag's version with__version__in the tagged commit and fails with both values when they differ. hatch builds from that file, so the artifacts always carry the committed version.id-token: writemoved from the workflow to thepublishjob.checkhascontents: readandactions: read.What I rejected
--ref <tag>) with the environment admitting<slug>-v*tags, as publish.yml can publish from any branch or tag: the pypi environment has no deployment policy and the tag input is used as-is #10 proposes. The tag's own copy ofpublish.ymlwould then run. Anyone who can push a matching tag could point it at a commit whose workflow skips every check, so the policy would need a tag ruleset, with a bypass for whatever creates release tags (GITHUB_TOKENtoday, an App after CI never runs on Release Please PRs, so every release needs a manual close and reopen #12). Ifmasteris the only ref that can deploy, tags need no protection for publishing to be safe.workflow_runof CI. CI and Release Please start from the same push, so publish would depend on which one happens to finish first, and a dispatch would still need its own wait.pypi. Every automated release would wait for a person.Evidence
Render of
origin/masteragainst a render of this branch (copier copy --defaults,demo-lib):id-token: writeat workflow level.checkandpublishcheck outinputs.tag/needs.check.outputs.tagas given. A "Set version from release tag" step is there.actionlint1.7.12 (with shellcheck) is clean on all four generated workflows.The "Resolve the release tag" step, taken from the rendered file and run with bash
-eo pipefail(as Actions runs it) in a git repo with a bareorigin:The "Wait for CI" step, run read-only against real clientwright runs:
With a fake
gh, a run that only appears on the third listing and staysin_progressfor two polls is waited out and passes.setup_repo.pywith a fakeghthat records the writes:The response shapes are the ones clientwright's
github-pagesenvironment returns today ({"custom_branch_policies":true,"protected_branches":false}andbranch_policies[].{id,name,type}). The generated gate on the render:ruff check/ruff format --check/mypyclean, 1 test passed,uv buildbuilt both files.After merging
Re-run
setup_repo.pyon each library to set the policy. Until a library runs it, itspypienvironment stays open to any branch, as it is today.Closes #10
Closes #11