Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Expand Down
19 changes: 17 additions & 2 deletions script/release-packages.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down
Loading