diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 047a796..b9f2451 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,8 +16,12 @@ # What it does WHEN a Release is published: # build — builds sdist + wheel, uploads them as workflow artifacts. # pypi-publish — publishes those artifacts to PyPI via OIDC Trusted Publishing -# (NO API token, NO stored secret). On a manual workflow_dispatch -# with publish-target=testpypi it targets TestPyPI instead. +# (NO API token, NO stored secret). GATED on the repo variable +# PYPI_TRUSTED_PUBLISHER == 'true': until that is set, this job +# is SKIPPED cleanly (gray) so a Release still builds + signs + +# attaches bundles without a red publish failure. On a manual +# workflow_dispatch with publish-target=testpypi it targets +# TestPyPI instead. # sign — signs the sdist + wheel with Sigstore keyless (ambient OIDC -> # Fulcio, same trust model as sigstore-fixture.yml / integrity-gate.yml) # and attaches the .sigstore bundles to the GitHub Release assets. @@ -113,10 +117,18 @@ jobs: permissions: id-token: write # REQUIRED for OIDC Trusted Publishing — mints the token contents: read - # Skip entirely on a manual run that opted out of publishing. + # Gate: only run when OIDC Trusted Publishing is explicitly enabled via the + # repo variable PYPI_TRUSTED_PUBLISHER == 'true'. While the variable is unset + # (or any value other than 'true'), this job is SKIPPED cleanly (gray, not red) + # so a published Release still builds + Sigstore-signs + attaches bundles + # without a red "publish" failure. Set it once the Trusted Publisher exists: + # gh variable set PYPI_TRUSTED_PUBLISHER --body true (see RELEASING.md). + # The event-type guard is preserved: skip entirely on a manual run that opted + # out of publishing. if: >- - github.event_name == 'release' || - (github.event_name == 'workflow_dispatch' && github.event.inputs.publish-target != 'none') + vars.PYPI_TRUSTED_PUBLISHER == 'true' && + (github.event_name == 'release' || + (github.event_name == 'workflow_dispatch' && github.event.inputs.publish-target != 'none')) steps: - name: Download dist artifacts @@ -132,6 +144,9 @@ jobs: with: packages-dir: dist/ # No `password:` — OIDC Trusted Publishing is used (no stored secret). + # Re-running a Release for an already-published version no-ops on the + # duplicate instead of failing red (e.g. token-published 1.0.0 / 1.0.1). + skip-existing: true # TestPyPI — only on a manual dry run. Same OIDC mechanism, different index. # Requires a SEPARATE Trusted Publisher on test.pypi.org (optional; see RELEASING.md). @@ -141,6 +156,8 @@ jobs: with: packages-dir: dist/ repository-url: https://test.pypi.org/legacy/ + # Re-running a dry run no-ops on an already-uploaded TestPyPI version. + skip-existing: true # -------------------------------------------------------------------------- # Job 3: sign the release artifacts with Sigstore keyless ("heal thyself", W2.3). diff --git a/RELEASING.md b/RELEASING.md index 2641c6d..e188f0f 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -62,6 +62,31 @@ If you would rather seed the project manually first: > Prefer the pending-publisher path. It avoids ever minting a long-lived token and > keeps the entire supply chain OIDC-only from release #1. +### Enable OIDC publishing (the `PYPI_TRUSTED_PUBLISHER` gate) + +The `pypi-publish` job in `release.yml` is gated behind the repo variable +`PYPI_TRUSTED_PUBLISHER` and only runs when it equals `true`. This lets you publish +a GitHub Release that **builds + Sigstore-signs + attaches `.sigstore` bundles** +without the publish job failing red before the Trusted Publisher exists. + +- **While the variable is unset** (or any value other than `true`): publishing a + GitHub Release builds the sdist + wheel, Sigstore-signs them, and attaches the + bundles to the Release — but the `pypi-publish` job is **SKIPPED** (gray, not red) + and nothing is uploaded to PyPI. Use this to cut signed GitHub Releases for + versions already published by token (e.g. `1.0.0`, `1.0.1`). +- **After you have configured the Trusted Publisher above** (project + `mcp-warden-cli`, owner `ernestprovo23`, repo `mcp-warden`, workflow + `release.yml`), enable OIDC publishing for future releases by setting the + variable: + ```bash + gh variable set PYPI_TRUSTED_PUBLISHER --body true + ``` + or in the GitHub UI: **Settings → Secrets and variables → Actions → Variables → + New repository variable**, name `PYPI_TRUSTED_PUBLISHER`, value `true`. + +Re-running a Release for an already-published version is also safe: the publish step +uses `skip-existing: true`, so a duplicate version no-ops instead of failing. + ### (Optional) TestPyPI dry-run publisher The workflow has a manual `workflow_dispatch` path that publishes to TestPyPI for a @@ -107,9 +132,12 @@ Do this on a clean checkout of `main` with all v1 PRs merged. Publishing the Release fires `release.yml`, which: - **build** — builds the sdist + wheel and uploads them as workflow artifacts; - - **pypi-publish** — publishes those artifacts to PyPI via OIDC (no token); + - **pypi-publish** — publishes those artifacts to PyPI via OIDC (no token). + **Skipped unless** the repo variable `PYPI_TRUSTED_PUBLISHER` is `true` + (see "Enable OIDC publishing" in section 0). For versions already published + by token (`1.0.0`, `1.0.1`) leave it unset so this job skips cleanly; - **sign** — signs the sdist + wheel with Sigstore keyless and attaches the - `.sigstore` bundle(s) to the Release assets. + `.sigstore` bundle(s) to the Release assets (runs regardless of the gate). ---