From 8ba2305bf36ab5c62dc9248110274019fe538446 Mon Sep 17 00:00:00 2001 From: Max Isbey <224885523+maxisbey@users.noreply.github.com> Date: Sat, 25 Jul 2026 23:34:25 +0000 Subject: [PATCH 1/2] Document the two-line release process for stable v2 RELEASE.md described a v1.x stable line and v2 pre-releases only. With v2 becoming the stable line cut from main, it now documents both shipping lines and the stable procedure: a v2.X.Y stable release from main that takes GitHub "Latest" and PyPI's default install, a v1.28.Z maintenance release from v1.x that must keep --latest=false, and pre-releases from main. It also notes that both packages are yanked together and that the generated notes' previous-tag baseline must be set to the same line. No-Verification-Needed: doc-only --- RELEASE.md | 155 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 105 insertions(+), 50 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index f86da2ea67..c6b52bf06c 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -7,63 +7,118 @@ `[tool.hatch.metadata.hooks.uv-dynamic-versioning].dependencies`. 2. Upgrade lock with `uv lock --resolution lowest-direct` -## Major or Minor Release - -Stable releases are cut from the `v1.x` branch. Create a GitHub release via UI -with the tag being `vX.Y.Z` where `X.Y.Z` is the version and the release title -being the same, and **set the tag's target to the `v1.x` branch** — the UI -defaults to `main`, which is the v2 rework, and a v1 tag created there would -publish the v2 codebase as a stable release. Then ask someone to review the -release. - -The package version will be set automatically from the tag. - -## v2 Pre-releases - -v2 pre-releases are cut from `main` with a PEP 440 pre-release tag: `v2.0.0aN` -for alphas, later `bN`/`rcN` for betas and release candidates. - -A release publishes two distributions, `mcp` and `mcp-types`, at the same -version, and the `mcp` wheel exact-pins `mcp-types`. Before the first release -that includes both, the `mcp-types` PyPI project must be given the same -trusted publisher as `mcp` (this repository, workflow `publish-pypi.yml`, -environment `release`) and the same owners — without it the `mcp-types` -upload is rejected. If only some of the files upload, fix the cause and re-run -the publish job — `skip-existing` makes it skip whatever already landed. The -`Development Status` classifier in both `pyproject.toml` files is permanently -`5 - Production/Stable`; it is not bumped as part of any release. - -1. Update the pre-release version examples in `README.md` and the docs - (grep the outgoing version — the pins live in the README Installation - section, `docs/index.md`, `docs/get-started/installation.md`, and `docs/get-started/real-host.md`) so the tagged - commit — and therefore the README PyPI publishes — names the version - being released. When entering a new phase (alpha → beta → rc), update - the banner wording too. -2. Check the full test matrix is green on the release commit. The publish - workflow re-runs the checks and blocks publishing until they pass, so a - red leg there means re-running the failed jobs on the Publishing run. -3. Create the release as a pre-release, passing the exact commit verified in - step 2 as `--target` (otherwise the tag is created from whatever `main`'s - HEAD is by then). The tagged commit determines everything about the +## Release lines + +Two branches ship, and the package version comes from the git tag +(`uv-dynamic-versioning`). Publishing a GitHub release runs `publish-pypi.yml` +**from the tagged commit**, so the workflow that fires is the tagged branch's +own: a `main` tag builds and publishes two distributions (`mcp` and +`mcp-types`, lock-stepped via `Requires-Dist: mcp-types=={{ version }}`), and a +`v1.x` tag builds and publishes `mcp` only. + +| Line | Branch | Tag | GitHub release flags | +| ---------------------------- | ------ | ------------------------- | ------------------------------------- | +| Current stable | `main` | `v2.X.Y` | not a pre-release; becomes **Latest** | +| Maintenance (previous major) | `v1.x` | `v1.28.Z` | not a pre-release; **not** Latest | +| Pre-releases | `main` | `v2.X.YaN` / `bN` / `rcN` | **Pre-release** ticked, never Latest | + +The `Development Status` classifier in both `pyproject.toml` files is +permanently `5 - Production/Stable`; it is not bumped as part of any release. +The `mcp-types` PyPI project carries the same trusted publisher as `mcp` (this +repository, workflow `publish-pypi.yml`, environment `release`). If only some +of the four files upload, fix the cause and re-run the publish job — +`skip-existing` makes it skip whatever already landed. + +## Stable release from `main` (`v2.X.Y`) + +The stable line's README and docs carry no version pin (`pip install "mcp[cli]"` +installs the newest stable release), so a stable release needs no pin-flip +commit. `README.md` at the tagged commit is the PyPI long description, so any +README fix has to merge before the tag. + +1. Check the full test matrix is green on the release commit. The publish + workflow re-runs the same checks and blocks publishing until they pass, so a + red leg there means re-running the failed jobs on the Publishing run — but + verify green before creating the release rather than discovering red after + the tag exists. +2. Freeze `main` from that commit until the tag exists: the release is created + with an explicit `--target`, and nothing else should land in between. +3. Create the release NOT as a pre-release, passing the verified commit as + `--target` (otherwise the tag is created from whatever `main`'s HEAD is by + then). It becomes GitHub "Latest", and PyPI's default `pip install mcp` + version moves to it. The tagged commit determines everything about the release — the workflows that run and the package metadata (readme, classifiers) that gets published — so it must contain the current release tooling, not just pass tests. `--target` is ignored if the tag already exists: when re-creating a release, delete the old tag first and - double-check where the new tag points. The pre-release flag keeps GitHub's - "Latest" badge and `/releases/latest` pointing at the stable v1.x line: + double-check where the new tag points. + + ```shell + gh release create v2.X.Y --title v2.X.Y --target --notes-file + ``` + +4. Curate the release notes above the auto-generated `## What's Changed` list: + the highlights, anything known-incomplete, and links to the docs and + migration guide. Use absolute URLs (relative links don't resolve in GitHub + release bodies), and set the generated list's **Previous tag** to the + previous release on this line by hand — the auto-picked baseline is the + newest tag, which may sit on the other line. +5. If a stable release turns out to be broken, yank it on PyPI and release the + fix as the next patch version. Never delete a release from PyPI — version + numbers cannot be reused. Yank `mcp` and `mcp-types` together (they are one + release), and set the yank reason and the GitHub release notes to point at + the replacement version, since yanking doesn't stop `==` pins from installing + the broken version. + +## Maintenance release from `v1.x` (`v1.28.Z`) + +Land the `[v1.x]`-prefixed backport PRs (and any README banner update, which is +the README PyPI shows for that version), verify the branch tip green, then +create the release the same way with two differences: + +- **The tag's target is the `v1.x` branch.** The UI and CLI default the target + to `main`, which is the v2 codebase — a v1 tag created there would publish v2 + code as a v1 stable release. +- **It must not take "Latest" back from the 2.x line.** The UI ticks "Set as + the latest release" by default for the newest non-pre-release; untick it, or + pass `--latest=false`, and afterwards confirm `/releases/latest` still names + the newest v2 tag. If it slipped, `gh release edit v1.28.Z --latest=false` + fixes it — release metadata only, no re-cut. + +```shell +gh release create v1.28.Z --title v1.28.Z --target v1.x --latest=false --notes-file +``` + +When generating notes, set **Previous tag** to the previous `v1.*` release by +hand for the same reason as above. Then ask someone to review the release. + +## Pre-releases from `main` + +Pre-releases of the next version are cut from `main` with a PEP 440 +pre-release tag: `aN` for alphas, later `bN`/`rcN` for betas and release +candidates. The PEP 440 suffix is what keeps `pip install mcp` on the stable +version — installers only select a pre-release when it is requested by exact +pin. + +1. During a pre-release phase the README and docs pin the exact pre-release + version, so update those examples first (grep the outgoing version — the + pins live in the README Installation section, `docs/index.md`, + `docs/get-started/installation.md`, and `docs/get-started/real-host.md`) so + the tagged commit — and therefore the README PyPI publishes — names the + version being released. When entering a new phase (alpha → beta → rc → + stable), update the banner wording too; the stable phase drops the pins. +2. Check the full test matrix is green on the release commit, as above. +3. Create the release as a pre-release, passing the verified commit as + `--target`. The pre-release flag keeps GitHub's "Latest" badge and + `/releases/latest` on the newest stable release: ```shell - gh release create v2.0.0aN --prerelease --title v2.0.0aN --target + gh release create v2.X.YbN --prerelease --title v2.X.YbN --target ``` -4. Curate the release notes instead of relying on auto-generated ones: what - changed since the previous pre-release, what is known-incomplete, the - install line (`pip install mcp==2.0.0aN`), and a link to the migration - guide. Use the absolute URL - (`https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/migration.md`) - because relative links don't resolve in GitHub release bodies. +4. Curate the release notes: what changed since the previous pre-release, what + is known-incomplete, the install line (`pip install mcp==2.X.YbN`), and a + link to the migration guide, with absolute URLs. 5. If a pre-release turns out to be broken, yank it on PyPI and cut the next - one. Never delete a release from PyPI — version numbers cannot be reused. - Yanking doesn't stop `==` pins from installing the broken version, so set - the yank reason (and edit the GitHub release notes) to point at the + one, pointing the yank reason and the GitHub release notes at the replacement version. From 76075f2accceb759a193e587eeb407ac8a310255 Mon Sep 17 00:00:00 2001 From: Max Isbey <224885523+maxisbey@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:18:06 +0000 Subject: [PATCH 2/2] Address review nits in the release process doc Scope the partial-upload re-run advice to releases cut from main (the v1.x publisher has no skip-existing setting), note that the first stable release of a major still needs its pre-release flip, spell out how the generated What's Changed list is produced, target the verified commit rather than the branch name for a v1.x release, describe pre-release selection accurately, and yank both packages when a pre-release is bad. No-Verification-Needed: doc-only --- RELEASE.md | 49 ++++++++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index c6b52bf06c..1785da1225 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -25,16 +25,20 @@ own: a `main` tag builds and publishes two distributions (`mcp` and The `Development Status` classifier in both `pyproject.toml` files is permanently `5 - Production/Stable`; it is not bumped as part of any release. The `mcp-types` PyPI project carries the same trusted publisher as `mcp` (this -repository, workflow `publish-pypi.yml`, environment `release`). If only some -of the four files upload, fix the cause and re-run the publish job — -`skip-existing` makes it skip whatever already landed. +repository, workflow `publish-pypi.yml`, environment `release`). For a release +cut from `main`, if only some of the four files upload, fix the cause and +re-run the publish job — its `skip-existing` setting makes it skip whatever +already landed (the `v1.x` workflow publishes a single distribution and has no +such setting). ## Stable release from `main` (`v2.X.Y`) The stable line's README and docs carry no version pin (`pip install "mcp[cli]"` -installs the newest stable release), so a stable release needs no pin-flip -commit. `README.md` at the tagged commit is the PyPI long description, so any -README fix has to merge before the tag. +installs the newest stable release), so a routine stable release needs no +pin-flip commit; the exception is the first stable release of a new major, +whose pre-release banner and pins are replaced by that flip. `README.md` at the +tagged commit is the PyPI long description, so any README fix has to merge +before the tag. 1. Check the full test matrix is green on the release commit. The publish workflow re-runs the same checks and blocks publishing until they pass, so a @@ -57,12 +61,13 @@ README fix has to merge before the tag. gh release create v2.X.Y --title v2.X.Y --target --notes-file ``` -4. Curate the release notes above the auto-generated `## What's Changed` list: - the highlights, anything known-incomplete, and links to the docs and - migration guide. Use absolute URLs (relative links don't resolve in GitHub - release bodies), and set the generated list's **Previous tag** to the - previous release on this line by hand — the auto-picked baseline is the - newest tag, which may sit on the other line. +4. Curate the release notes: the highlights, anything known-incomplete, and + links to the docs and migration guide, above a `## What's Changed` list. + Generate that list with the release UI's "Generate release notes" (setting + its **Previous tag** to the previous release on this line by hand — the + auto-picked baseline is the newest tag, which may sit on the other line), or + assemble the whole body in the file passed to `--notes-file`. Use absolute + URLs (relative links don't resolve in GitHub release bodies). 5. If a stable release turns out to be broken, yank it on PyPI and release the fix as the next patch version. Never delete a release from PyPI — version numbers cannot be reused. Yank `mcp` and `mcp-types` together (they are one @@ -76,9 +81,11 @@ Land the `[v1.x]`-prefixed backport PRs (and any README banner update, which is the README PyPI shows for that version), verify the branch tip green, then create the release the same way with two differences: -- **The tag's target is the `v1.x` branch.** The UI and CLI default the target - to `main`, which is the v2 codebase — a v1 tag created there would publish v2 - code as a v1 stable release. +- **The tag's target is the verified commit on the `v1.x` branch.** The UI and + CLI default the target to `main`, which is the v2 codebase — a v1 tag created + there would publish v2 code as a v1 stable release. Pass the exact commit + verified green in the previous step rather than the branch name, for the + same moving-HEAD reason as above. - **It must not take "Latest" back from the 2.x line.** The UI ticks "Set as the latest release" by default for the newest non-pre-release; untick it, or pass `--latest=false`, and afterwards confirm `/releases/latest` still names @@ -86,7 +93,7 @@ create the release the same way with two differences: fixes it — release metadata only, no re-cut. ```shell -gh release create v1.28.Z --title v1.28.Z --target v1.x --latest=false --notes-file +gh release create v1.28.Z --title v1.28.Z --target --latest=false --notes-file ``` When generating notes, set **Previous tag** to the previous `v1.*` release by @@ -97,8 +104,8 @@ hand for the same reason as above. Then ask someone to review the release. Pre-releases of the next version are cut from `main` with a PEP 440 pre-release tag: `aN` for alphas, later `bN`/`rcN` for betas and release candidates. The PEP 440 suffix is what keeps `pip install mcp` on the stable -version — installers only select a pre-release when it is requested by exact -pin. +version — installers only select a pre-release when it is requested explicitly (an +exact pin, a specifier that names a pre-release version, or `--pre`). 1. During a pre-release phase the README and docs pin the exact pre-release version, so update those examples first (grep the outgoing version — the @@ -119,6 +126,6 @@ pin. 4. Curate the release notes: what changed since the previous pre-release, what is known-incomplete, the install line (`pip install mcp==2.X.YbN`), and a link to the migration guide, with absolute URLs. -5. If a pre-release turns out to be broken, yank it on PyPI and cut the next - one, pointing the yank reason and the GitHub release notes at the - replacement version. +5. If a pre-release turns out to be broken, yank both `mcp` and `mcp-types` on PyPI + and cut the next one, pointing the yank reason and the GitHub release notes + at the replacement version.