diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f600809b..0e44a1d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -99,17 +99,25 @@ jobs: # OIDC migration replaces this isolated auth step only. Candidate and # promotion behavior deliberately know nothing about credential source. - name: verify npm publisher identity - run: npm whoami --registry=https://registry.npmjs.org + shell: bash + run: | + for attempt in $(seq 1 10); do + if npm whoami --registry=https://registry.npmjs.org; then exit 0; fi + echo "npm whoami failed (attempt ${attempt}/10); retrying after the registry settles" + sleep 30 + done + exit 1 env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: node script/release-packages.mjs publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # Staged publishing can accept a publish without making it installable. - # Surface the maintainer approval queue in the failing run's log. + # Surface the maintainer approval queue in the failing run's log. Bound + # the npx download so a registry outage cannot hang the job for hours. - name: list npm staged releases awaiting approval if: failure() - run: npx --yes npm@12 stage list || true + run: timeout 180 npx --yes npm@12 stage list || true env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/script/release-packages.mjs b/script/release-packages.mjs index caf345cb..e1c0fa9c 100644 --- a/script/release-packages.mjs +++ b/script/release-packages.mjs @@ -27,9 +27,24 @@ const problems = [ if (problems.length > 0) throw new Error(`release preflight failed: ${problems.join('; ')}`) for (const pkg of packages) pkg.filename = join(ROOT, '.artifacts', 'pack', pkg.filename) +/** npm registry failures that are worth retrying inside one release step. */ +const RETRYABLE_NPM = /\b(?:E503|EAI_AGAIN|ETIMEDOUT|ECONNRESET)\b|503 Service Unavailable|network/u + +/** Block one release step between registry attempts without an async refactor. */ +function sleepSync(ms) { + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms) +} + function npmView(spec, field) { - const result = spawnSync('npm', ['view', spec, field, '--json'], { cwd: ROOT, encoding: 'utf8' }) - return readNpmView(result, spec, field) + let last + for (let attempt = 1; attempt <= 4; attempt += 1) { + const result = spawnSync('npm', ['view', spec, field, '--json'], { cwd: ROOT, encoding: 'utf8' }) + const failure = `${result.stderr ?? ''}${result.error?.message ?? ''}` + if (result.status === 0 || !RETRYABLE_NPM.test(failure)) return readNpmView(result, spec, field) + last = new Error(`npm view ${spec} ${field} failed during a registry outage (attempt ${attempt}/4)`) + if (attempt < 4) sleepSync(attempt * 3_000) + } + throw last } /**