Skip to content

Automate package release process via GitHub Actions - #505

Draft
psobot wants to merge 6 commits into
masterfrom
automate-releases
Draft

Automate package release process via GitHub Actions#505
psobot wants to merge 6 commits into
masterfrom
automate-releases

Conversation

@psobot

@psobot psobot commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

Cutting a release previously meant editing the version in three places
(pedalboard/version.py, pyproject.toml, uv.lock) and then manually
creating a release through the GitHub UI. Now it's one line.

Single source of truth
----------------------
pyproject.toml switches to `dynamic = ["version"]`, reading the version
out of pedalboard/version.py via scikit-build-core's regex metadata
provider. setup.py already read that same file, so version.py is now the
only place a version string appears.

uv.lock no longer pins a version either: uv deliberately omits the
version field for dynamic source trees ("Omit versions for dynamic
source trees", uv-resolver/src/lock/mod.rs), so future bumps won't make
the lockfile stale and `uv sync --locked` keeps working untouched.

Automatic releases
------------------
A new `detect-release` job notices when a push to master changes
__version__. When it does, that same workflow run builds all wheels,
runs the full test and ASan suites, uploads to PyPI, and finally tags
the commit and creates the GitHub release with generated notes.

Everything deliberately stays inside one workflow run: releases created
with the default GITHUB_TOKEN do not trigger new workflow runs, so a
separate "create the release" workflow could never have kicked off the
wheel builds. Keeping it in-run also means no PAT is needed.

The existing manual path (publish a release in the UI) still works, and
nothing is published unless __version__ actually changes. Re-releasing an
existing tag is skipped with a warning rather than failing.

`cancel-in-progress` is now limited to pull requests. A push to master
can publish to PyPI, and cancelling that partway through would leave a
half-finished release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ND3oQGgms3aXVsoBikUr93
@psobot psobot changed the title Automate releases: bump one file, get a published release Automate package release process via GitHub Actions Aug 10, 2026
psobot and others added 5 commits August 10, 2026 17:23
Two follow-ups to the release automation.

Logic out of YAML
-----------------
The detect-release job was a 40-line inline Bash block, which was both
hard to read and impossible to test. It's now scripts/detect_release.py,
with the decision logic in pure functions and 29 tests in
tests/test_detect_release.py covering version parsing, validation, and
every branch of the release decision.

The tests load the script by path rather than importing it as a package,
so they also pass when tests/ is run from a temporary directory during
post-wheel-build testing.

Guarding against accidental releases
------------------------------------
Merging a version bump publishes to PyPI irreversibly, and it would be
easy to do that without realising. Two defences:

Any pull request that changes __version__ now says so. The job emits a
::warning:: against version.py and writes a note to the run summary
spelling out that merging will publish, and that PyPI releases can be
yanked but never replaced.

The upload-pypi job now runs in the "PyPI Deployment" environment, which
already existed in this repo but was unused and had no protection rules.
Adding a required reviewer to it makes publishing pause for approval;
until then behaviour is unchanged. Moving PYPI_DEPLOY_TOKEN from a
repo-level secret into that environment would further ensure no other
job -- or future workflow change -- can publish at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ND3oQGgms3aXVsoBikUr93
The "PyPI Deployment" environment has no protection rules, and nobody on
the project currently has the admin access needed to add a required
reviewer or move PYPI_DEPLOY_TOKEN into it.

An environment reference with no protection rules fails open silently: it
reads like an approval gate to anyone scanning the workflow, while
gating nothing at all. That's worse than not having one, so it's removed
rather than left as decoration, with a comment recording why.

The safeguard against an accidental release is now solely the warning
that detect-release puts on any pull request changing __version__.
That's advisory rather than enforcing, and the docs say so plainly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ND3oQGgms3aXVsoBikUr93
The docs were rebuilt and deployed on every push to master, so the
published documentation described whatever was on master rather than
what people can actually install from PyPI.

Filter the push trigger to pedalboard/version.py. That file holds
nothing but the version, so touching it is as good a signal as parsing
it, and a version bump always changes it -- no release can slip through
without its docs. The existing workflow_dispatch trigger still covers
docs-only fixes that shouldn't wait for a release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ND3oQGgms3aXVsoBikUr93
The previous commit filtered the docs deploy on changes to
pedalboard/version.py, on the assumption that the file holds nothing but
the version. It doesn't: it also carries a copyright header whose year
range gets bumped annually, usually by a sweep across the whole repo.

That would have deployed the docs with no version bump, publishing
documentation for an unreleased master -- exactly what gating the docs
was meant to prevent.

detect_release.py now also reports version-changed, distinct from
should-release: the version can change without the run being allowed to
publish (a pull request, or a tag that already exists), and the docs
should follow the version rather than the release permission.

deploy-docs.yml keeps the path filter as a cheap pre-filter -- a version
bump always touches that file -- and adds a check-version-bump job that
confirms __version__ actually changed. Manual runs still deploy
unconditionally, so docs-only fixes don't need a release.

Tested both ways: editing only the copyright year yields
version-changed=false, and a real bump yields true.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ND3oQGgms3aXVsoBikUr93
Gating the docs workflow on its own version check was the wrong shape.
It answered "did the version change?" when the question is "did we
publish?", and those differ: a push whose tag already exists changes the
version but publishes nothing, and the separate workflow ran in parallel
with the upload anyway, so the docs could go live for a version that
then failed to reach PyPI.

deploy-docs.yml becomes a reusable workflow (workflow_call), and all.yml
calls it from a job that needs upload-pypi. That dependency is the entire
gate: if upload-pypi is skipped because this isn't a release, or fails
because PyPI rejected the wheels, the docs are never published. The docs
now describe exactly what's installable, which is the point.

This also deletes the duplicated gate: no second detect job, and
detect_release.py no longer emits version-changed, since nothing consumes
it. version_changed stays as an internal field because the "merging this
publishes" warning on pull requests still needs it.

deploy-docs.yml keeps workflow_dispatch, so docs-only fixes can still be
published without cutting a release. Its push and tag triggers are gone:
the push trigger is superseded, and the tag trigger never fired anyway,
since tags created with GITHUB_TOKEN don't trigger workflows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ND3oQGgms3aXVsoBikUr93
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