Skip to content

ci: add publish_pypi workflow for trusted-publisher PyPI releases#69

Open
malteos wants to merge 1 commit intomainfrom
ci/publish-pypi-workflow
Open

ci: add publish_pypi workflow for trusted-publisher PyPI releases#69
malteos wants to merge 1 commit intomainfrom
ci/publish-pypi-workflow

Conversation

@malteos
Copy link
Copy Markdown
Collaborator

@malteos malteos commented May 4, 2026

Summary

Adds .github/workflows/publish_pypi.yaml — a manually-dispatched release pipeline that uses PyPI Trusted Publishing (OIDC, no API tokens) to publish cdx_toolkit to TestPyPI and PyPI, then cuts a GitHub Release.

Replaces the manual make dist flow currently documented in the Makefile. The workflow is workflow_dispatch only — releases never happen automatically on push.

Why this design

The project uses setup.py + setuptools-scm (version derived from git tags, no pyproject.toml, no uv/uv.lock). The workflow keeps this versioning model — the git tag is the source of truth. The bare-tag convention (0.9.39, no v) documented in the Makefile is preserved.

Trusted publishing is already configured on:

  • PyPI — environment release
  • TestPyPI — environment testpypi

Both bound to workflow filename publish_pypi.yaml.

Workflow inputs

Input Type Default Effect
version_bump choice (patch/minor/major) patch Which semver component to bump from the latest tag
dry_run boolean true If true: .devN-suffixed build → TestPyPI only; no tag push, no PyPI, no GitHub Release

The dry_run=true default is intentional — the very first dispatch after merging is harmless.

Job graph

bump-and-build  ──►  publish-testpypi  ──►  publish-pypi  ──►  github-release
                                            (skipped if         (skipped if
                                              dry_run)            dry_run)
  1. bump-and-build — checkout (full history + tags), set up Python 3.13, pip install build twine packaging, compute the next version by parsing the latest ^[0-9]+\.[0-9]+\.[0-9]+$ tag and bumping per version_bump (or appending .dev${GITHUB_RUN_NUMBER} for dry runs), validate with packaging.version.Version, build sdist + wheel with SETUPTOOLS_SCM_PRETEND_VERSION_FOR_CDX_TOOLKIT set so the artifact carries the intended version before any tag exists, run twine check, verify built filenames, upload dist/. Real-release-only final step: create + push annotated tag.
  2. publish-testpypi — environment testpypi, id-token: write. Downloads the artifact and publishes to https://test.pypi.org/legacy/ with skip-existing: true. Runs for both dry-run and real release.
  3. publish-pypi — environment release, id-token: write. Skipped on dry runs. No skip-existing (hard fail on accidental version reuse).
  4. github-release — checks out the tag, downloads the artifact, runs gh release create "${VERSION}" dist/* --title "${VERSION}" --generate-notes --verify-tag.

Concurrency lock group: publish, cancel-in-progress: false prevents racing dispatches.

Step-by-step usage

Dry run (recommended first invocation)

gh workflow run publish_pypi.yaml -f version_bump=patch -f dry_run=true

Expected:

  • Latest tag is 0.9.38 → computed version is 0.9.39.dev<run_number>.
  • bump-and-build produces dist/cdx_toolkit-0.9.39.dev<run>.tar.gz + matching wheel.
  • publish-testpypi succeeds; release visible at https://test.pypi.org/project/cdx-toolkit/0.9.39.dev<run>/.
  • publish-pypi and github-release are skipped.
  • No git tag is pushed.

Real patch release

gh workflow run publish_pypi.yaml -f version_bump=patch -f dry_run=false

Expected (latest tag 0.9.38 → release 0.9.39):

  1. bump-and-build builds cdx_toolkit-0.9.39.tar.gz + wheel and pushes annotated tag 0.9.39 to origin.
  2. publish-testpypi uploads to TestPyPI (idempotent if re-run).
  3. publish-pypi uploads to PyPI; release live at https://pypi.org/project/cdx-toolkit/0.9.39/.
  4. github-release cuts the GitHub Release on tag 0.9.39 with auto-generated notes and the sdist + wheel attached.

Verifying after a real release

git fetch --tags
git tag --list | grep '^0\.9\.39$'                  # tag exists locally
pip install --force-reinstall cdx_toolkit==0.9.39    # in a clean venv
pip show cdx_toolkit                                  # reports 0.9.39

Operational notes

  • Tag pushed before publish. If a publish job fails after the tag is pushed, the tag persists and a retry would conflict. Recovery: git push --delete origin <tag> and re-dispatch. (Matches the source workflow's behavior; not overengineered into a multi-step rollback.)
  • Required secrets. None — trusted publishing replaces tokens. The default GITHUB_TOKEN is sufficient for tag push and gh release create.
  • Required environments. Both testpypi and release GitHub Environments must exist with their respective trusted-publisher configs on test.pypi.org and pypi.org. (User confirmed these are configured.)
  • Permissions per job.
    • bump-and-build: contents: write (push tag).
    • publish-testpypi, publish-pypi: id-token: write only.
    • github-release: contents: write (create release).

Test plan

  • Merge to main (workflow file must be on the default branch before workflow_dispatch is dispatchable).
  • Run dry run from main: gh workflow run publish_pypi.yaml -f version_bump=patch -f dry_run=true. Verify (a) all four jobs behave as expected, (b) 0.9.39.dev<run> appears on TestPyPI, (c) no tag is pushed, (d) publish-pypi and github-release are skipped.
  • If anything needs tweaking, iterate on a branch and re-test with gh workflow run publish_pypi.yaml --ref my-branch -f dry_run=true (the workflow file must already be on main once for --ref dispatches to work).
  • When ready: real patch release gh workflow run publish_pypi.yaml -f version_bump=patch -f dry_run=false. Verify tag 0.9.39 on origin, release on pypi.org, GitHub Release page populated, and pip install cdx_toolkit==0.9.39 works in a clean venv.

🤖 Generated with Claude Code

Adds .github/workflows/publish_pypi.yaml — a workflow_dispatch
release pipeline using PyPI/TestPyPI trusted publishing via OIDC.

Adapted from a uv/pyproject.toml-based template to fit this
project's setuptools-scm + setup.py build, bare-tag convention
(0.x.y, no v), and the configured trusted publishers
(env=release on PyPI, env=testpypi on TestPyPI).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 4, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.12%. Comparing base (13d629c) to head (9351f2c).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main      #69   +/-   ##
=======================================
  Coverage   97.12%   97.12%           
=======================================
  Files           9        9           
  Lines         939      939           
=======================================
  Hits          912      912           
  Misses         27       27           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@malteos malteos requested review from lfoppiano and wumpus May 4, 2026 12:07
@lfoppiano
Copy link
Copy Markdown

@malteos do you have an action traces tested with dry-run? I cannot find it, and I don't see any "workflow_dispatcher" in the action tab.

@malteos
Copy link
Copy Markdown
Collaborator Author

malteos commented May 4, 2026

That's the tricky thing about PRs like this. You can only run workflow_dispatch when the workflow exists in the default branch. Thus, let's do a read-only review, merge this to main, and then run the dry-run version of the workflow including publish to testpypi.

@malteos
Copy link
Copy Markdown
Collaborator Author

malteos commented May 4, 2026

Copy link
Copy Markdown

@lfoppiano lfoppiano left a comment

Choose a reason for hiding this comment

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

Based on the description and on the test, LGTM. I particularly like the idea of using special dev suffix when dry-run is set to true.
I'm curious about the automated github release, I might steal it 😄

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