Release runbook¶
How a uniprot-mcp release ships, link by link, and what to check
when something looks wrong. The chain is designed to be re-runnable
and auditable years after the fact — every artefact carries a
provenance attestation, an SBOM, and a Zenodo DOI.
Release policy — releases are deliberate¶
release-please opens a release PR only for feat: (minor) and
fix: (patch) commits. Housekeeping types — ci, docs, test,
refactor, perf, deps — are marked hidden: true in
release-please-config.json, so merging them lands silently on main
without proposing a version bump. This stops a steady stream of
ci:/docs: merges from each cutting their own patch release. A
housekeeping change that genuinely warrants a release (e.g. a
security-relevant dependency bump) should be committed as fix: so it
triggers one. Past releases are immutable on PyPI/Zenodo regardless;
this only governs what gets cut going forward.
The chain¶
git tag v1.1.6 ──► release.yml (Actions) ──► PyPI (Trusted Publishing, OIDC)
│ │ │
│ │ └──► MCP Registry (registry.modelcontextprotocol.io, GitHub OIDC)
│ │
│ ├──► GitHub Release (assets: dist/*, sbom.cdx.json, *.sigstore.json)
│ │ │
│ │ ├──► release-verify.yml (immediate)
│ │ │ ├── pip index versions uniprot-mcp-server
│ │ │ ├── gh release view (asset presence)
│ │ │ └── gh attestation verify (SLSA)
│ │ │
│ │ └──► Zenodo webhook ──► version DOI minted
│ │ │
│ │ └──► zenodo-verify.yml
│ │ (scheduled, delayed)
│ │
│ └──► Sigstore signing (gh-action-sigstore-python)
│
└──► release-drafter.yml (next-release draft updated on every push to main)
Step-by-step¶
1. Pre-tag (on the release branch)¶
- [ ]
python scripts/check_versions.pyexits 0 (every version-bearing file agrees withpyproject.toml). - [ ]
pytest tests/unit tests/property tests/client tests/contractgreen (includestest_version_consistencyandtest_changelog_has_current_version). - [ ]
CHANGELOG.mdhas a## [X.Y.Z] - YYYY-MM-DDentry. - [ ] PR is merged into
main; CI on the merge commit is green.
2. Tag push¶
The tag triggers .github/workflows/release.yml. Watch the run at
https://github.com/smaniches/uniprot-mcp/actions/workflows/release.yml.
3. release.yml jobs¶
| Job | Output |
|---|---|
build |
sdist + wheel; CycloneDX SBOM (sbom.cdx.json); SLSA build-provenance attestation; SBOM attestation; uploads as release-artifacts |
publish-pypi |
PyPI upload via OIDC Trusted Publishing (no token) |
publish-mcp-registry |
server.json published to the official MCP Registry (registry.modelcontextprotocol.io); runs after publish-pypi; GitHub OIDC auth, no secret |
sign-and-release |
Sigstore signatures (*.sigstore.json); GitHub Release created with all assets |
The publish-mcp-registry job authenticates to the registry with GitHub
OIDC for the io.github.smaniches namespace, and the registry verifies
control of the PyPI package via the mcp-name: io.github.smaniches/uniprot-mcp
marker in README.md (the PyPI long description). Both must stay intact for
the publish to validate. server.json's version is bumped by release-please
(an extra-files target), so it always matches the released PyPI version.
If any job fails: read the failure, fix on a follow-up commit, push
a new tag (v1.1.6.post1 or v1.1.7). Do not delete and re-push
the original tag — Zenodo treats the original as a separate version
and the duplicate DOI is forever.
4. Zenodo webhook (passive)¶
The Zenodo–GitHub integration is enabled per-account. Once flipped
on for the repo, every new GitHub Release triggers Zenodo to read
.zenodo.json and mint a new version DOI under the concept DOI
10.5281/zenodo.19817710.
The synchronization is asynchronous. It often completes within 30 seconds to five minutes, but five minutes is not a failure boundary. Release v1.3.2 was still represented as v1.3.1 when its original five-minute verification ended and later advanced automatically to v1.3.2. Do not create a manual deposit, change the concept DOI badge, or move the release tag to compensate for normal propagation delay.
Enabling the webhook (one-time):
- Visit
https://zenodo.org/account/settings/github/. - Toggle the
smaniches/uniprot-mcprepo to ON. - Confirm the GitHub-side webhook at
https://github.com/smaniches/uniprot-mcp/settings/hooksshows a recent successful delivery tozenodo.org/api/hooks/receivers/...with HTTP 200.
The webhook is idempotent — re-enabling and re-disabling will not mint duplicate DOIs.
5. PyPI Trusted Publishing (passive)¶
The publisher entry on
https://pypi.org/manage/account/publishing/ binds:
- Project:
uniprot-mcp-server - Owner:
smaniches - Repository:
uniprot-mcp - Workflow:
release.yml - Environment:
pypi
If the entry is missing (e.g. after a PyPI account migration), the
publish-pypi job will fail with OIDC token verification. Re-add
the publisher entry with the four fields above.
6. Post-tag verification (automatic)¶
Verification is split by expected consistency window.
release-verify.yml fires immediately on release: [published]. It verifies
PyPI, the complete GitHub Release asset set, and SLSA provenance. A failure in
one of those links opens Release verification failed for <tag> with the
release-drift label. Zenodo is deliberately excluded from this immediate
gate.
zenodo-verify.yml runs every six hours. For scheduled runs it checks the
latest stable GitHub Release only after that release is at least six hours old.
It compares the release tag with metadata.version on Zenodo concept record
19817710. Only a mismatch after that delayed threshold opens
Zenodo verification failed for <tag>. When a later delayed run observes the
expected version, it records the successful run on that Zenodo-specific issue
and closes it. A transient inability to read the Zenodo API fails the workflow
without claiming that the deposit itself is missing.
To re-trigger immediate verification manually for an older tag:
To verify Zenodo immediately for the latest stable tag, bypassing the scheduled age gate explicitly:
The manual Zenodo workflow rejects older tags because the concept-record API exposes only the latest-version pointer. Historical version records must be verified by their immutable version DOI rather than compared with that pointer.
7. CITATION.cff version-DOI append (next cycle)¶
Each release mints a Zenodo version DOI that is only known after
the deposit lands. The convention: the v1.1.6 release adds the
v1.1.6 changelog entry; the following release cycle (v1.1.7)
appends the v1.1.6 version DOI to CITATION.cff identifiers:.
This is why the file currently lists DOIs for v1.1.1 and v1.1.2
but not v1.1.5 — that entry will be added with v1.1.6's polish
work.
When a release goes wrong¶
| Symptom | Probable cause | Fix |
|---|---|---|
Immediate release-verify fails |
PyPI, GitHub assets, or SLSA provenance is incomplete | inspect the named failed step and repair the release chain; Zenodo is not part of this result |
Delayed zenodo-verify opens an issue |
Zenodo still does not expose the release after the six-hour threshold | inspect the GitHub-Zenodo integration delivery and the existing concept record; do not mint manually or change the DOI badge |
zenodo-verify cannot read the API |
Zenodo or the network is temporarily unavailable | retry later; this condition does not assert that a deposit is missing |
publish-pypi fails with Token request error |
OIDC publisher entry missing on PyPI | re-add at pypi.org/manage/account/publishing/ |
release-verify says missing sigstore bundle |
sign-and-release job was skipped (likely a permissions regression) |
re-grant id-token: write on the job, re-run the workflow |
pip install uniprot-mcp-server==X says version not found |
PyPI CDN lag, usually clears in 60 s | wait, retry; if persistent, check pypi.org/project/uniprot-mcp-server/#history |
Rolling forward, not back¶
Never delete or re-push a release tag. PyPI rejects re-uploads
of the same (name, version). Zenodo treats the second push as a
separate version. GitHub renders both as duplicate releases. If a
release goes wrong, ship a .postN or a Z+1 patch — fast forward
only.