|
7 | 7 | `[tool.hatch.metadata.hooks.uv-dynamic-versioning].dependencies`. |
8 | 8 | 2. Upgrade lock with `uv lock --resolution lowest-direct` |
9 | 9 |
|
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 |
48 | 50 | release — the workflows that run and the package metadata (readme, |
49 | 51 | classifiers) that gets published — so it must contain the current release |
50 | 52 | tooling, not just pass tests. `--target` is ignored if the tag already |
51 | 53 | 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: |
54 | 114 |
|
55 | 115 | ```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> |
57 | 117 | ``` |
58 | 118 |
|
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. |
65 | 122 | 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 |
69 | 124 | replacement version. |
0 commit comments