ci(adr-184): migrate pip-release publish to PyPI OIDC Trusted Publishing

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.
This commit is contained in:
ruv
2026-07-21 16:07:11 -07:00
parent cca5bd8113
commit cc153e8b56
+51 -8
View File
@@ -13,10 +13,29 @@
# 1. cut tag `v1.99.0-pip` → publishes the tombstone wheel first # 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 # 2. cut tag `v2.0.0-pip` → publishes the PyO3 v2 wheel matrix
# #
# Publishes via the `PYPI_API_TOKEN` GitHub Actions secret. The # Publishes via PyPI **Trusted Publishing (OIDC)** — ADR-184 P1. The
# token-refresh runbook (GCP Secret Manager → gh secret set) lives in # publish jobs mint a short-lived OIDC token from `id-token: write` and
# docs/integrations/pypi-release.md so KICS does not flag the # the `pypi` GitHub environment; the `pypa/gh-action-pypi-publish` action
# secret name as a generic-secret literal in the workflow. # 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 # 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 # before the first v2.0.0 publish. When v2 lands, add a parallel
@@ -222,6 +241,15 @@ jobs:
publish-v2: publish-v2:
name: Publish v2 wheels name: Publish v2 wheels
needs: [build-wheels, build-sdist] 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: | if: |
always() && always() &&
needs.build-wheels.result == 'success' && needs.build-wheels.result == 'success' &&
@@ -241,26 +269,38 @@ jobs:
mkdir -p dist mkdir -p dist
find dist-staging -type f \( -name '*.whl' -o -name '*.tar.gz' \) -exec cp -v {} dist/ \; find dist-staging -type f \( -name '*.whl' -o -name '*.tar.gz' \) -exec cp -v {} dist/ \;
ls -lh 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) - name: Publish to TestPyPI (dry-run target)
if: github.event_name == 'workflow_dispatch' && inputs.publish_to == 'testpypi' if: github.event_name == 'workflow_dispatch' && inputs.publish_to == 'testpypi'
uses: pypa/gh-action-pypi-publish@release/v1 uses: pypa/gh-action-pypi-publish@release/v1
with: with:
repository-url: https://test.pypi.org/legacy/ repository-url: https://test.pypi.org/legacy/
password: ${{ secrets.PYPI_API_TOKEN }}
packages-dir: dist packages-dir: dist
skip-existing: true 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 - name: Publish to PyPI
if: | if: |
startsWith(github.ref, 'refs/tags/v2.') || startsWith(github.ref, 'refs/tags/v2.') ||
(github.event_name == 'workflow_dispatch' && inputs.publish_to == 'pypi') (github.event_name == 'workflow_dispatch' && inputs.publish_to == 'pypi')
uses: pypa/gh-action-pypi-publish@release/v1 uses: pypa/gh-action-pypi-publish@release/v1
with: with:
password: ${{ secrets.PYPI_API_TOKEN }}
packages-dir: dist packages-dir: dist
publish-tombstone: publish-tombstone:
name: Publish v1.99 tombstone name: Publish v1.99 tombstone
needs: [build-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: | if: |
always() && always() &&
needs.build-tombstone.result == 'success' && needs.build-tombstone.result == 'success' &&
@@ -274,19 +314,22 @@ jobs:
with: with:
name: tombstone name: tombstone
path: dist 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) - name: Publish to TestPyPI (dry-run target)
if: github.event_name == 'workflow_dispatch' && inputs.publish_to == 'testpypi' if: github.event_name == 'workflow_dispatch' && inputs.publish_to == 'testpypi'
uses: pypa/gh-action-pypi-publish@release/v1 uses: pypa/gh-action-pypi-publish@release/v1
with: with:
repository-url: https://test.pypi.org/legacy/ repository-url: https://test.pypi.org/legacy/
password: ${{ secrets.PYPI_API_TOKEN }}
packages-dir: dist packages-dir: dist
skip-existing: true 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 - name: Publish to PyPI
if: | if: |
startsWith(github.ref, 'refs/tags/v1.99') || startsWith(github.ref, 'refs/tags/v1.99') ||
(github.event_name == 'workflow_dispatch' && inputs.publish_to == 'pypi') (github.event_name == 'workflow_dispatch' && inputs.publish_to == 'pypi')
uses: pypa/gh-action-pypi-publish@release/v1 uses: pypa/gh-action-pypi-publish@release/v1
with: with:
password: ${{ secrets.PYPI_API_TOKEN }}
packages-dir: dist packages-dir: dist