This is the operator runbook for cutting a release of mcp-warden.
Two names matter and they are deliberately different:
| Thing | Value |
|---|---|
PyPI distribution name (what pip install uses) |
mcp-warden-cli |
| CLI command (what users type) | mcp-warden |
| GitHub repository | DataScience-EngineeringExperts/mcp-warden |
Install is therefore pip install mcp-warden-cli, but the command stays mcp-warden.
The PyPI name mcp-warden is an unrelated package by another author. PyPI rejects
mcpwarden as "too similar" to it — PyPI's anti-typosquat guard strips separators,
so mcpwarden and mcp-warden collapse to the same string. mcp-warden-cli
normalizes to letters-only mcpwardencli, which is distinct, so it is accepted and
does not collide.
The publish + signing automation lives in
.github/workflows/release.yml. That workflow is
publishes production packages only on a published GitHub Release. Manual modes
are TestPyPI upload, build-only, and production authentication verification without
uploading. Production authentication needs a matching PyPI Trusted Publisher.
The production project already exists. Its publisher is managed at PyPI → mcp-warden-cli → Publishing by a logged-in project owner. An upload API token is not a publisher-admin browser session.
For the existing workflow, the GitHub publisher fields are:
| Field | Expected value |
|---|---|
| Owner | DataScience-EngineeringExperts |
| Repository name | mcp-warden |
| Workflow name | release.yml |
| Environment name | blank: the current publishing job has no deployment environment |
Inspect the current publisher before changing it. An existing environment restriction must be deliberately aligned on both sides, preserving any required reviewer gate. A repository transfer can also change the immutable owner ID PyPI records; a matching display name alone does not prove identity alignment. See PyPI troubleshooting.
The workflow uses OIDC Trusted Publishing, without a stored GitHub upload token.
The repo variable PYPI_TRUSTED_PUBLISHER=true permits an attempt; it does not prove
that a matching PyPI publisher exists. Verify the exchange before cutting a new release.
- Log in to https://pypi.org as the account that will own
mcp-warden-cli. - Go to Account → Publishing (https://pypi.org/manage/account/publishing/).
- Under Add a new pending publisher, fill in exactly:
- PyPI Project Name:
mcp-warden-cli - Owner:
DataScience-EngineeringExperts - Repository name:
mcp-warden - Workflow name:
release.yml - Environment name: (leave blank — the workflow does not use a GitHub
deployment environment; if you later add one, set it here and add
environment:to thepypi-publishjob)
- PyPI Project Name:
- Save the pending configuration. A successful authorized OIDC flow can create the new project if the name remains available; saving does not reserve it.
A pending publisher does not reserve a name or create a project. It is not the
setup path for the already-existing mcp-warden-cli project. See
PyPI pending publishers.
If you would rather seed the project manually first:
- Build locally:
python -m build(producesdist/*.tar.gz+dist/*.whl). twine upload dist/*with a temporary PyPI API token (createsmcp-warden-cli).- Then go to Manage project → Publishing on the new
mcp-warden-cliproject and add the Trusted Publisher with the same owner/repo/workflow values as above. - Revoke the temporary token.
Prefer the pending-publisher path. It avoids ever minting a long-lived token and keeps the entire supply chain OIDC-only from release #1.
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 thepypi-publishjob is SKIPPED (gray, not red) and nothing is uploaded to PyPI. Use this to cut signed GitHub Releases for historical 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, ownerDataScience-EngineeringExperts, repomcp-warden, workflowrelease.yml), enable OIDC publishing for future releases by setting the variable:or in the GitHub UI: Settings → Secrets and variables → Actions → Variables → New repository variable, namegh variable set PYPI_TRUSTED_PUBLISHER --body true
PYPI_TRUSTED_PUBLISHER, valuetrue.
The publish action must authenticate before skip-existing: true can handle
duplicate files. A previously uploaded version does not bypass a broken publisher.
Before dispatch, a logged-in project owner must inspect the normal publisher on the existing project's page above and check Account → Publishing for matching pending publishers visible in that account. Confirm the intended owner/repository/ workflow/environment and that no matching pending entry is visible. Do not dispatch against a pending publisher or acknowledge an inspection that has not occurred.
The exchange is not universally read-only: PyPI's mint endpoint checks pending publishers first and can activate/create or remove their server-side records. The owning-account check cannot exclude other users' pending records. The verification path creates only an in-memory upload credential and never uploads a package; it is not a server-wide non-mutation guarantee.
After that inspection, dispatch the existing release workflow on reviewed main:
gh workflow run release.yml --ref main \
--field publish-target=verify-pypi --field publisher-checked=true
gh run list --workflow release.yml --event workflow_dispatch --limit 1
gh run view <run-id> --logThe check refuses other branches/tags and an unconfirmed publisher before requesting any tokens. Only Verify PyPI OIDC (no upload) runs. Build, signing and upload jobs are skipped; the script uses Python's standard library and preserves the release workflow's repository/owner/workflow identity and current environment contract. It validates the GitHub request host, refuses redirects, limits response sizes and suppresses credentials and arbitrary server error text. The minted short-lived credential remains only in memory and is discarded without being used.
Pass: the exchange job succeeds and reports that PyPI accepted the workflow
identity. This confirms authentication, not project-specific upload permission or
package safety. Confirm the normal publisher is attached to mcp-warden-cli in
the project settings; a pending publisher is not the intended verification target.
Fail: any failed/malformed exchange, identity mismatch or missing permission
exits nonzero. A skipped publication job is not proof of working authentication.
For invalid-publisher, compare the existing project's four fields above and the
immutable owner identity with the current workflow. Do not copy tokens into logs,
add a workflow token fallback, disable the gate to hide the failure, or remove a
reviewer/environment restriction to make the check pass.
Release run 36960647544
built and signed the release but failed PyPI's exchange with invalid-publisher.
The same exchange error occurred on the prior 1.2.0 release. The authorized manual
2.0.0 upload used the exact signed GitHub wheel/sdist; their SHA-256 hashes match
PyPI 2.0.0. That recovery proves
publication, not that OIDC automation was repaired.
After the publisher is aligned and authentication succeeds, rerun only the failed job from the original release run, using its retained build artifacts:
gh run rerun 36960647544 --failedDo not rebuild/re-sign/replace the released assets as a repair attempt. If the
original artifacts expired, stop and plan a fix-forward release. Success means
OIDC authentication passes and the existing immutable files are handled by
skip-existing; verify the public wheel/sdist hashes remain unchanged.
The workflow has a manual workflow_dispatch path that publishes to TestPyPI for a
dry run. To use it, repeat step 2–4 above on https://test.pypi.org (separate
account + separate pending publisher for mcp-warden-cli). This is optional and only
needed if you want to rehearse the publish without touching production PyPI.
Do this on a clean checkout of main with the intended changes merged through a reviewed PR.
The commands below use 2.0.1 as an example next version, not a published release.
-
Update the changelog. In
CHANGELOG.md, move the## [Unreleased]entries under a new## [2.0.1] - <YYYY-MM-DD>heading with today's date. Leave a fresh empty## [Unreleased]section above it. -
Bump both Python versions. In
pyproject.toml, set[project] version = "2.0.1"; update__version__insrc/mcp_warden/__init__.pyto match. -
Commit.
git add CHANGELOG.md pyproject.toml src/mcp_warden/__init__.py git commit -m "release: v2.0.1" # Push a release branch and merge its reviewed PR; do not push directly to main.
-
Refresh the merged
main, then tag and push the tag. Required CI must be green at the reviewed release commit. A tag alone does not publish packages; the Release in the next step triggers the workflow.git tag v2.0.1 git push origin v2.0.1
-
Create the GitHub Release. This is the trigger.
gh release create v2.0.1 --verify-tag \ --title "v2.0.1" \ --notes-file /tmp/release-notes.mdor use the GitHub UI: Releases → Draft a new release → choose tag
v2.0.1→ Publish release.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).
Skipped unless the repo variable
PYPI_TRUSTED_PUBLISHERistrue(see "Enable OIDC publishing" in section 0). Previously published files still require valid authentication; do not hide an exchange failure; - sign — signs the sdist + wheel with Sigstore keyless and attaches the
.sigstore.jsonbundle(s) to the Release assets (runs regardless of the gate).
The build job also runs the shared TypeScript conformance suite and npm pack.
The npm-installable .tgz and SHA256SUMS are downloaded only by the signing job;
PyPI receives only the Python distribution artifact. The existing release identity
signs and attaches the TypeScript tarball and checksums alongside Python artifacts.
No npm registry token or publisher is configured by this workflow.
Verify the tarball bundle against the same release workflow identity, then install
the exact GitHub asset into a fresh consumer and import @mcp-warden/lock.
-
Install from PyPI (give the CDN a minute):
pip install mcp-warden-cli==2.0.1 mcp-warden --version
The version must print
2.0.1. Note the install name ismcp-warden-cli, the command ismcp-warden. -
Verify the Sigstore bundle. On the GitHub Release page, confirm there is a
.sigstore.json(bundle) asset next to each.tar.gz/.whl. Thesignjob already self-verified against this workflow's own identity before attaching, but you can re-verify any artifact locally:pip install sigstore sigstore verify identity dist/mcp_warden_cli-2.0.1-py3-none-any.whl \ --bundle mcp_warden_cli-2.0.1-py3-none-any.whl.sigstore.json \ --cert-identity \ "https://github.com/DataScience-EngineeringExperts/mcp-warden/.github/workflows/release.yml@refs/tags/v2.0.1" \ --cert-oidc-issuer "https://token.actions.githubusercontent.com"(Download the
.whlintodist/and its.sigstore.jsonbundle first.) -
Confirm the PyPI page. Visit https://pypi.org/project/mcp-warden-cli/ and check:
- version
2.0.1is listed; - the project URLs (homepage / repository) point at
DataScience-EngineeringExperts/mcp-warden; - an automated upload records Trusted Publisher provenance; the manual 2.0.0 recovery must not be described as an OIDC upload.
- version
-
Smoke-test the gate in a throwaway dir to confirm the published wheel works:
mcp-warden --help
PyPI uploads are immutable — you cannot overwrite a published version. If a release is broken:
- Yank the bad version (keeps existing pins working, hides it from new installs): on https://pypi.org/project/mcp-warden-cli/ → Manage → Releases → Options → Yank. Yanking is reversible.
- Ship a fix-forward release (
2.0.2) following section 1 again. This is the preferred remedy — never try to re-upload2.0.1. - GitHub Release: you may delete or edit the GitHub Release and its assets
only with a deliberate repair plan; that does not change PyPI bytes. For publisher
repair, rerun only the failed upload job with the original artifacts. Normal
duplicate handling uses
skip-existingafter successful authentication.
- No stored secret. OIDC Trusted Publishing means GitHub never holds a PyPI token; PyPI trusts the workflow identity directly. Same trust model as the repo's existing keyless Sigstore signing.
- Heal thyself. mcp-warden signs everyone else's locks; from v2.0.1 it signs its
own release artifacts too (the
signjob), so consumers can verify the wheel they install came from this repo's release workflow. - Explicit gesture. A pushed tag does nothing; only publishing a Release ships. That keeps accidental tags from triggering a publish.