From 1896c374e13ef7f453e197331824524e5efc32ba Mon Sep 17 00:00:00 2001 From: Jake Fineman Date: Thu, 3 Sep 2026 17:29:05 -0400 Subject: [PATCH] ci(release): make npm trusted publishing actually work + post-publish verification release.yml (from #44) already tried OIDC trusted publishing with an NPM_TOKEN fallback, but Node 22's bundled npm (10.9.2) is below the 11.5.1 floor npm requires for trusted publishing -- the OIDC path was silently dead and every publish was quietly running through the fallback, or would fail outright once no NPM_TOKEN secret exists. release.yml also stopped verifying anything the moment npm publish returned 0, which is exactly the class of gap that let 1.0.8 (broken --version, silent-success wave status) reach real users undetected. What changed: - Upgrades npm to 11.19.0 before the OIDC publish attempt (pinned exact version, then asserts the resulting npm version is >= 11.5.1 before continuing) -- same pattern already proven in mcp-server's release.yml. - Adds a verify-publish job, gated on needs: release, that: - polls npm view @wave-av/cli@ until the registry confirms the exact tagged version is live; - installs that version from the real registry (not the packed tarball the release job's own smoke test already checked) and asserts npx wave --version prints the tagged version; - runs wave status --output json and asserts the parsed apiEndpoint is exactly https://api.wave.online -- the receipt that the published binary defaults to the real API, never the wave.online marketing site. --version reading package.json at runtime and wave status exiting non-zero on failure are both already on main (the 1.0.9 fix), with their own unit tests in src/cli.test.ts and src/commands/status/index.test.ts; nothing in that path needed changing here. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01MPeHryYVubEwzmnnf8pykK --- .github/workflows/release.yml | 98 +++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0c8f598..87744d3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,6 +48,24 @@ jobs: with: node-version: "22" + # npm trusted publishing (OIDC) requires npm >= 11.5.1; Node 22 bundles an older npm + # (10.9.2 as of this writing), under which the OIDC path silently cannot run and every + # publish falls through to the NPM_TOKEN fallback even when a Trusted Publisher IS + # registered. Pinned to an exact version so this privileged job never runs an unvetted + # npm release with its OIDC publishing credentials, then PROVE the floor is met. + - name: Upgrade npm to a trusted-publishing-capable CLI (>= 11.5.1) + run: | + set -euo pipefail + npm install -g npm@11.19.0 + NPM_VER="$(npm --version)" + echo "npm version: $NPM_VER" + NPM_VER="$NPM_VER" node -e " + const cur=process.env.NPM_VER.split('.').map(Number), min=[11,5,1]; + for(let i=0;i<3;i++){ + if(cur[i]>min[i]) process.exit(0); + if(cur[i]> "$GITHUB_OUTPUT" + + - name: npm view — published version is live on the registry + env: + EXPECTED: ${{ steps.ver.outputs.version }} + run: | + set -euo pipefail + PUBLISHED="" + for i in 1 2 3 4 5 6 7 8; do + PUBLISHED="$(npm view @wave-av/cli@"$EXPECTED" version 2>/dev/null || true)" + if [ "$PUBLISHED" = "$EXPECTED" ]; then break; fi + echo "waiting for the registry to index @wave-av/cli@$EXPECTED (attempt $i)" + sleep 15 + done + if [ "$PUBLISHED" != "$EXPECTED" ]; then + echo "::error::npm view never returned $EXPECTED for @wave-av/cli" + exit 1 + fi + echo "npm view: @wave-av/cli@$EXPECTED confirmed live" + + - name: Fresh install from the real registry (not the packed tarball) + env: + EXPECTED: ${{ steps.ver.outputs.version }} + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/verify" + echo "SMOKE=$RUNNER_TEMP/verify" >> "$GITHUB_ENV" + cd "$RUNNER_TEMP/verify" + npm init -y >/dev/null + npm install "@wave-av/cli@$EXPECTED" --registry=https://registry.npmjs.org + GOT="$(npx --no wave --version)" + echo "expected=$EXPECTED got=$GOT" + if [ "$GOT" != "$EXPECTED" ]; then + echo "::error::published package reports version '$GOT', expected '$EXPECTED'" + exit 1 + fi + + - name: Default endpoint is api.wave.online, never wave.online + # `wave status` is unauthenticated in this job (no keychain entry exists on a fresh + # runner), so it always reports "not authenticated" -- what matters here is which + # host the published binary targets by default, read back via --output json rather + # than grepping stdout copy that could change wording without changing behavior. + run: | + set -euo pipefail + cd "$SMOKE" + # `status` prints a human summary AND a trailing JSON block under --output json; the + # command's own exit code is non-zero when unauthenticated/unreachable (by design -- + # see cli.test.ts), so this step captures output regardless of exit status and parses + # only the JSON block (from the first '{' onward), not the whole mixed stream. + OUT="$(npx --no wave status --output json 2>&1 || true)" + echo "$OUT" + ENDPOINT="$(echo "$OUT" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=d.indexOf('{');try{console.log(JSON.parse(d.slice(i)).apiEndpoint||'')}catch{console.log('')}})")" + echo "apiEndpoint=$ENDPOINT" + if [ "$ENDPOINT" != "https://api.wave.online" ]; then + echo "::error::published package's default apiEndpoint is '$ENDPOINT', expected https://api.wave.online" + exit 1 + fi