From cc153e8b56c37433680a695e06df880edc5879a9 Mon Sep 17 00:00:00 2001 From: ruv Date: Tue, 21 Jul 2026 16:07:11 -0700 Subject: [PATCH] ci(adr-184): migrate pip-release publish to PyPI OIDC Trusted Publishing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop all four PYPI_API_TOKEN password inputs from the publish-v2 and publish-tombstone jobs, grant id-token: write, and bind both jobs to the pypi GitHub environment so pypa/gh-action-pypi-publish uses secretless Trusted Publishing. Rewrite the header + add step comments documenting the blocking manual pypi.org trusted-publisher registration (ADR-184 §3.1) and the token fallback (§3.2). Build matrix untouched. Refs ADR-184 P1. --- .github/workflows/pip-release.yml | 59 ++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pip-release.yml b/.github/workflows/pip-release.yml index 99bab495..40ddc320 100644 --- a/.github/workflows/pip-release.yml +++ b/.github/workflows/pip-release.yml @@ -13,10 +13,29 @@ # 1. cut tag `v1.99.0-pip` → publishes the tombstone wheel first # 2. cut tag `v2.0.0-pip` → publishes the PyO3 v2 wheel matrix # -# Publishes via the `PYPI_API_TOKEN` GitHub Actions secret. The -# token-refresh runbook (GCP Secret Manager → gh secret set) lives in -# docs/integrations/pypi-release.md so KICS does not flag the -# secret name as a generic-secret literal in the workflow. +# Publishes via PyPI **Trusted Publishing (OIDC)** — ADR-184 P1. The +# publish jobs mint a short-lived OIDC token from `id-token: write` and +# the `pypi` GitHub environment; the `pypa/gh-action-pypi-publish` action +# exchanges it for a per-run PyPI upload token. There is NO static +# `PYPI_API_TOKEN` secret in the publish steps anymore — a manually +# rotated token is exactly the failure mode (silent expiry → 403) this +# migration removes. +# +# BLOCKING MANUAL PREREQUISITE (ADR-184 §3.1 — cannot be automated): +# A human with owner rights must register a Trusted Publisher on +# pypi.org for EACH project (wifi-densepose, and ruview as a pending +# publisher) with: +# Owner=ruvnet Repository=RuView Workflow=pip-release.yml Environment=pypi +# Until BOTH entries exist, the publish steps fail with a +# "no trusted publisher configured" error (not a 403). Do NOT re-add a +# `password:` to "fix" that — doing so silently disables OIDC again. +# Fallback (ADR-184 §3.2): if OIDC is declined, restore `password: +# ${{ secrets.PYPI_API_TOKEN }}` on the publish steps per the token +# runbook in docs/integrations/pypi-release.md. +# +# TestPyPI dry-run: the test.pypi.org publish steps also dropped their +# `password:` — they require a SEPARATE trusted-publisher entry on +# test.pypi.org (same Environment=pypi) before that path will work. # # Q3 (witness hash v2 — open in ADR-117 §11.3) MUST be resolved # before the first v2.0.0 publish. When v2 lands, add a parallel @@ -222,6 +241,15 @@ jobs: publish-v2: name: Publish v2 wheels needs: [build-wheels, build-sdist] + # ADR-184 P1: OIDC Trusted Publishing. `id-token: write` mints the + # OIDC token; `environment: pypi` binds this job to the pypi.org + # trusted-publisher entry (see §3.1 blocking manual step in header). + permissions: + id-token: write + contents: read + environment: + name: pypi + url: https://pypi.org/p/wifi-densepose if: | always() && needs.build-wheels.result == 'success' && @@ -241,26 +269,38 @@ jobs: mkdir -p dist find dist-staging -type f \( -name '*.whl' -o -name '*.tar.gz' \) -exec cp -v {} dist/ \; ls -lh dist/ + # ADR-184 P1: Trusted Publishing (OIDC) — no `password:`. Needs a + # test.pypi.org trusted-publisher entry (Environment=pypi) to work. - name: Publish to TestPyPI (dry-run target) if: github.event_name == 'workflow_dispatch' && inputs.publish_to == 'testpypi' uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ - password: ${{ secrets.PYPI_API_TOKEN }} packages-dir: dist skip-existing: true + # ADR-184 P1: Trusted Publishing (OIDC) — no `password:`. INERT until + # the pypi.org trusted publisher is registered (header §3.1); until + # then this step fails "no trusted publisher configured", not 403. - name: Publish to PyPI if: | startsWith(github.ref, 'refs/tags/v2.') || (github.event_name == 'workflow_dispatch' && inputs.publish_to == 'pypi') uses: pypa/gh-action-pypi-publish@release/v1 with: - password: ${{ secrets.PYPI_API_TOKEN }} packages-dir: dist publish-tombstone: name: Publish v1.99 tombstone needs: [build-tombstone] + # ADR-184 P1: OIDC Trusted Publishing. `id-token: write` mints the + # OIDC token; `environment: pypi` binds this job to the pypi.org + # trusted-publisher entry (see §3.1 blocking manual step in header). + permissions: + id-token: write + contents: read + environment: + name: pypi + url: https://pypi.org/p/wifi-densepose if: | always() && needs.build-tombstone.result == 'success' && @@ -274,19 +314,22 @@ jobs: with: name: tombstone path: dist + # ADR-184 P1: Trusted Publishing (OIDC) — no `password:`. Needs a + # test.pypi.org trusted-publisher entry (Environment=pypi) to work. - name: Publish to TestPyPI (dry-run target) if: github.event_name == 'workflow_dispatch' && inputs.publish_to == 'testpypi' uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ - password: ${{ secrets.PYPI_API_TOKEN }} packages-dir: dist skip-existing: true + # ADR-184 P1: Trusted Publishing (OIDC) — no `password:`. INERT until + # the pypi.org trusted publisher is registered (header §3.1); until + # then this step fails "no trusted publisher configured", not 403. - name: Publish to PyPI if: | startsWith(github.ref, 'refs/tags/v1.99') || (github.event_name == 'workflow_dispatch' && inputs.publish_to == 'pypi') uses: pypa/gh-action-pypi-publish@release/v1 with: - password: ${{ secrets.PYPI_API_TOKEN }} packages-dir: dist