Skip to content

ci: add PyPI publish workflow with OIDC trusted publishing#226

Merged
ccf merged 2 commits into
mainfrom
chore/pypi-publish-workflow
May 6, 2026
Merged

ci: add PyPI publish workflow with OIDC trusted publishing#226
ccf merged 2 commits into
mainfrom
chore/pypi-publish-workflow

Conversation

@ccf
Copy link
Copy Markdown
Owner

@ccf ccf commented May 6, 2026

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:

  • Pushing a v*.*.* tag → builds, verifies the tag matches the pyproject version, runs twine check --strict, publishes to TestPyPI, then to PyPI
  • Manual workflow_dispatch with target=testpypi|pypi for rehearsals

Both uploads run inside named GitHub Environments (testpypi and pypi), so prod releases can be gated behind reviewer approval.

PUBLISHING.md (rewritten)

  • One-time setup: register trusted publishers on PyPI + TestPyPI, create the two GitHub environments, and do one token-based upload to claim the useprimer project name (PyPI requires the project to exist before trusted publishing works)
  • Per-release flow becomes: bump version → branch → PR → tag → CI publishes

Test plan

  • python -m build from current main produces useprimer-0.2.0 artifacts
  • python -m twine check --strict dist/* PASSED on both wheel + sdist
  • After merge: configure trusted publishers + environments, then do the one-time token upload to claim useprimer
  • Then cut v0.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 on v*.*.* tags (and via manual dispatch), using OIDC trusted publishing instead of stored PyPI tokens. The workflow builds wheel/sdist, runs twine check --strict, verifies the tag version matches pyproject.toml, publishes to TestPyPI first, then to PyPI via separate GitHub Environments (testpypi/pypi) to support approval gating.

Rewrites PUBLISHING.md to 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.

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>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

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.

Create PR

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.

Comment thread .github/workflows/publish.yml
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.
@ccf ccf merged commit fdeb858 into main May 6, 2026
6 checks passed
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