Skip to content

Commit 025f77a

Browse files
committed
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
1 parent 45f2a88 commit 025f77a

1 file changed

Lines changed: 105 additions & 50 deletions

File tree

RELEASE.md

Lines changed: 105 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -7,63 +7,118 @@
77
`[tool.hatch.metadata.hooks.uv-dynamic-versioning].dependencies`.
88
2. Upgrade lock with `uv lock --resolution lowest-direct`
99

10-
## Major or Minor Release
11-
12-
Stable releases are cut from the `v1.x` branch. Create a GitHub release via UI
13-
with the tag being `vX.Y.Z` where `X.Y.Z` is the version and the release title
14-
being the same, and **set the tag's target to the `v1.x` branch** — the UI
15-
defaults to `main`, which is the v2 rework, and a v1 tag created there would
16-
publish the v2 codebase as a stable release. Then ask someone to review the
17-
release.
18-
19-
The package version will be set automatically from the tag.
20-
21-
## v2 Pre-releases
22-
23-
v2 pre-releases are cut from `main` with a PEP 440 pre-release tag: `v2.0.0aN`
24-
for alphas, later `bN`/`rcN` for betas and release candidates.
25-
26-
A release publishes two distributions, `mcp` and `mcp-types`, at the same
27-
version, and the `mcp` wheel exact-pins `mcp-types`. Before the first release
28-
that includes both, the `mcp-types` PyPI project must be given the same
29-
trusted publisher as `mcp` (this repository, workflow `publish-pypi.yml`,
30-
environment `release`) and the same owners — without it the `mcp-types`
31-
upload is rejected. If only some of the files upload, fix the cause and re-run
32-
the publish job — `skip-existing` makes it skip whatever already landed. The
33-
`Development Status` classifier in both `pyproject.toml` files is permanently
34-
`5 - Production/Stable`; it is not bumped as part of any release.
35-
36-
1. Update the pre-release version examples in `README.md` and the docs
37-
(grep the outgoing version — the pins live in the README Installation
38-
section, `docs/index.md`, `docs/get-started/installation.md`, and `docs/get-started/real-host.md`) so the tagged
39-
commit — and therefore the README PyPI publishes — names the version
40-
being released. When entering a new phase (alpha → beta → rc), update
41-
the banner wording too.
42-
2. Check the full test matrix is green on the release commit. The publish
43-
workflow re-runs the checks and blocks publishing until they pass, so a
44-
red leg there means re-running the failed jobs on the Publishing run.
45-
3. Create the release as a pre-release, passing the exact commit verified in
46-
step 2 as `--target` (otherwise the tag is created from whatever `main`'s
47-
HEAD is by then). The tagged commit determines everything about the
10+
## Release lines
11+
12+
Two branches ship, and the package version comes from the git tag
13+
(`uv-dynamic-versioning`). Publishing a GitHub release runs `publish-pypi.yml`
14+
**from the tagged commit**, so the workflow that fires is the tagged branch's
15+
own: a `main` tag builds and publishes two distributions (`mcp` and
16+
`mcp-types`, lock-stepped via `Requires-Dist: mcp-types=={{ version }}`), and a
17+
`v1.x` tag builds and publishes `mcp` only.
18+
19+
| Line | Branch | Tag | GitHub release flags |
20+
| ---------------------------- | ------ | ------------------------- | ------------------------------------- |
21+
| Current stable | `main` | `v2.X.Y` | not a pre-release; becomes **Latest** |
22+
| Maintenance (previous major) | `v1.x` | `v1.28.Z` | not a pre-release; **not** Latest |
23+
| Pre-releases | `main` | `v2.X.YaN` / `bN` / `rcN` | **Pre-release** ticked, never Latest |
24+
25+
The `Development Status` classifier in both `pyproject.toml` files is
26+
permanently `5 - Production/Stable`; it is not bumped as part of any release.
27+
The `mcp-types` PyPI project carries the same trusted publisher as `mcp` (this
28+
repository, workflow `publish-pypi.yml`, environment `release`). If only some
29+
of the four files upload, fix the cause and re-run the publish job —
30+
`skip-existing` makes it skip whatever already landed.
31+
32+
## Stable release from `main` (`v2.X.Y`)
33+
34+
The stable line's README and docs carry no version pin (`pip install "mcp[cli]"`
35+
installs the newest stable release), so a stable release needs no pin-flip
36+
commit. `README.md` at the tagged commit is the PyPI long description, so any
37+
README fix has to merge before the tag.
38+
39+
1. Check the full test matrix is green on the release commit. The publish
40+
workflow re-runs the same checks and blocks publishing until they pass, so a
41+
red leg there means re-running the failed jobs on the Publishing run — but
42+
verify green before creating the release rather than discovering red after
43+
the tag exists.
44+
2. Freeze `main` from that commit until the tag exists: the release is created
45+
with an explicit `--target`, and nothing else should land in between.
46+
3. Create the release NOT as a pre-release, passing the verified commit as
47+
`--target` (otherwise the tag is created from whatever `main`'s HEAD is by
48+
then). It becomes GitHub "Latest", and PyPI's default `pip install mcp`
49+
version moves to it. The tagged commit determines everything about the
4850
release — the workflows that run and the package metadata (readme,
4951
classifiers) that gets published — so it must contain the current release
5052
tooling, not just pass tests. `--target` is ignored if the tag already
5153
exists: when re-creating a release, delete the old tag first and
52-
double-check where the new tag points. The pre-release flag keeps GitHub's
53-
"Latest" badge and `/releases/latest` pointing at the stable v1.x line:
54+
double-check where the new tag points.
55+
56+
```shell
57+
gh release create v2.X.Y --title v2.X.Y --target <commit-sha> --notes-file <notes.md>
58+
```
59+
60+
4. Curate the release notes above the auto-generated `## What's Changed` list:
61+
the highlights, anything known-incomplete, and links to the docs and
62+
migration guide. Use absolute URLs (relative links don't resolve in GitHub
63+
release bodies), and set the generated list's **Previous tag** to the
64+
previous release on this line by hand — the auto-picked baseline is the
65+
newest tag, which may sit on the other line.
66+
5. If a stable release turns out to be broken, yank it on PyPI and release the
67+
fix as the next patch version. Never delete a release from PyPI — version
68+
numbers cannot be reused. Yank `mcp` and `mcp-types` together (they are one
69+
release), and set the yank reason and the GitHub release notes to point at
70+
the replacement version, since yanking doesn't stop `==` pins from installing
71+
the broken version.
72+
73+
## Maintenance release from `v1.x` (`v1.28.Z`)
74+
75+
Land the `[v1.x]`-prefixed backport PRs (and any README banner update, which is
76+
the README PyPI shows for that version), verify the branch tip green, then
77+
create the release the same way with two differences:
78+
79+
- **The tag's target is the `v1.x` branch.** The UI and CLI default the target
80+
to `main`, which is the v2 codebase — a v1 tag created there would publish v2
81+
code as a v1 stable release.
82+
- **It must not take "Latest" back from the 2.x line.** The UI ticks "Set as
83+
the latest release" by default for the newest non-pre-release; untick it, or
84+
pass `--latest=false`, and afterwards confirm `/releases/latest` still names
85+
the newest v2 tag. If it slipped, `gh release edit v1.28.Z --latest=false`
86+
fixes it — release metadata only, no re-cut.
87+
88+
```shell
89+
gh release create v1.28.Z --title v1.28.Z --target v1.x --latest=false --notes-file <notes.md>
90+
```
91+
92+
When generating notes, set **Previous tag** to the previous `v1.*` release by
93+
hand for the same reason as above. Then ask someone to review the release.
94+
95+
## Pre-releases from `main`
96+
97+
Pre-releases of the next version are cut from `main` with a PEP 440
98+
pre-release tag: `aN` for alphas, later `bN`/`rcN` for betas and release
99+
candidates. The PEP 440 suffix is what keeps `pip install mcp` on the stable
100+
version — installers only select a pre-release when it is requested by exact
101+
pin.
102+
103+
1. During a pre-release phase the README and docs pin the exact pre-release
104+
version, so update those examples first (grep the outgoing version — the
105+
pins live in the README Installation section, `docs/index.md`,
106+
`docs/get-started/installation.md`, and `docs/get-started/real-host.md`) so
107+
the tagged commit — and therefore the README PyPI publishes — names the
108+
version being released. When entering a new phase (alpha → beta → rc →
109+
stable), update the banner wording too; the stable phase drops the pins.
110+
2. Check the full test matrix is green on the release commit, as above.
111+
3. Create the release as a pre-release, passing the verified commit as
112+
`--target`. The pre-release flag keeps GitHub's "Latest" badge and
113+
`/releases/latest` on the newest stable release:
54114

55115
```shell
56-
gh release create v2.0.0aN --prerelease --title v2.0.0aN --target <commit-sha>
116+
gh release create v2.X.YbN --prerelease --title v2.X.YbN --target <commit-sha>
57117
```
58118

59-
4. Curate the release notes instead of relying on auto-generated ones: what
60-
changed since the previous pre-release, what is known-incomplete, the
61-
install line (`pip install mcp==2.0.0aN`), and a link to the migration
62-
guide. Use the absolute URL
63-
(`https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/migration.md`)
64-
because relative links don't resolve in GitHub release bodies.
119+
4. Curate the release notes: what changed since the previous pre-release, what
120+
is known-incomplete, the install line (`pip install mcp==2.X.YbN`), and a
121+
link to the migration guide, with absolute URLs.
65122
5. If a pre-release turns out to be broken, yank it on PyPI and cut the next
66-
one. Never delete a release from PyPI — version numbers cannot be reused.
67-
Yanking doesn't stop `==` pins from installing the broken version, so set
68-
the yank reason (and edit the GitHub release notes) to point at the
123+
one, pointing the yank reason and the GitHub release notes at the
69124
replacement version.

0 commit comments

Comments
 (0)