From 38f677ee0acb5c3a245cb18a587ff04ea444e5ea Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 24 Oct 2026 06:43:56 -0700 Subject: [PATCH 1/3] ci(npm-publish): poll the registry in the final assert so a lagging replica can't red a sound release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'Assert tag → this commit and registry → this version' step read the registry once with 'npm view'. npm replicas can lag a publish by seconds, so that single read returned E404 / an older version on three good publishes on main (cli 0.1.159 run 40097709043: 'E404 No match found for version 0.1.159' while 0.1.159 is served now). A false red here hides a real release failure. Poll the registry half up to 10 times, 15s apart (~150s), failing only once exhausted. The tag→commit half stays strict — a wrong tag is never propagation lag. The scripted readback (TASK-100) still owns the primary budget; this is a bounded tolerance on the final re-read. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01V7T2fgquLRaqaYiGLC6Dey --- .github/workflows/npm-publish.yml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index bb1ff2a6a1..fb3a3aa315 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -274,10 +274,25 @@ jobs: TAG: ${{ env.TAG_PREFIX }}${{ steps.cmp.outputs.version }} run: | set -euo pipefail + # The tag→commit half is strict: a tag that points at the wrong commit + # is never a propagation delay, so one read, no retry. tag_sha=$(git ls-remote origin "refs/tags/$TAG" | cut -f1) [ "$tag_sha" = "$GITHUB_SHA" ] || { echo "::error::$TAG points at '$tag_sha', not the built commit $GITHUB_SHA"; exit 1; } - got=$(npm view "$NAME@$NEXT" version) - [ "$got" = "$NEXT" ] || { echo "::error::npm serves '$got' for $NAME@$NEXT"; exit 1; } + # The registry half polls: `npm view` can read a replica that is seconds + # behind the publish and return E404 / an older version, which failed + # this assert on three good publishes on main (cli 0.1.159 run + # 40097709043 among them: "E404 No match found for version 0.1.159" + # while that version is served now). The scripted readback above owns + # the primary propagation budget; this is a bounded tolerance on the + # final re-read so a lagging replica can't red a sound release. + got="" + for i in $(seq 1 10); do + got=$(npm view "$NAME@$NEXT" version 2>/dev/null || true) + [ "$got" = "$NEXT" ] && break + echo "· registry serves '${got:-}' for $NAME@$NEXT (try $i/10); waiting 15s for propagation" + sleep 15 + done + [ "$got" = "$NEXT" ] || { echo "::error::npm still serves '${got:-}' for $NAME@$NEXT after 10 tries over ~150s"; exit 1; } echo "✓ $NAME $NEXT = $TAG = $GITHUB_SHA" # The MCP Registry lists commonly-mcp from its server.json, after npm From d99eae4e4a986b4c71d753b1b99a0bf6cb0f7209 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 24 Oct 2026 07:03:17 -0700 Subject: [PATCH 2/3] ci(npm-publish): query registry.npmjs.org directly so the readback bypasses npm's packument cache Lark/Sable gate on #2634 found the retry loop couldn't fix the race it targeted: `npm view` caches a packument for up to 300s, so ten reads in one runner re-serve the same stale cache, and each `npm view` had no per-request timeout so the "~150s" budget wasn't actually bounded. Replace the registry half with a direct GET of the version-specific manifest on registry.npmjs.org (uncached; its 200/404 is the existence assertion) under `curl -m 20`, so each try is fresh and time-bounded. The tag->commit half is unchanged. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01V7T2fgquLRaqaYiGLC6Dey --- .github/workflows/npm-publish.yml | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index fb3a3aa315..6b044d6148 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -278,21 +278,24 @@ jobs: # is never a propagation delay, so one read, no retry. tag_sha=$(git ls-remote origin "refs/tags/$TAG" | cut -f1) [ "$tag_sha" = "$GITHUB_SHA" ] || { echo "::error::$TAG points at '$tag_sha', not the built commit $GITHUB_SHA"; exit 1; } - # The registry half polls: `npm view` can read a replica that is seconds - # behind the publish and return E404 / an older version, which failed - # this assert on three good publishes on main (cli 0.1.159 run - # 40097709043 among them: "E404 No match found for version 0.1.159" - # while that version is served now). The scripted readback above owns - # the primary propagation budget; this is a bounded tolerance on the - # final re-read so a lagging replica can't red a sound release. - got="" + # The registry half polls registry.npmjs.org directly with curl, not + # `npm view`: npm caches a packument for up to 300s, so retrying + # `npm view` in one runner re-reads the same stale cache and never + # sees the fresh publish — which false-red three sound main publishes + # (cli 0.1.159 / run 40097709043 among them: "E404 No match found for + # version 0.1.159" while 0.1.159 is served now). A GET of the + # version-specific manifest is uncached and its 200/404 is itself the + # assertion that this exact version exists; -m 20 bounds each request + # so a hung connection can't stall the loop past its ~150s budget. + found="" for i in $(seq 1 10); do - got=$(npm view "$NAME@$NEXT" version 2>/dev/null || true) - [ "$got" = "$NEXT" ] && break - echo "· registry serves '${got:-}' for $NAME@$NEXT (try $i/10); waiting 15s for propagation" + if curl -fsS -m 20 -o /dev/null "https://registry.npmjs.org/$NAME/$NEXT"; then + found=yes; break + fi + echo "· registry.npmjs.org has no $NAME@$NEXT yet (try $i/10); waiting 15s for propagation" sleep 15 done - [ "$got" = "$NEXT" ] || { echo "::error::npm still serves '${got:-}' for $NAME@$NEXT after 10 tries over ~150s"; exit 1; } + [ -n "$found" ] || { echo "::error::registry.npmjs.org still has no $NAME@$NEXT after 10 tries over ~150s"; exit 1; } echo "✓ $NAME $NEXT = $TAG = $GITHUB_SHA" # The MCP Registry lists commonly-mcp from its server.json, after npm From 32a9aef949b34cd495daf5cb6c121221b518d379 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 24 Oct 2026 07:24:42 -0700 Subject: [PATCH 3/3] ci(npm-publish): assert an exact 200 + body .version in the registry readback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lark/Sable HOLD: `curl -f` passes a 3xx and an empty 2xx, so a redirect or blank body read as "found". Require an exact HTTP 200 (captured via `-w '%{http_code}'`, no -L so a redirect is rejected) AND parse the version-specific manifest, requiring `.version == $NEXT` via node -e — only the real manifest for this exact version passes. Correct the budget comment to the true worst case (~335s: 10 × 20s curl + 9 × 15s sleep, no sleep after the last try). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01V7T2fgquLRaqaYiGLC6Dey --- .github/workflows/npm-publish.yml | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 6b044d6148..2de4832e15 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -284,18 +284,26 @@ jobs: # sees the fresh publish — which false-red three sound main publishes # (cli 0.1.159 / run 40097709043 among them: "E404 No match found for # version 0.1.159" while 0.1.159 is served now). A GET of the - # version-specific manifest is uncached and its 200/404 is itself the - # assertion that this exact version exists; -m 20 bounds each request - # so a hung connection can't stall the loop past its ~150s budget. - found="" + # version-specific manifest is uncached. We do NOT trust `curl -f` + # alone: it passes a 3xx and an empty 2xx, so a redirect or a blank + # body would read as "found". Instead require an exact HTTP 200 (no + # -L, so a redirect stays a redirect and is rejected) AND parse the + # body, requiring .version == $NEXT — only the real manifest for this + # exact version passes. -m 20 bounds each request. Worst case ~335s: + # 10 attempts × 20s curl + 9 × 15s sleep (no sleep after the last). + found=""; code=000 for i in $(seq 1 10); do - if curl -fsS -m 20 -o /dev/null "https://registry.npmjs.org/$NAME/$NEXT"; then - found=yes; break + code=$(curl -sS -m 20 -o /tmp/reg-manifest.json -w '%{http_code}' "https://registry.npmjs.org/$NAME/$NEXT" || echo 000) + if [ "$code" = 200 ]; then + ver=$(node -e 'try{process.stdout.write(String(require(process.argv[1]).version||""))}catch(e){}' /tmp/reg-manifest.json) + if [ "$ver" = "$NEXT" ]; then found=yes; break; fi + echo "· registry.npmjs.org returned 200 but version='$ver' (want $NEXT, try $i/10)" + else + echo "· registry.npmjs.org has no $NAME@$NEXT yet (HTTP $code, try $i/10)" fi - echo "· registry.npmjs.org has no $NAME@$NEXT yet (try $i/10); waiting 15s for propagation" - sleep 15 + [ "$i" -lt 10 ] && sleep 15 done - [ -n "$found" ] || { echo "::error::registry.npmjs.org still has no $NAME@$NEXT after 10 tries over ~150s"; exit 1; } + [ -n "$found" ] || { echo "::error::registry.npmjs.org still has no $NAME@$NEXT (last HTTP $code) after 10 tries over ~335s"; exit 1; } echo "✓ $NAME $NEXT = $TAG = $GITHUB_SHA" # The MCP Registry lists commonly-mcp from its server.json, after npm