ci: add PyPI publish workflow with OIDC trusted publishing#226
Merged
Conversation
Releases are now CI-driven: pushing a v*.*.* tag triggers
.github/workflows/publish.yml, which builds the wheel + sdist,
verifies the tag matches pyproject's version, runs twine check
--strict, publishes to TestPyPI, then publishes to PyPI. Both
uploads use OIDC trusted publishing — no PyPI tokens stored as
secrets and no credentials on developer laptops.
Workflow also supports manual dispatch (Actions → Run workflow)
for rehearsals against TestPyPI without cutting a tag.
PUBLISHING.md rewritten to cover:
- One-time PyPI/TestPyPI trusted-publisher registration
- GitHub environment setup ("pypi" + "testpypi", with optional
reviewer protection on prod)
- Initial token-based upload to claim the useprimer name (PyPI
requires the project to exist before trusted publishing works)
- Standard release flow: bump → branch → PR → tag → CI publishes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Manual PyPI dispatch is skipped
- Added always() with explicit needs.build.result and needs.publish-testpypi.result checks to the publish-pypi job condition, allowing it to run when publish-testpypi is skipped during target=pypi dispatches.
Or push these changes by commenting:
@cursor push db33a8a5c8
Preview (db33a8a5c8)
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -110,8 +110,13 @@
publish-pypi:
# Only tag pushes (or an explicit dispatch with target=pypi) reach prod.
if: |
- github.event_name == 'push' ||
- (github.event_name == 'workflow_dispatch' && inputs.target == 'pypi')
+ always() &&
+ needs.build.result == 'success' &&
+ (needs.publish-testpypi.result == 'success' || needs.publish-testpypi.result == 'skipped') &&
+ (
+ github.event_name == 'push' ||
+ (github.event_name == 'workflow_dispatch' && inputs.target == 'pypi')
+ )
needs: [build, publish-testpypi]
runs-on: ubuntu-latest
environment:You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit c9f2e14. Configure here.
A workflow_dispatch with target=pypi was getting skipped because: publish-testpypi.if -> only runs for push or target=testpypi, so skipped publish-pypi.needs: [build, publish-testpypi] -> inherits the skip Wrap the condition in always() and accept skipped TestPyPI as a passing dependency state. Build still has to succeed; tag pushes still rehearse on TestPyPI before prod. Caught by Cursor Bugbot on PR #226.
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
Set up a CI-driven release pipeline so future PyPI publishes happen on tag push instead of from a developer's laptop. Uses OIDC trusted publishing — no PyPI tokens stored as repo secrets.
.github/workflows/publish.yml(new)Triggers on:
v*.*.*tag → builds, verifies the tag matches the pyproject version, runstwine check --strict, publishes to TestPyPI, then to PyPIworkflow_dispatchwithtarget=testpypi|pypifor rehearsalsBoth uploads run inside named GitHub Environments (
testpypiandpypi), so prod releases can be gated behind reviewer approval.PUBLISHING.md(rewritten)useprimerproject name (PyPI requires the project to exist before trusted publishing works)Test plan
python -m buildfrom current main producesuseprimer-0.2.0artifactspython -m twine check --strict dist/*PASSED on both wheel + sdistuseprimerv0.2.1(bump pyproject) → CI publishes end-to-end🤖 Generated with Claude Code
Note
Medium Risk
Introduces an automated release pipeline that can publish artifacts to TestPyPI/PyPI; misconfiguration or incorrect tagging/versioning could result in failed or unintended releases.
Overview
Adds a new GitHub Actions workflow (
.github/workflows/publish.yml) to build and publish the package onv*.*.*tags (and via manual dispatch), using OIDC trusted publishing instead of stored PyPI tokens. The workflow builds wheel/sdist, runstwine check --strict, verifies the tag version matchespyproject.toml, publishes to TestPyPI first, then to PyPI via separate GitHub Environments (testpypi/pypi) to support approval gating.Rewrites
PUBLISHING.mdto document the new CI-driven release process, including one-time trusted publisher/environment setup, the initial token-based project claim, and the updated tag-based release and rehearsal steps.Reviewed by Cursor Bugbot for commit 404396c. Bugbot is set up for automated code reviews on this repo. Configure here.