ci(release): 4463 / Phase 1 - Switch npm publishing to OIDC trusted publishing#4466
Conversation
The NPM_TOKEN used by publish-npm is dead (npm classic-token sunset) — both June release attempts failed publishing with a disguised auth error (E404 on the publish PUT) after PyPI legs succeeded. Each @modelcontextprotocol/* package's npm trusted publisher is being bound to this workflow (release.yml) + the release environment, so publish-npm now authenticates via OIDC instead of a token: - add id-token: write permission to publish-npm (required to mint the OIDC token; contents: read added explicitly since setting permissions drops the defaults) - upgrade the npm CLI in the publish job (trusted publishing requires npm >= 11.5.1; Node 22 bundles an older npm) - drop NODE_AUTH_TOKEN from the publish step Also touch each TypeScript server's README (add everything's missing License section; link each package's npm page) so scripts/release.py includes all four packages in the next npm publish matrix — they've been stranded on npm at 2026.1.26 while PyPI advanced to 2026.6.16. See #4463 for the rollout plan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AViME1R9ES2UXT1uQDx1TG
Align with modelcontextprotocol/inspector#1199: - set NPM_CONFIG_PROVENANCE so published packages get provenance attestations - pin the npm upgrade to ^11.5.1 instead of latest Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AViME1R9ES2UXT1uQDx1TG
Gate both publish jobs on the package's test suite: npm test --if-present before the npm build/publish, and pytest (when a tests/ or test/ directory exists, matching python.yml's guard) before the PyPI build/publish. Previously nothing between tagging and publishing ran a single test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AViME1R9ES2UXT1uQDx1TG
|
@claude review |
|
Claude finished @cliffhall's task in 2m 15s —— View job Review: OIDC trusted publishing
The OIDC workflow changes are correct and closely mirror the inspector#1199 precedent. But there's one significant problem: the README touches won't actually do what the PR says they will. 🔴 Blocking: README (
|
…ytest Review findings from the PR: - release.py's has_changes() only counted .py/.ts files, so the README touches meant to enroll the four TS packages in the npm matrix would have been ignored. Include .md — READMEs ship inside the published package (npm tarball / PyPI long_description), so doc changes are release-relevant. - uv run --frozen pytest for consistency with the job's other uv calls. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AViME1R9ES2UXT1uQDx1TG
|
Addressed in f4c86ff:
@claude review |
|
Claude finished @cliffhall's task in 52s —— View job Re-review of
|
|
Acknowledging the sign-off note: yes, the |
Describes how packages publish (release.yml + release environment, trusted publishing on npm and PyPI, no registry tokens, provenance), what a release run does, and how to recover from a failed publish leg (rerun --failed within the window, or ride the next release). Linked from the root README alongside the other maintainer docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AViME1R9ES2UXT1uQDx1TG
…anned Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AViME1R9ES2UXT1uQDx1TG
Rewrite RELEASING.md as the reference for the target process rather than a snapshot of the interim state: semver via changesets for TypeScript / CalVer for Python, versions changed only by PRs on main, publishing triggered by creating a GitHub Release, per-package registry-diff guard, OIDC trusted publishing with no registry tokens, and the failed-publish retry runbook. Implementation tracked in #4463. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AViME1R9ES2UXT1uQDx1TG
RELEASING.md documents the pipeline this PR ships (scheduled/dispatch CalVer releases, OIDC publishing, retry runbook), with a footer noting the planned Phase 2 changes tracked in #4463. The doc gets updated when that work merges, not before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AViME1R9ES2UXT1uQDx1TG
|
@claude review — focus on RELEASING.md: verify every claim in it matches what this branch actually implements once merged (triggers, change detection, versioning, publish-job steps and their order, guard behavior, auth, retry mechanics, environment approvals). Flag anything the doc asserts that the workflows/scripts on this branch don't actually do, and anything notable they do that the doc misstates. |
|
Claude finished @cliffhall's task in 3m 3s —— View job RELEASING.md accuracy reviewI checked every claim in 🔴 "publish exclusively from
|
…ims true Review findings: RELEASING.md claimed publishing happens exclusively from release.yml with no registry tokens, but typescript.yml and python.yml still carried release:[published] publish jobs — never fired (releases are created with GITHUB_TOKEN, which suppresses workflow triggers) and unable to succeed under OIDC (typescript.yml's still referenced the deleted NPM_TOKEN). Remove both jobs and their release triggers; the workflows are now pure CI. Also: - add skip-existing to the PyPI publish action so re-runs tolerate already-uploaded files, and describe the per-registry guard accurately in RELEASING.md (npm aborts; PyPI skips) - correct the fresh-dispatch note: same-day dispatches collide on the date-granular tag rather than minting a new version Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AViME1R9ES2UXT1uQDx1TG
|
Addressed in 06a8169:
@claude review — please re-verify RELEASING.md against the branch as it now stands. |
|
Claude finished @cliffhall's task in 1m 5s —— View job RELEASING.md re-verification against
|
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AViME1R9ES2UXT1uQDx1TG
|
Applied the cosmetic nuance too (see latest commit): the guard sentence now states placement per registry — npm aborts before tests, PyPI's skip happens at the publish step. RELEASING.md is now verified line-by-line against the branch. Closing out the review loop — thanks for the thorough passes. |
Releases are now triggered deliberately via workflow_dispatch. The schedule produced a year of unattended runs stuck awaiting environment approval, every real release was manually dispatched anyway, and two runs on the same UTC day collide on the date-based tag. Rename the workflow to "Release" accordingly and update RELEASING.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AViME1R9ES2UXT1uQDx1TG
Previously push-triggered CI ran only on main, so same-repo branches got no CI until a PR was opened (fork contributions were and remain covered by pull_request, which fires on open and on every subsequent push). Run on all pushes, and add a per-ref concurrency group so rapid amend-pushes cancel superseded runs. Same-repo branches with an open PR will run under both events; acceptable at this repo's scale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AViME1R9ES2UXT1uQDx1TG
|
@claude review — full-PR pass please: all workflow changes (release.yml, typescript.yml, python.yml), scripts/release.py, RELEASING.md, README, and the PR description itself. The branch has grown since the last full review (dead publish jobs removed, cron removed, CI triggers broadened to all pushes with per-ref concurrency, PyPI skip-existing, doc revisions) — verify it all hangs together and that the docs/description match the code exactly. |
|
Claude finished @cliffhall's task in 2m 24s —— View job Full-PR review pass
I traced every workflow, the script, the doc, and the PR description against the branch as it now stands. It all hangs together — nothing in the docs or description contradicts the code. Two non-blocking observations below; neither should hold up merge. 🟢 Everything cross-checks
README changes — root PR description — every itemized change (permissions, npm@11, provenance, token removal, 🟡 Two non-blocking observations
Bottom line: the branch is internally consistent and the docs/description match the code exactly. RELEASING.md's triggers, change-detection, versioning, per-job step order, per-registry guard behavior, OIDC auth, and retry mechanics all line up with
|
Per-ref cancellation is for superseded branch amend-pushes; on main, every merge keeps its own CI run so failures stay attributable to a specific commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AViME1R9ES2UXT1uQDx1TG
|
Both observations actioned in 5850690 / #4463:
@claude review — one confirmation pass on the concurrency change, since it touched workflow code after your LGTM. |
|
Claude finished @cliffhall's task in 57s —— View job Confirmation pass — concurrency change (
|
|
Sign-off on the pending-run nuance: accepted as-is. The in-progress main run is always protected, coalescing queued main runs under burst merges is fine (the newest run tests the union anyway), and per-commit attributability isn't a hard requirement worth a SHA-keyed group's cost. Review loop closed — thanks for three thorough passes. |
Summary
Phase 1 of #4463: make
release.yml's npm publishing work via trusted publishing (OIDC) instead of the deadNPM_TOKEN, and nudge all four TypeScript packages into the next release matrix.Both June release attempts (26882432128, 27626321345) published to PyPI but failed every npm leg with a disguised auth error (
E404on the publish PUT) — the token is expired and non-refreshable. Each@modelcontextprotocol/*package's npm trusted publisher is being registered against this workflow (release.yml) + thereleaseenvironment, so the workflow must authenticate via OIDC.Changes
.github/workflows/release.yml(publish-npmjob) — mirrors modelcontextprotocol/inspector#1199:permissions: id-token: write(required to mint the OIDC token;contents: readadded explicitly since settingpermissionsdrops defaults)npm@^11.5.1— trusted publishing requires npm ≥ 11.5.1, Node 22 bundles npm 10.xNODE_AUTH_TOKENfrom the publish stepNPM_CONFIG_PROVENANCE: "true"so packages get provenance attestationsnpm test --if-presentbefore build/publish — previously nothing between tagging and publishing ran a testpublish-pypijob: runpytestbefore build/publish (guarded by the same tests-directory checkpython.ymluses; pyright already ran)scripts/release.py: count.mdchanges when building the release matrix — READMEs ship inside the published artifacts, and.py/.ts-only detection would have ignored this PR's README touches entirely (review finding)publish-pypijob (cont.):skip-existing: trueon the PyPI publish action so re-runs tolerate already-uploaded files (review finding)typescript.yml/python.yml: removed therelease: [published]triggers andpublishjobs — they never fired (releases are created withGITHUB_TOKEN, which suppresses workflow triggers), could never succeed under OIDC (wrong workflow in the claims), andtypescript.yml's still referenced the deletedNPM_TOKEN. Both workflows are now pure CI (review finding). This makesrelease.ymlthe only workflow that publishes, matching the trusted-publisher binding andRELEASING.md's description. CI now also runs on pushes to any branch (not justmain) so same-repo branches get CI before a PR exists; a per-refconcurrencygroup cancels superseded runs on rapid amend-pushes (fork PRs were and remain covered bypull_request).Daily cron removed —
release.ymlis nowworkflow_dispatch-only (renamed "Release"): the schedule produced a year of unattended runs stuck awaiting approval, real releases were always dispatched manually, and same-day runs collide on the date-based tag. Phase 2 (#4463) replaces the trigger withrelease: [published].Docs: new root
RELEASING.md(publishing mechanics as of this PR + failed-publish retry runbook), linked from README alongside the other maintainer docsCI trigger behavior after this PR
typescript.ymlandpython.ymlare pure CI (detect-packages→test→build, all packages each run) and fire as follows:pull_requestpull_request(synchronize)pushpush+ per-refconcurrencypush+pull_requestmainpushFork pushes never fire this repo's
pushtrigger (they happen in the fork), which is whypull_requestremains essential for contributor coverage.Release(capital R), while this yaml and the npm trusted-publisher config sayrelease. npm compares the OIDC environment claim case-sensitively, so the environment must be deleted and recreated as lowercasereleasebefore the first OIDC publish (exact commands in #4463, Phase 0). Bonus: deleting it automatically fails the 31 stale waiting release runs, clearing that backlog in one stroke. Requires repo admin.README touch in each TypeScript server so
scripts/release.pyincludes all four in the next npm matrix (they're stranded on npm at2026.1.26while PyPI advanced to2026.6.16):everything: add the License section the other three servers already havefilesystem/memory/sequential-thinking: link the package's npm page under the introMerging is publish-safe
Merging this PR publishes nothing. Publishing only happens when a
release-environment deployment is approved on arelease.ymlrun (cron runs queue daily but sit awaiting approval). The actual release will be dispatched deliberately once the npm trusted-publisher registrations are complete for all four packages.If a package's trusted publisher isn't registered by then, its matrix leg fails alone (
fail-fast: false) and catches up in a later release.Prerequisites before dispatching the release (not part of this PR)
Release→release— done 2026-07-04 (delete + recreate; also auto-failed all 31 stale waiting runs and deleted the dead env-levelNPM_TOKENsecret)server-everything,server-memory,server-filesystem,server-sequential-thinking(workflowrelease.yml, environmentrelease) — done 2026-07-04NPM_TOKENsecrets remain anywhere; going full OIDC — no tokens will be issued againPart of #4463.
🤖 Generated with Claude Code