-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Document the two-line release process for stable v2 #3179
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
Check failure on line 37 in RELEASE.md
|
||
|
Comment on lines
+34
to
+37
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 The stable-release section opens with "The stable line's README and docs carry no version pin ... so a stable release needs no pin-flip commit", but at the moment this section is first used — cutting v2.0.0 — the README and docs still pin Extended reasoning...The bug. The new "Stable release from Current state of the tree. Why the doc doesn't otherwise prevent the mistake. The only instruction to drop the pins lives in step 1 of the separate "Pre-releases from Impact. The doc itself explains why this matters: " Step-by-step proof. (1) The release engineer preparing v2.0.0 opens RELEASE.md and jumps to "Stable release from Fix. One sentence in the stable section: note that the first stable release (v2.0.0) requires the pin-drop/banner-removal commit to land before the tag (the phase-transition step described in the Pre-releases section, step 1) — or rephrase the opening so the "no pin-flip" claim is explicitly conditional on the pins having already been dropped at the pre-release → stable transition. Severity. All three verifiers confirmed the finding; two rated it normal, one nit (on the grounds that the information exists elsewhere in the same file and the window is a single release). Normal is warranted here: the single release in the window is exactly the release this PR was written to instruct, the section doesn't merely omit the step but asserts its negation, and the failure it invites is an immutable published artifact fixable only by cutting another version — while the fix is one sentence. |
||
|
|
||
| 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 <commit-sha> --notes-file <notes.md> | ||
| ``` | ||
|
|
||
| 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 <notes.md> | ||
| ``` | ||
|
|
||
| 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 | ||
|
Check warning on line 104 in RELEASE.md
|
||
|
Comment on lines
+100
to
+104
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 The claim that installers only select a pre-release "when it is requested by exact pin" is over-narrow: pip also resolves pre-releases with Extended reasoning...The issue. The new "Pre-releases from
uv behaves analogously: Concrete walk-through. Suppose Why it matters (and why it's minor). This file exists to give the release manager a precise mental model of release mechanics — that's the stated purpose of the rewrite in this PR — so a factually wrong universal in freshly-added prose is worth fixing. That said, nothing in the actual release procedure depends on the erroneous clause: the operative claim (the PEP 440 suffix keeps plain Fix. One-line rewording, e.g.: "The PEP 440 suffix is what keeps All three verifiers independently confirmed the factual claim against pip's documented behavior and agreed on nit severity; there were no refutations. Docs-only, non-blocking. |
||
| 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 <commit-sha> | ||
| gh release create v2.X.YbN --prerelease --title v2.X.YbN --target <commit-sha> | ||
| ``` | ||
|
|
||
| 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. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: A
v1.xrelease will use the default-branch release workflow, not a workflow definition fromv1.x; currentpublish-pypi.ymltherefore attempts to buildmcp-typestoo. Document/configure a default-branch workflow that branches on the release tag (or use a supported separate trigger) before directing maintainers to cut v1 releases this way.Prompt for AI agents