Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions .claude/skills/release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,3 +408,122 @@ Flat: `feature branch → v1/main → npm v1-latest`, with no merge into `main`
any point. The two lines publish independently under separate dist-tags, so a v1
fix does **not** need forward-porting to reach users on
`npx @modelcontextprotocol/inspector@v1-latest`.

None of the v2 machinery above applies: no milestone-merge PR, no ledger, no
`release:notes` script, no `package` job. v1 is a four-package npm workspace
(root, `client/`, `server/`, `cli/`) with its own, older workflow on `v1/main`.
This procedure was reconstructed for 1.0.3 (#2753) from the earlier releases and
that workflow, and the first attempt got the tag and the Latest flag wrong —
both are called out below.

### v1-1. Bump the version on `v1/main`

The bump is an ordinary PR against **`v1/main`**, cut from `origin/v1/main`
(the two lines have unrelated histories), labeled `v1` + `chore`, linked to a
`v1` release issue on board **#11**. That issue takes **no milestone** — every
milestone is a v2 bucket — and board #11 has no Priority field.

Edit the versions **by hand**. Don't use `npm run update-version`: its trailing
bare `npm install` re-resolves the whole tree, turning a version bump into an
uncontrolled dependency update (seen on 1.0.2, #2080). The edits are:

- `version` in the **four** `package.json` files: root, `client/`, `server/`,
`cli/`.
- The **three** `@modelcontextprotocol/inspector-{cli,client,server}` ranges in
the root `package.json` `dependencies` (`^X.Y.Z`).
- The **eight** matching fields in `package-lock.json`: the top-level
`version`, `packages[""].version`, the three ranges under
`packages[""].dependencies`, and `packages["cli"|"client"|"server"].version`.
The `node_modules/@modelcontextprotocol/inspector-*` entries are workspace
links with no version, so they need no edit.

Then prove the edit is complete and touched nothing else:

```sh
npm run check-version # every version field agrees
cp package-lock.json /tmp/lock-before.json
npm ci && cmp package-lock.json /tmp/lock-before.json # silent: npm ci left the lockfile alone
```

Compare the lockfile against itself, not against `git status`: the bump is
still uncommitted here, so the worktree is dirty by design.

### v1-2. Merge order: the bump merges last

Merge every fix the release ships **first**, then the bump, and cut the Release
right after it. That way `v1/main` never carries the new version number without
the fixes it names, and the commit the tag lands on is the bump itself.

When the fixes are security advisories, they merge from private forks, which
have constraints of their own — required checks that can never report, and
an advisory merging all of its PRs at once. Those are in the
`security-advisory` skill, step 5.

### v1-3. Cut the Release

```sh
gh release create X.Y.Z --repo modelcontextprotocol/inspector \
--target v1/main --title X.Y.Z --latest=false --notes-file <notes.md>
```

- ⚠️ **The tag is bare — `1.0.3`, not `v1.0.3`** — like `1.0.0` through
`1.0.2`. The `v1 release tags` ruleset protects `refs/tags/1.*` against
deletion and update; a `v`-prefixed tag would sit outside it, and outside the
sequence every earlier v1 release established.
- ⚠️ **`--latest=false` is required.** Every v1 release has it. GitHub's
"Latest" badge is repo-wide, so a v1 release marked Latest takes the badge
from the current v2 release — and `gh release create` marks a new release
Latest by default.
- `--target v1/main` sets where the tag is created. The tag does not exist yet,
so without it GitHub would tag the default branch, `main`, which is v2.

### v1-4. What CI does

The `release` event runs `v1/main`'s `.github/workflows/main.yml` at the tagged
commit:

- **`build`** first.
- **`publish`** (`needs: build`) — npm **trusted publishing** over OIDC
(`id-token: write`, no npm token), running `npm run publish-all`, which
publishes the three workspaces and then the root, all with
`--tag v1-latest`. That tag is what keeps a v1 publish off `latest`.
- **`publish-github-container-registry`** (`needs: build`) — the GHCR image.

### v1-5. Verify

```sh
npm view @modelcontextprotocol/inspector dist-tags --prefer-online
```

`v1-latest` must show the new version **and `latest` must still show the
current v2 release**. As with v2 (3c), a fresh version can sit in npm's
validation window before `dist-tags` moves, so query the exact version
(`npm view @modelcontextprotocol/inspector@X.Y.Z version --prefer-online`)
before concluding anything failed. Confirm the release still shows
`isLatest: false`:

```sh
gh release list --repo modelcontextprotocol/inspector --limit 100 \
--json tagName,isLatest --jq '.[] | select(.tagName == "X.Y.Z")'
```

(`gh release view` has no `isLatest` field; `gh release list` does.)

### v1-6. Release notes for unpublished advisories

When the release ships fixes for advisories that are **not yet published**,
keep the notes **neutral** at release time: say what changed in behavior, with
no GHSA link and no description of the vulnerability. The Release is public
the moment it is created, and an advisory link there points at a page nobody
outside it can open while the notes themselves describe the bug.

Once the advisories are published (which waits on every affected line having
shipped — `security-advisory`, step 5), **edit the Release** to link each one
and thank its reporters, as 1.0.3's notes now do:

```sh
gh release edit X.Y.Z --repo modelcontextprotocol/inspector --notes-file <notes.md>
```

Re-check `isLatest` afterwards (v1-5): an edit must not hand the Latest badge
to the v1 release.
32 changes: 32 additions & 0 deletions .claude/skills/security-advisory/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,38 @@ publish is not a period of secrecy to protect; it is a period of **exposure to
anyone reading commits**, which is why it should be short. Merge close to the
release rather than early, and publish as soon as the release is out.

#### Merging from the private fork

Three things about the merge itself, all learned on 1.0.3 (#2753):

- ⚠️ **A fork PR can never satisfy a required Actions check.** Private advisory
forks do not run GitHub Actions, so `v2/main`'s required `DCO` check (the
`v2/main - DCO` ruleset) and `v1/main`'s required `build` check (the
`v1/main - full protection` ruleset) stay **"Expected"** forever, and the
advisory page offers no bypass. The workable path is to remove **only** the
`required_status_checks` rule from that ruleset for the merge, then restore
it from a snapshot taken first — never delete the ruleset, which also carries
the deletion and force-push protection:

```sh
gh api repos/modelcontextprotocol/inspector/rulesets/<ID> > ruleset-<ID>.json # snapshot
# PUT the ruleset back with its rules minus required_status_checks, merge,
# then PUT the snapshot's rules back and diff the result against the snapshot.
```

That leaves the fork's commits unchecked, so run the DCO check and the
line's gate on the fix branch yourself before merging. Changing a ruleset is
an admin act on a protected branch: say what you are about to change and get
the maintainer's go-ahead, and restore it in the same sitting.
- ⚠️ **Merging an advisory merges all of its fork's PRs at once.** An advisory
with one PR per affected line (a v2 PR and a v1 PR) lands both together.
Plan any cross-advisory merge order around that: you cannot land one
advisory's v1 fix before another's and its v2 fix after.
- **Use "Create a merge commit"** when several fix branches were chained on top
of each other in the fork. Squash or rebase rewrites the lower branches'
commits, so the branches stacked on them no longer share history with what
landed and their PRs show the lower fixes again, or conflict.

**Do not hand this off to the release skill.** It is `disable-model-invocation:
true`, so a pointer to it from here is a dead end for the model anyway — a
maintainer invokes `/release` themselves. Say which lines need a release and
Expand Down
Loading