Cutting a release
A release is a tag on the branch that owns the line: <branch> below is main for
the current major and release/3.x for 3.x maintenance. Every step runs against that
branch, including the docs PR, so a maintenance entry lands in the maintenance
branch's own changelog above its newest 3.x entry. The tag triggers Publish fastmcp-slim to PyPI;
its success fans out to fastmcp-tasks, fastmcp-remote, and fastmcp, and the
fastmcp publish job opens the published-docs PR that makes gofastmcp.com reflect
the release. Two hand-maintained docs files mirror every release and must be on the
tagged commit, so the docs PR always lands first. PyPI releases are immutable; a bad
one gets a follow-up patch, never a re-tag.
Procedure
Preview what's in it.
git fetch origin && git log v<prev>..origin/<branch> --format='%h %s'
gh api -X POST repos/PrefectHQ/fastmcp/releases/generate-notes \
-f tag_name=v<new> -f target_commitish=<branch> -f previous_tag_name=v<prev> --jq .body
Read the two most recent releases for voice: gh release list -L 5, then
gh release view <tag> on each.
Propose titles. Titles are v<version>: <pun>, pun on the release's main
theme from the release preview. Check prior titles with
gh release list --repo PrefectHQ/fastmcp --limit 60 to avoid repeats and keep
the options on brand: two to four words, a familiar phrase bent toward the
theme ("Cache Me If You Can", "Trust, but Proxy", "Come Back Any Time"),
and a running family for a major's prereleases ("Fourst Contact", "Group
Effourt"). Offer several; the maintainer picks. Draft the handwritten notes at the
same time: one or two sentences for a patch, narrative prose for a point release.
Get sign-off on both before anything is pushed.
Write the notes file outside the repository (a temp directory), intro only.
The release title is the heading, so the file has none.
Docs entries. Branch docs/changelog-<new> from origin/<branch> and insert
both blocks (the last argument is the branch the notes are generated against):
uv run .claude/skills/release/scripts/changelog_entry.py v<new> v<prev> "<pun>" /tmp/notes.md <branch>
Validate, and repeat until it reports no errors; a parse error names the file
and line:col:
cd docs && npx --yes mint@latest broken-links
(@latest on purpose: the hosted Mintlify build is always current.) Commit,
push, open the PR titled docs: add v<new> changelog entries with base
<branch> and a one-line body, and merge it. Branch policy needs --admin on a maintainer-driven release.
The docs PR itself appears in the GitHub notes but not in the mirror; that is
expected.
Tag, immediately after the docs PR merges. Re-run the step 1 preview first;
if <branch> moved, delete the entry blocks from both docs files, re-run the
helper (it refuses to insert a label that already exists), and land that as a
follow-up docs PR before tagging.
gh release create v<new> --target <branch> --title "v<new>: <pun>" \
--generate-notes --notes-start-tag v<prev> --notes-file /tmp/notes.md
v<prev> is the last stable tag on that branch. The compare link at the bottom of the created release must read v<prev>...v<new>.
Watch the fan-out. The slim run is keyed to the tag; the others are
workflow_run events on the default branch, so select them by workflow name.
The slim run appears within a minute of the tag; the fastmcp run is queued the
moment slim succeeds, so watch slim to completion first and the newest fastmcp
run is the right one.
gh run watch $(gh run list --event release --branch v<new> --limit 1 --json databaseId --jq '.[0].databaseId') --exit-status
gh run watch $(gh run list --workflow "Publish fastmcp to PyPI" --limit 1 --json databaseId --jq '.[0].databaseId') --exit-status
Confirm each package with curl -fsS https://pypi.org/pypi/<pkg>/<new>/json | jq .info.version;
the unversioned endpoint lags a few minutes.
Publish docs (main releases only; maintenance releases skip this step and
leave gofastmcp.com on the current major). When the fastmcp run finishes, its last job has opened
Publish FastMCP v<new> docs against published-docs:
gh pr list --base published-docs --state open --json number,title
gh pr merge <n> --merge --admin
Merging pushes published-docs, which runs the Deploy docs workflow: it asks
Mintlify's admin API for a deployment of the tip and waits for the verdict.
That run is the signal that the site changed; the merge alone proves nothing.
gh run watch $(gh run list --workflow "Deploy docs" --branch published-docs --limit 1 --json databaseId --jq '.[0].databaseId') --exit-status
A red run prints Mintlify's summary and logs; fix the cause on main and
publish again. If Mintlify's queue is backed up (status.mintlify.com), the run
waits up to 40 minutes; re-run it with
gh workflow run deploy-docs.yml --ref published-docs once their incident clears.
Verify through the markdown endpoints, which bypass the HTML edge cache:
curl -fsS https://gofastmcp.com/changelog.md | grep -c "<pun>", and the same
for updates.md and any page the release touched.
Publishing docs by hand
Used when a deploy failed and was fixed on main, or for a docs change outside a
release. The publication commit must match main's tree exactly.
git fetch origin main published-docs
git checkout -b publish-docs-<topic> origin/published-docs
git read-tree -m -u origin/main && git commit -m "docs: publish main @ $(git rev-parse --short origin/main)"
git diff --quiet HEAD origin/main && echo "tree matches main"
git push -u origin publish-docs-<topic>
gh pr create --base published-docs --title "docs: publish main to published-docs (<what>)" --body "<one line>"
gh pr merge <n> --merge --admin
Then watch the Deploy docs run and verify the endpoints as in step 7.
Template: notes file for a patch
`ClientGroup` now reference-counts its context the way `Client` does, so entering a
connected group from a nested block or a concurrent task reuses the existing
connections instead of raising.
Gotchas
--generate-notes copies PR titles verbatim; a title containing <1, {, or }
breaks MDX. scripts/changelog_entry.py wraps those in backticks; the validator
in step 4 catches anything it misses.
- Mintlify's GitHub App also deploys on its own, but it only rebuilds files
changed since the last commit it recorded, and it records commits it failed on
or skipped. Only the
Deploy docs run's verdict counts; its API-triggered
deployment is what brings a page a skipped deploy left stale back in line.
- Without
--notes-start-tag, a prerelease tag becomes the changelog start and the
PR list is silently truncated.
- Maintenance releases publish packages and notes but never repoint
published-docs;
their changelog entries go on the maintenance branch under the matching major section.
Check before finishing
Each of these prints a count above zero:
curl -fsS https://pypi.org/pypi/fastmcp/<new>/json | grep -c '"version"',
curl -fsS https://gofastmcp.com/changelog.md | grep -c "<pun>",
curl -fsS https://gofastmcp.com/updates.md | grep -c "<pun>".
1---2name: release3description: Cut a FastMCP release end to end. Use when the maintainer says "cut a release", "prep a patch", "ship 4.x.y", or asks what a release would contain. Covers the notes preview, the title pun, the docs changelog PR that must land before the tag, the tag itself, the publish fan-out, and verifying gofastmcp.com actually deployed.4---56# Cutting a release78A release is a tag on the branch that owns the line: `<branch>` below is `main` for9the current major and `release/3.x` for 3.x maintenance. Every step runs against that10branch, including the docs PR, so a maintenance entry lands in the maintenance11branch's own changelog above its newest 3.x entry. The tag triggers `Publish fastmcp-slim to PyPI`;12its success fans out to `fastmcp-tasks`, `fastmcp-remote`, and `fastmcp`, and the13`fastmcp` publish job opens the `published-docs` PR that makes gofastmcp.com reflect14the release. Two hand-maintained docs files mirror every release and must be on the15tagged commit, so the docs PR always lands first. PyPI releases are immutable; a bad16one gets a follow-up patch, never a re-tag.1718## Procedure19201. **Preview what's in it.**2122 ```bash23 git fetch origin && git log v<prev>..origin/<branch> --format='%h %s'24 gh api -X POST repos/PrefectHQ/fastmcp/releases/generate-notes \25 -f tag_name=v<new> -f target_commitish=<branch> -f previous_tag_name=v<prev> --jq .body26 ```2728 Read the two most recent releases for voice: `gh release list -L 5`, then29 `gh release view <tag>` on each.30312. **Propose titles.** Titles are `v<version>: <pun>`, pun on the release's main32 theme from the release preview. Check prior titles with33 `gh release list --repo PrefectHQ/fastmcp --limit 60` to avoid repeats and keep34 the options on brand: two to four words, a familiar phrase bent toward the35 theme ("Cache Me If You Can", "Trust, but Proxy", "Come Back Any Time"),36 and a running family for a major's prereleases ("Fourst Contact", "Group37 Effourt"). Offer several; the maintainer picks. Draft the handwritten notes at the38 same time: one or two sentences for a patch, narrative prose for a point release.39 Get sign-off on both before anything is pushed.40413. **Write the notes file** outside the repository (a temp directory), intro only.42 The release title is the heading, so the file has none.43444. **Docs entries.** Branch `docs/changelog-<new>` from `origin/<branch>` and insert45 both blocks (the last argument is the branch the notes are generated against):4647 ```bash48 uv run .claude/skills/release/scripts/changelog_entry.py v<new> v<prev> "<pun>" /tmp/notes.md <branch>49 ```5051 Validate, and repeat until it reports no errors; a parse error names the file52 and line:col:5354 ```bash55 cd docs && npx --yes mint@latest broken-links56 ```5758 (`@latest` on purpose: the hosted Mintlify build is always current.) Commit,59 push, open the PR titled `docs: add v<new> changelog entries` with base60 `<branch>` and a one-line body, and merge it. Branch policy needs `--admin` on a maintainer-driven release.61 The docs PR itself appears in the GitHub notes but not in the mirror; that is62 expected.63645. **Tag**, immediately after the docs PR merges. Re-run the step 1 preview first;65 if `<branch>` moved, delete the entry blocks from both docs files, re-run the66 helper (it refuses to insert a label that already exists), and land that as a67 follow-up docs PR before tagging.6869 ```bash70 gh release create v<new> --target <branch> --title "v<new>: <pun>" \71 --generate-notes --notes-start-tag v<prev> --notes-file /tmp/notes.md72 ```7374 `v<prev>` is the last stable tag on that branch. The compare link at the bottom of the created release must read `v<prev>...v<new>`.75766. **Watch the fan-out.** The slim run is keyed to the tag; the others are77 `workflow_run` events on the default branch, so select them by workflow name.78 The slim run appears within a minute of the tag; the `fastmcp` run is queued the79 moment slim succeeds, so watch slim to completion first and the newest `fastmcp`80 run is the right one.8182 ```bash83 gh run watch $(gh run list --event release --branch v<new> --limit 1 --json databaseId --jq '.[0].databaseId') --exit-status84 gh run watch $(gh run list --workflow "Publish fastmcp to PyPI" --limit 1 --json databaseId --jq '.[0].databaseId') --exit-status85 ```8687 Confirm each package with `curl -fsS https://pypi.org/pypi/<pkg>/<new>/json | jq .info.version`;88 the unversioned endpoint lags a few minutes.89907. **Publish docs** (`main` releases only; maintenance releases skip this step and91 leave gofastmcp.com on the current major). When the `fastmcp` run finishes, its last job has opened92 `Publish FastMCP v<new> docs` against `published-docs`:9394 ```bash95 gh pr list --base published-docs --state open --json number,title96 gh pr merge <n> --merge --admin97 ```9899 Merging pushes `published-docs`, which runs the `Deploy docs` workflow: it asks100 Mintlify's admin API for a deployment of the tip and waits for the verdict.101 That run is the signal that the site changed; the merge alone proves nothing.102103 ```bash104 gh run watch $(gh run list --workflow "Deploy docs" --branch published-docs --limit 1 --json databaseId --jq '.[0].databaseId') --exit-status105 ```106107 A red run prints Mintlify's summary and logs; fix the cause on `main` and108 publish again. If Mintlify's queue is backed up (status.mintlify.com), the run109 waits up to 40 minutes; re-run it with110 `gh workflow run deploy-docs.yml --ref published-docs` once their incident clears.111112 Verify through the markdown endpoints, which bypass the HTML edge cache:113 `curl -fsS https://gofastmcp.com/changelog.md | grep -c "<pun>"`, and the same114 for `updates.md` and any page the release touched.115116## Publishing docs by hand117118Used when a deploy failed and was fixed on `main`, or for a docs change outside a119release. The publication commit must match `main`'s tree exactly.120121```bash122git fetch origin main published-docs123git checkout -b publish-docs-<topic> origin/published-docs124git read-tree -m -u origin/main && git commit -m "docs: publish main @ $(git rev-parse --short origin/main)"125git diff --quiet HEAD origin/main && echo "tree matches main"126git push -u origin publish-docs-<topic>127gh pr create --base published-docs --title "docs: publish main to published-docs (<what>)" --body "<one line>"128gh pr merge <n> --merge --admin129```130131Then watch the `Deploy docs` run and verify the endpoints as in step 7.132133## Template: notes file for a patch134135```136`ClientGroup` now reference-counts its context the way `Client` does, so entering a137connected group from a nested block or a concurrent task reuses the existing138connections instead of raising.139```140141## Gotchas142143- `--generate-notes` copies PR titles verbatim; a title containing `<1`, `{`, or `}`144 breaks MDX. `scripts/changelog_entry.py` wraps those in backticks; the validator145 in step 4 catches anything it misses.146- Mintlify's GitHub App also deploys on its own, but it only rebuilds files147 changed since the last commit it recorded, and it records commits it failed on148 or skipped. Only the `Deploy docs` run's verdict counts; its API-triggered149 deployment is what brings a page a skipped deploy left stale back in line.150- Without `--notes-start-tag`, a prerelease tag becomes the changelog start and the151 PR list is silently truncated.152- Maintenance releases publish packages and notes but never repoint `published-docs`;153 their changelog entries go on the maintenance branch under the matching major section.154155## Check before finishing156157Each of these prints a count above zero:158`curl -fsS https://pypi.org/pypi/fastmcp/<new>/json | grep -c '"version"'`,159`curl -fsS https://gofastmcp.com/changelog.md | grep -c "<pun>"`,160`curl -fsS https://gofastmcp.com/updates.md | grep -c "<pun>"`.