Skip to content

Automate PyPI releases via GitHub Actions - #697

Open
hassaku63 wants to merge 5 commits into
facetoe:masterfrom
hassaku63:feat/release-automation
Open

hassaku63 wants to merge 5 commits into
facetoe:masterfrom
hassaku63:feat/release-automation

Conversation

@hassaku63

Copy link
Copy Markdown

Closes the request in #692 (comment) — cutting a release currently means a manual twine upload from a maintainer's machine.

What this does

Adds .github/workflows/publish.yml: when a (non pre-release) GitHub Release is published, it builds the sdist/wheel and uploads them to PyPI via Trusted Publishing (OIDC) — no PyPI token needs to be stored as a repo secret.

Version bumping stays manual (zenpy/__init__.py + setup.py, same as today) — the workflow only builds/publishes once you tag and release. It checks that the release tag matches the version in both files before building, and build/publish run as separate jobs (per PyPA's Trusted Publishing guidance) so the job holding the OIDC token never executes untrusted build-time code.

One-time setup needed from you

Register a Trusted Publisher for the zenpy project (a few clicks, no secret to manage): https://pypi.org/manage/project/zenpy/settings/publishing/

  • Owner: facetoe, Repository: zenpy, Workflow: publish.yml, Environment: blank

Full updated release steps in DEPLOY.md.

Validated separately

End-to-end tested (including catching and fixing a couple of real mistakes — a Test PyPI/production mix-up, and a setuptools-not-available failure — along the way) in a throwaway repo before proposing this here: https://github.com/hassaku63/zenpy-release-poc. Same workflow, published to real PyPI, confirms pre-releases are excluded, the split build/publish jobs work as intended, and Trusted Publishing works end-to-end.

🤖 Generated with Claude Code

hassaku63 and others added 5 commits September 2, 2026 22:37
Adds a workflow that publishes to PyPI via Trusted Publishing (OIDC)
whenever a GitHub Release is published, so tagging a release no longer
requires a manual build+twine step. Guards against publishing a
mismatched version by checking the tag against zenpy/__init__.py's
__version__ before building.

Addresses facetoe#692 (comment)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KiP1kDx4T3Ea8UyviXD8NM
- Trigger on 'released' instead of 'published' - the latter also
  fires for GitHub pre-releases, which would otherwise publish an
  unintended version to PyPI. Added a job-level 'if' guard on
  release.prerelease as defense in depth.
- Pin pypa/gh-action-pypi-publish to a commit SHA (v1.14.2) instead
  of the floating release/v1 tag, so a compromised/repointed tag on
  that action can't run inside a trusted-publishing job.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KiP1kDx4T3Ea8UyviXD8NM
The comment in publish.yml and DEPLOY.md still described the trigger
as "a GitHub Release is published", left over from before the
release/pre-release hardening. Clarifies that a release marked
pre-release will not run the publish workflow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KiP1kDx4T3Ea8UyviXD8NM
Addresses findings from a max-effort code review of this branch:

- Split into a 'build' job (untrusted code: pip install, setup.py via
  python -m build; no PyPI credential) and a 'publish' job (only
  downloads the artifact and uploads it; holds the OIDC token). Per
  PyPA's Trusted Publishing guidance, this stops anything running
  during the build from being able to reach the id-token permission.
- The version check now verifies setup.py's version too, not just
  zenpy/__init__.py's __version__ - the two are independently
  hand-maintained and only setup.py's is what `python -m build`
  actually embeds in the built package, since no active
  pyproject.toml exists.
- Anchored the __version__ regex and added a clear failure message
  instead of an unhandled AttributeError if it doesn't match.
- Added skip-existing: true to the publish step, so a re-run after a
  partial failure (e.g. wheel uploaded, sdist didn't) doesn't hard-fail
  on re-uploading the file that already succeeded.
- DEPLOY.md: added a step to confirm the run succeeded and the version
  landed on PyPI, noted that unmarking an existing pre-release also
  triggers publishing, de-duplicated the PyPI publisher setup values
  (single source of truth in publish.yml's header comment), and
  flagged that the manual fallback now needs a PyPI token of its own
  since Trusted Publishing only authorizes GitHub Actions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KiP1kDx4T3Ea8UyviXD8NM
python setup.py --version failed in CI with "No module named
setuptools" - GitHub's Python 3.14 runner doesn't have setuptools
importable in the base env before the build tool is installed, and
executing setup.py at all isn't necessary just to read its version=.
Extract it with the same regex approach already used for
zenpy/__init__.py's __version__, avoiding the dependency and the
unnecessary code execution.

Caught by re-running the hassaku63/zenpy-release-poc validation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KiP1kDx4T3Ea8UyviXD8NM
@joshomolewa

joshomolewa commented Sep 3, 2026 •

Copy link
Copy Markdown

@facetoe please Is it possible to look into this as you requested it. cc @etowle

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.

2 participants