From 492af8264d2520e048a48ea4839bebbf2b315cf3 Mon Sep 17 00:00:00 2001 From: Brian McMahon Date: Wed, 13 May 2026 13:48:00 -0700 Subject: [PATCH] =?UTF-8?q?Add=20release.yml=20=E2=80=94=20PyPI=20publish?= =?UTF-8?q?=20on=20tag=20via=20OIDC=20Trusted=20Publisher?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fires on tag push matching v* (e.g. v0.5.0rc3, v0.5.0, v0.6.0). Two jobs: build (sdist + wheel + twine check + tag-vs-version sanity check) and publish (downloads the artifact + uploads via pypa/gh-action-pypi-publish using PyPI's OIDC Trusted Publisher). No API token lives in repo secrets — the action exchanges the short-lived GitHub OIDC token for PyPI publish creds, scoped to this exact workflow on this exact repo. Token rotation hygiene becomes automatic. `workflow_dispatch` with a `ref` input is included as a manual re-publish fallback for cases where the tag-push event was missed or the artifact needs to be rebuilt from a known ref. The build job verifies that the tag (refs/tags/vX.Y.Z) matches the built wheel's version metadata before handoff to publish, so a forgotten flow_doctor/__init__.py / pyproject.toml bump fails the release loud at the tag push instead of shipping a stale version. The publish job runs under a `pypi` GitHub environment so an optional manual approval gate can be added later (Settings → Environments → pypi → Required reviewers) without changing the workflow. One-time PyPI-side setup required before this workflow can publish: 1. https://pypi.org/manage/project/flow-doctor/settings/publishing/ 2. Add a new pending publisher: - PyPI Project Name: flow-doctor - Owner: cipher813 - Repository name: flow-doctor - Workflow filename: release.yml - Environment name: pypi Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/release.yml | 92 +++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..12eddb7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,92 @@ +name: Publish to PyPI + +# Fires on a pushed tag matching v* (e.g. v0.5.0rc3, v0.5.0, v0.6.0). +# Builds wheel + sdist, then publishes via PyPI's OIDC Trusted Publisher +# integration — no API token lives in repo secrets. The PyPI side needs +# a one-time Trusted Publisher entry registered at +# https://pypi.org/manage/project/flow-doctor/settings/publishing/ +# (owner=cipher813, repo=flow-doctor, workflow=release.yml, +# environment=pypi). + +on: + push: + tags: + - 'v*' + # Manual fallback for re-publishing if a tag-push event was missed. + workflow_dispatch: + inputs: + ref: + description: "Tag or branch to build" + required: true + default: "main" + +permissions: + contents: read + +jobs: + build: + name: Build distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + # On a tag push, ref is the tag; on manual dispatch, use the + # supplied ref so re-publishes are explicit. + ref: ${{ github.event.inputs.ref || github.ref }} + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install build + run: | + python -m pip install --upgrade pip + python -m pip install build twine + + - name: Build wheel + sdist + run: python -m build + + - name: Verify metadata + run: python -m twine check dist/* + + - name: Verify built version matches pushed tag + if: github.event_name == 'push' + run: | + # github.ref looks like refs/tags/v0.5.0rc2; strip the prefix + # and the leading v so it matches the project version. + tag_version="${GITHUB_REF#refs/tags/v}" + built_version=$(ls dist/*.whl | sed -E 's|.*flow_doctor-(.*)-py3.*|\1|') + if [ "$tag_version" != "$built_version" ]; then + echo "::error::Tag version ($tag_version) doesn't match built version ($built_version)." + echo "Bump version in flow_doctor/__init__.py + pyproject.toml before tagging." + exit 1 + fi + echo "Tag $tag_version matches built $built_version" + + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + name: Publish to PyPI + needs: build + runs-on: ubuntu-latest + # GitHub environment with optional approval gate — keeps a deliberate + # human-in-the-loop step before any tag actually fires a PyPI release. + environment: + name: pypi + url: https://pypi.org/p/flow-doctor + # id-token: write is the OIDC requirement for Trusted Publishing. + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - uses: pypa/gh-action-pypi-publish@release/v1 + # The action handles the OIDC token exchange + upload. No + # password / token field needed when Trusted Publisher is + # configured on the PyPI side.