Skip to content

Publish on a published release, not on every merged PR - #96

Open
Marius1311 wants to merge 1 commit into
dpeerlab:mainfrom
quadbio:publish-on-release
Open

Publish on a published release, not on every merged PR#96
Marius1311 wants to merge 1 commit into
dpeerlab:mainfrom
quadbio:publish-on-release

Conversation

@Marius1311

Copy link
Copy Markdown

Closes #95.

⚠️ Needs one setup step on PyPI before this is merged

This switches to trusted publishing,
so a trusted publisher has to be registered on PyPI for the scenvi project
first, or the first release after merging will fail to upload:

field value
Owner dpeerlab
Repository name ENVI
Workflow name publish.yaml
Environment name pypi

Once it works, the PYPI_TOKEN secret can be deleted — nothing reads it any
more. I can't do any of this myself, it needs someone with owner rights on the
PyPI project.

If you would rather keep the API token, say so and I'll swap the publish step
back to password: ${{ secrets.PYPI_TOKEN }}; everything else in this PR is
independent of that choice.

What changes

Releasing becomes two explicit steps:

  1. bump version in pyproject.toml in a normal pull request, and merge it;
  2. draft a GitHub release tagged v<version> and publish it.

Merging alone no longer releases anything. That means changes can be batched into
one release, the version number can say what kind of change it is rather than just
counting merges, and there is a natural place to write release notes.

The workflow itself follows the
scverse cookiecutter template:
trigger on release: types: [published], empty top-level permissions,
id-token: write on the job only, build with uv build, upload with
pypa/gh-action-pypi-publish.

One deliberate difference from the template. It derives the version from the
tag via hatch-vcs, so tag and version cannot disagree. scenvi's version is
written by hand in pyproject.toml, so they can. A step therefore compares the
release tag against the version of the wheel that was just built — the artifact
actually about to be uploaded, rather than a second parse of pyproject.toml
and fails the release on a mismatch:

::error::release tag 'v0.4.7' does not match the built version '0.4.6'

(If you do move to hatchling + hatch-vcs later, as #94 touches on, that step
can just be deleted.)

Also here

test.yaml now runs on pushes to main as well as on pull requests. Once
publishing stops happening on merge, nothing else re-checks the merge result, and
a release should only be cut from a green main. Happy to drop this if you'd
rather keep it to one concern per PR.

What is removed

The version bump, the commit, the tag push and the git push origin HEAD:main all
go, along with contents: write. CI no longer writes to the repository at all, so
this also stops bypassing branch protection on main.

Testing

Workflow changes can't really be exercised until they run on main, so what I
could check locally:

  • both files parse, and resolve to the intended triggers (release;
    workflow_dispatch/pull_request/push);
  • uv build succeeds against the current poetry-core backend, producing
    scenvi-0.4.6.tar.gz and scenvi-0.4.6-py3-none-any.whluv build only runs
    the PEP 517 backend, so it does not require moving off poetry;
  • the tag check accepts v0.4.6 against that wheel, rejects v0.4.7 with the
    error above, and errors if dist/ does not contain exactly one wheel.

publish.yaml triggered on `pull_request: closed` with `merged == true`, then bumped
the patch version, committed it to main, tagged, built and published. Every merge
was therefore a release, with several consequences (dpeerlab#95):

  * a docs typo, a CI tweak or an internal refactor each shipped a version, and
    changes could not be batched;
  * the bump was always `patch`, so the number said "how many merges", not what
    kind of change it was;
  * the publish job had no `needs:` and ran no tests, so nothing re-checked the
    merge result before uploading;
  * it cannot work for pull requests from forks, which get no secrets and a
    read-only token -- both `secrets.PYPI_TOKEN` and `git push origin HEAD:main`
    would fail;
  * CI pushed straight to main, bypassing any branch protection;
  * two merges landing close together raced on the same version number;
  * uploads used a long-lived API token rather than PyPI trusted publishing.

Releasing is now two explicit steps: bump `version` in pyproject.toml in a normal
pull request, then draft a GitHub release tagged `v<version>`. Merging alone
releases nothing.

The workflow follows scverse's cookiecutter template: trigger on
`release: types: [published]`, no top-level permissions, `id-token: write` on the
job, build with `uv build`, upload with pypa/gh-action-pypi-publish via trusted
publishing. No token is stored.

The template derives its version from the tag via hatch-vcs; scenvi's is written by
hand in pyproject.toml, so the two can drift. A step therefore compares the release
tag against the version of the wheel that was just built -- the artifact about to be
uploaded, rather than a second parse of pyproject.toml -- and fails the release on a
mismatch.

test.yaml additionally runs on pushes to main, so that main is known-good before a
release is cut from it. Nothing covered that once publishing stopped happening on
merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

Publishing on every merged PR is hard to control

1 participant