Skip to content

Document the two-line release process for stable v2 - #3179

Merged
maxisbey merged 2 commits into
mainfrom
release-md-stable
Jul 28, 2026
Merged

Document the two-line release process for stable v2#3179
maxisbey merged 2 commits into
mainfrom
release-md-stable

Conversation

@maxisbey

Copy link
Copy Markdown
Contributor

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.

Motivation and Context

The person cutting v2.0.0 (and every 2.x.y and 1.28.z after it) reads this file, so it needs to describe the world after the flip: a v2.X.Y stable release from main that takes GitHub "Latest" and PyPI's default pip install mcp, a v1.28.Z maintenance release from v1.x that must be created with --latest=false so it does not take "Latest" back, and pre-releases from main. It also records that mcp and mcp-types are yanked together and that a generated release body's previous-tag baseline must be set to the same line by hand.

How Has This Been Tested?

Docs-only.

Breaking Changes

None.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

AI disclosure

AI assistance was used to draft this change; I reviewed the result and take responsibility for it.

AI Disclaimer

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="RELEASE.md">

<violation number="1" location="RELEASE.md:14">
P1: A `v1.x` release will use the default-branch release workflow, not a workflow definition from `v1.x`; current `publish-pypi.yml` therefore attempts to build `mcp-types` too. 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.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread RELEASE.md

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: A v1.x release will use the default-branch release workflow, not a workflow definition from v1.x; current publish-pypi.yml therefore attempts to build mcp-types too. 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
Check if this issue is valid — if so, understand the root cause and fix it. At RELEASE.md, line 14:

<comment>A `v1.x` release will use the default-branch release workflow, not a workflow definition from `v1.x`; current `publish-pypi.yml` therefore attempts to build `mcp-types` too. 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.</comment>

<file context>
@@ -7,63 +7,118 @@
+
+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
</file context>

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline findings, two other candidates were examined and ruled out this run: (1) step 4's mention of the auto-generated "What's Changed" list vs. step 3's --notes-file command — not a contradiction, since the generated list can be added via the UI or --generate-notes and curated above; (2) the maintenance command's --target v1.x branch ref recreating the race the stable section warns about — the procedure verifies the branch tip green immediately before creating the release, so it holds.

Extended reasoning...

Docs-only PR rewriting RELEASE.md for the two-line (v2 stable from main, v1.x maintenance) release process. Two bugs were confirmed and posted inline (the missing first-stable pin-drop note, and the over-narrow pre-release-selection claim); this note only records the additional candidates that were investigated and refuted, so a later pass doesn't re-explore them from scratch. It is informational, not a correctness guarantee.

Comment thread RELEASE.md Outdated
Comment thread RELEASE.md Outdated
@maxisbey
maxisbey force-pushed the release-md-stable branch from cc5f6a6 to 025f77a Compare July 27, 2026 17:51

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline findings, I also checked the earlier P1 concern that a v1.x tag would run main's publish workflow and build mcp-types — it does not hold: release: published workflows run from the tagged commit, and the v1.x branch carries its own publish-pypi.yml that builds mcp only (plain uv build), so the doc's "from the tagged commit" description is accurate. That same v1.x workflow lacks skip-existing: true, which is what the inline note on the re-run advice refers to.

Extended reasoning...

Docs-only PR. This run's five inline findings are all nits, but my previous run's normal-severity finding (the stable section's "needs no pin-flip commit" premise being false for the v2.0.0 cut) is still unaddressed with no new commits since, so I am not approving. I verified the one standing reviewer concern not covered by the inline comments: cubic-dev-ai's P1 claiming a v1.x release would use main's workflow definition. Comparing .github/workflows/publish-pypi.yml on main (dual-package build, skip-existing: true) against the v1.x branch's copy (single-package uv build, no skip-existing) and GitHub's release-event semantics (the workflow runs from the tag's commit) shows the doc is correct and the P1 is not — recording that here so it is not re-litigated.

Comment thread RELEASE.md
Comment on lines +66 to +71
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The "ask someone to review the release" step now survives only in the lower-stakes v1.x maintenance section (line 93), while the new stable-from-main procedure — the one that takes GitHub "Latest" and moves PyPI's default pip install mcp, with mistakes fixable only by yank-plus-new-patch per step 5 — has no review step in any of its 5 steps. Consider adding the same sentence at the end of this section so the second-pair-of-eyes check covers the highest-stakes flow too.

Extended reasoning...

What happened. The pre-PR RELEASE.md ended its stable-release procedure ("Major or Minor Release") with "Then ask someone to review the release." After this rewrite, that sentence exists exactly once — at line 93, closing the Maintenance release from v1.x section. The new Stable release from main (v2.X.Y) section (lines 31–71) has no review step in any of its 5 steps.

Why this looks like an oversight, not a decision. One could argue the sentence simply stayed attached to the section it originally belonged to: the old stable procedure was the v1.x procedure, and its direct textual successor is the new maintenance section. That's mechanically true — but the review requirement was attached to the stable role, not to the v1.x branch as such: it closed the procedure for the release that takes GitHub "Latest" and PyPI's default install. After the flip, that role belongs to the v2-from-main cut, yet the safeguard didn't move with it. Two details in the new doc itself support the oversight reading: (1) the maintenance section frames itself as "create the release the same way with two differences" — and dropping the review step is not one of the listed differences, so the asymmetry isn't presented as intentional anywhere; (2) nothing in the PR description mentions removing the review requirement for stable releases.

Why it matters. The stable section's own step 5 explains why a pre-publish second pair of eyes is most valuable exactly here: never delete from PyPI, version numbers cannot be reused, and the only remedy for a broken stable release is yanking mcp and mcp-types together plus cutting the next patch. The failure modes the doc warns about — tagging the wrong commit (--target ignored when the tag exists), a stale README becoming the immutable PyPI long description, missing release tooling at the tagged commit — are all things a reviewer can catch before gh release create runs. The automated safeguards the new section adds (verify green, freeze main, explicit --target, publish workflow re-runs checks) all apply to both lines via the shared release environment, so none of them substitutes asymmetrically for the human review that now only the maintenance line gets.

Concrete walk-through. A maintainer cutting v2.1.0 reads only the stable section (it's named for their task). They follow steps 1–5: verify green, freeze main, run gh release create v2.1.0 --title v2.1.0 --target <sha> --notes-file notes.md, curate notes, done — no step tells them to have anyone look before or after publishing. Meanwhile a maintainer cutting v1.28.5 from the maintenance line is explicitly told "Then ask someone to review the release." The doc now requires more scrutiny for the release that can't even take "Latest" than for the one that moves pip install mcp for everyone.

Fix. Add "Then ask someone to review the release." (or an explicit review-before-publish step) at the end of the stable section, restoring parity with both the old doc's requirement and the maintenance section.

Severity. Nit: this is a docs-process omission — no code or automation is affected, and whether a v2 stable cut requires a second reviewer is ultimately a maintainer policy choice. It's a one-sentence fix that shouldn't block merge.

Comment thread RELEASE.md Outdated
Comment thread RELEASE.md Outdated
Comment thread RELEASE.md Outdated
Comment thread RELEASE.md Outdated
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
@maxisbey
maxisbey force-pushed the release-md-stable branch from 025f77a to 8ba2305 Compare July 28, 2026 11:21

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline findings, this pass also examined and ruled out three candidate concerns: (1) the yank-then-patch ordering in stable step 5 briefly resolving unpinned installs to the prior stable — inherent to any PyPI yank, and the doc already points the yank reason at the replacement; (2) the maintenance section's unconditional --latest=false being wrong before v2.0.0 exists — the doc explicitly describes the post-flip world, where it is always correct; (3) a supposed contradiction between pre-release step 1's pin policy and the stable section's no-pin premise — consistent, since the pins exist only during pre-release phases and step 1 says the stable phase drops them.

Extended reasoning...

Docs-only PR, third review pass. Two new findings were posted inline this run; per process, the ruled-out candidates are recorded here so a later review pass or the author can see they were already examined. This is informational only — not a claim of overall correctness and not an instruction to skip anything in future review.

Comment thread RELEASE.md
Comment on lines +82 to +86
- **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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The maintenance section's post-release check — "confirm /releases/latest still names the newest v2 tag" — is literally unsatisfiable whenever the newest v2 tag is a pre-release (e.g. v2.1.0b1), which is the normal cadence this doc documents: pre-releases are never Latest, so /releases/latest correctly names an older stable while the check reads as failed. One-word fix: "newest v2 tag" → "newest stable v2 release", matching the pre-release section's own wording.

Extended reasoning...

The issue. The Maintenance section (RELEASE.md lines 82–86) instructs, after cutting v1.28.Z with --latest=false: "afterwards confirm /releases/latest still names the newest v2 tag." Taken literally, this verification check fails in the doc's own steady state — and the doc itself explains why.

Why the literal check is unsatisfiable. Two other parts of this same PR establish that /releases/latest can never name a pre-release: the Release lines table says pre-releases are "Pre-release ticked, never Latest", and pre-release step 3 says "The pre-release flag keeps GitHub's 'Latest' badge and /releases/latest on the newest stable release." Meanwhile, the doc's documented cadence ("Pre-releases of the next version are cut from main") means the newest v2 tag is routinely a pre-release tag between stables. So the newest v2 tag and the tag /releases/latest names are, by design, often different tags.

Step-by-step proof. (1) v2.0.0 is released stable from main and takes Latest. (2) v2.1.0b1 is tagged as a pre-release, per the Pre-releases section — /releases/latest correctly stays on v2.0.0. (3) A release manager cuts v1.28.Z per the Maintenance section, passing --latest=false as instructed. (4) They run the prescribed confirmation: /releases/latest returns v2.0.0. (5) The newest v2 tag, however, is v2.1.0b1 — so the literal check "still names the newest v2 tag" is not satisfied, even though every release flag is correct.

Why the offered remedy makes it worse. The next sentence says "If it slipped, gh release edit v1.28.Z --latest=false fixes it." In the scenario above, nothing slipped — --latest=false was already passed and the edit changes nothing. Nor is there any command that could make the literal check pass: GitHub refuses to mark a pre-release as Latest. The reader is left with a verification step that reports failure and a remedy that is a no-op.

Why this is an internal inconsistency, not a pedantic reading. The same document uses the precise wording where it matters: the pre-release section says the flag keeps /releases/latest "on the newest stable release", and step 4 of the stable section even warns that "the auto-picked baseline is the newest tag, which may sit on the other line" — the author clearly distinguishes tags from Latest-eligible releases elsewhere. The Maintenance check is the one place the distinction was dropped.

Impact and fix. Impact is limited to release-time confusion: the bolded rule ("must not take Latest back from the 2.x line") makes the intent recoverable — a reader seeing any stable v2 release at /releases/latest would likely move on. But a runbook verification step whose literal success condition is false in the documented normal state is a genuine wording defect. One-word fix: change "the newest v2 tag" to "the newest stable v2 release", matching the pre-release section's own phrasing.

Comment thread RELEASE.md
Comment on lines 7 to 8
`[tool.hatch.metadata.hooks.uv-dynamic-versioning].dependencies`.
2. Upgrade lock with `uv lock --resolution lowest-direct`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟣 Pre-existing issue (this line is unchanged by the PR): step 2 of "Bumping Dependencies" says "Upgrade lock with uv lock --resolution lowest-direct", but that command doesn't upgrade anything — it re-resolves every direct dependency down to its declared floor, mass-downgrading the committed uv.lock (pydantic 2.12.5 → 2.12.0, typer 0.17.4 → 0.16.0, …) and persisting resolution-mode = "lowest-direct" so later plain uv lock runs silently stay in that mode. Since this PR's purpose is making RELEASE.md the accurate operative runbook, consider fixing the one carried-over stale line too: uv lock or uv lock --upgrade-package <package> (the procedure AGENTS.md already documents).

Extended reasoning...

The bug. The "Bumping Dependencies" section (RELEASE.md lines 7-8) survives this rewrite unchanged, and its step 2 — "Upgrade lock with uv lock --resolution lowest-direct" — is wrong in a way that would actively damage the repo if followed. --resolution lowest-direct tells uv to resolve every direct dependency in the workspace to the lowest version satisfying its specifier, i.e. its declared floor. Running it doesn't upgrade the lock after a floor bump; it rewrites uv.lock as a mass downgrade of everything else.

Evidence the committed lock was never produced this way. The current uv.lock is a standard highest-resolution lock: (1) it contains no [options] section / resolution-mode entry, which uv writes whenever a non-default --resolution is used; (2) locked versions sit above their floors — pydantic 2.12.5 vs >=2.12.0 (pyproject.toml:129), typer 0.17.4 vs >=0.16.0 (pyproject.toml:29), anyio 4.10.0. A lowest-direct resolution would pin all of these at their floors. So the documented command has never actually been used to produce the committed lock, and running it would produce a huge unintended diff.

Step-by-step proof of the failure. (1) A maintainer bumps one dependency floor per step 1, e.g. raises httpx to >=0.28. (2) Following step 2 verbatim, they run uv lock --resolution lowest-direct. (3) uv re-resolves the whole workspace in lowest-direct mode: typer drops 0.17.4 → 0.16.0, pydantic 2.12.5 → 2.12.0, and every other direct dependency falls to its floor. (4) uv also records resolution-mode = "lowest-direct" in the lockfile's [options], so every subsequent plain uv lock silently stays in lowest-direct mode. (5) Once committed, CI's "locked" matrix leg (which installs --frozen from uv.lock) and every developer environment run against floor versions — while the dedicated lowest-direct leg (.github/workflows/shared.yml:64-68, which applies --upgrade --resolution lowest-direct at install time precisely so the committed lock stays highest-resolution) becomes redundant. The repo loses its highest-resolution test coverage entirely.

Why nothing else prevents it. RELEASE.md is the operative runbook a maintainer follows verbatim, and the instruction is internally self-contradictory only on close reading (a command labeled "Upgrade lock" performs a downgrade). It also directly contradicts the repo's own AGENTS.md, which documents the correct procedure: "Upgrading: uv lock --upgrade-package <package>" and "Always pass --frozen so uv doesn't rewrite uv.lock as a side effect."

Why flag it on this PR despite being untouched. All four verifiers confirmed the line predates this PR (it dates to when RELEASE.md was created) and sits as unchanged context above the rewrite — hence pre-existing, not blocking. But the PR's stated purpose is making RELEASE.md accurate for the person actually cutting releases, and this is the one remaining section carried over unreviewed. Fixing it is a one-line change.

Fix. Change step 2 to "Upgrade lock with uv lock --upgrade-package <package>" (matching AGENTS.md), or simply uv lock if the intent is a plain re-lock after editing the floor.

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

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="RELEASE.md">

<violation number="1" location="RELEASE.md:107">
P3: Dependency resolution can select a pre-release without an exact pin, pre-release-bearing specifier, or `--pre` when no final version satisfies its constraint. Qualify this as the normal/default behavior so release guidance does not promise an exclusion PEP 440 does not guarantee.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread RELEASE.md
Comment on lines +107 to +108
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`).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Dependency resolution can select a pre-release without an exact pin, pre-release-bearing specifier, or --pre when no final version satisfies its constraint. Qualify this as the normal/default behavior so release guidance does not promise an exclusion PEP 440 does not guarantee.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At RELEASE.md, line 107:

<comment>Dependency resolution can select a pre-release without an exact pin, pre-release-bearing specifier, or `--pre` when no final version satisfies its constraint. Qualify this as the normal/default behavior so release guidance does not promise an exclusion PEP 440 does not guarantee.</comment>

<file context>
@@ -97,8 +104,8 @@ hand for the same reason as above. Then ask someone to review the 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`).
 
</file context>
Suggested change
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`).
version — installers normally exclude pre-releases, so `pip install mcp` stays
on the stable version.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good — docs-only rewrite of RELEASE.md, and the latest commit addressed the earlier round of inline feedback (scoped the skip-existing advice, pinned the v1.x target to a verified SHA, spelled out notes generation, dual-package yank for pre-releases).

What was reviewed on this revision: the release-lines table and workflow-from-tagged-commit claims against the actual publish setup; the stable/maintenance/pre-release runbooks for internal consistency (freeze window, --target, --latest=false, yank procedure); and three candidate issues raised this run were investigated and ruled out (pre-release step 1 in the steady state, tag re-creation vs skip-existing, and the maintenance-line yank wording). The two remaining inline comments are a one-word wording nit and a pre-existing line outside this diff — neither blocks.

Extended reasoning...

Overview

This PR touches a single file, RELEASE.md, restructuring it from a v1.x-stable / v2-prerelease world into three runbooks (stable from main, maintenance from v1.x, pre-releases from main) plus a shared "Release lines" section. It is purely documentation — no code, workflows, or configuration change. The PR now carries two commits: the original rewrite and a follow-up ("Address review nits in the release process doc") that resolved the substantive points from earlier review rounds, including the moving-HEAD race in the v1.x command, the over-generalized skip-existing recovery advice, the pre-release yank scope, and the ambiguity around how the What's Changed list is generated.

Security risks

None from the change itself — no executable code or CI configuration is modified. The residual risk is indirect: a release runbook that misstates procedure could lead a maintainer to publish incorrectly. Three rounds of review examined exactly that class of issue; the findings that mattered (wrong-commit tagging, incomplete yank, inapplicable recovery advice) have been fixed in the current revision, and this run's bug hunt found nothing new, with three further candidates investigated and refuted.

Level of scrutiny

Docs-only changes warrant modest scrutiny, and this one has received considerably more than that: the runbook's claims were cross-checked against the actual publish workflows on both branches across multiple review passes. The remaining open inline comments are a one-word wording nit ("newest v2 tag" vs "newest stable v2 release" in a post-release verification step) and a pre-existing uv lock --resolution lowest-direct line that this PR does not touch. Neither can cause a bad release — the first is a confusing-but-recoverable check, the second predates the PR — so they do not justify holding a documentation change for human review.

Other factors

The author has been responsive: the follow-up commit addressed the earlier feedback point-by-point. The document's policy content (v2 stable from main, --latest=false for v1.x maintenance releases, yank-together rule) records decisions already reflected in the repository's branching model rather than making new ones. Approving here closes the loop on a PR whose remaining feedback is entirely cosmetic and already visible as inline comments.

@maxisbey
maxisbey merged commit 68ca87e into main Jul 28, 2026
36 checks passed
@maxisbey
maxisbey deleted the release-md-stable branch July 28, 2026 12:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant