From edd47ade5c38b4080cebc865c3a59178d3553d04 Mon Sep 17 00:00:00 2001 From: Brent Date: Thu, 20 Aug 2026 15:20:42 -0400 Subject: [PATCH] Fix the npm read-after-write race I introduced in the publish gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #194 made each registry publish gate on "does npm already serve this version", to stop a mid-run failure silently stranding four registries. It reintroduced the same failure through a different door: npm registry READS are eventually consistent, so `npm view @smooai/logger@4.5.4` moments after a successful publish still 404s. The gate concluded "not released", skipped PyPI, crates.io, the Go tag and NuGet, and the run went green — leaving 4.5.4 on npm alone. Two independent signals now count as released, because neither alone is enough: - changesets published it in THIS run (steps.changesets.outputs.published). Authoritative the instant it happens, no registry read involved. This is the normal path and the one the race broke. - npm already serves it. The backfill path, which is what lets a re-run catch up registries an earlier run left behind — the whole point of #194. Only when neither holds do the four steps skip, which is correct: that is a run that merely opened the version PR, where package.json is bumped to something nothing has published and the workspace sits on an unmerged branch. Deliberately NO changeset. With package.json staying at 4.5.4, merging this takes the backfill path and ships 4.5.4 to the four registries it is missing from. A changeset would bump to 4.5.5 and strand 4.5.4 permanently. Verified all three states by hand: published-this-run -> proceed even while npm 404s; npm-serves-it -> proceed; unreleased 9.9.9 -> skip all four. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0152bbE1veqfG1SVJdyLCBxC --- .github/workflows/release.yml | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 85ad09f6..0e7a1fe0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -118,18 +118,33 @@ jobs: # at 4.5.0 here. Asking each registry directly also makes a re-run heal it. - name: Which registries are behind package.json? id: lag + env: + CHANGESETS_PUBLISHED: ${{ steps.changesets.outputs.published }} run: | set -euo pipefail VERSION=$(node -p "require('./package.json').version") echo "Target version: ${VERSION}" - # npm is the source of truth for "this version is released". The + # Is ${VERSION} actually released? Two independent yeses, because + # neither alone is sufficient: + # + # - changesets published it in THIS run. Authoritative immediately, + # and necessary because npm reads are eventually consistent: a + # `npm view` moments after a successful publish still 404s, which + # skipped all four steps for 4.5.4 and left them a version behind. + # - npm already serves it. This is the backfill path — it lets a + # re-run catch up registries that an earlier run left behind. + # + # Neither true means package.json is bumped but unreleased. The # changesets action leaves the workspace on the version branch, so in a - # run that only OPENS the version PR, package.json is already bumped to - # a version nothing has published yet — without this gate the four - # steps below would ship it ahead of npm, from an unmerged branch. - if ! npm view "@smooai/logger@${VERSION}" version >/dev/null 2>&1; then - echo "npm does not have ${VERSION} yet — nothing to backfill." + # run that only OPENS the version PR this is exactly the case, and + # publishing here would ship ahead of npm from an unmerged branch. + if [ "${CHANGESETS_PUBLISHED:-false}" = "true" ]; then + echo "changesets published ${VERSION} in this run." + elif npm view "@smooai/logger@${VERSION}" version >/dev/null 2>&1; then + echo "npm already serves ${VERSION} — checking the others for backfill." + else + echo "${VERSION} is not released (not published here, not on npm) — nothing to do." { echo "pypi=no"; echo "crates=no"; echo "nuget=no"; echo "gotag=no" } >> "$GITHUB_OUTPUT"