Repository navigation
fix(scripts): reconcile publish conflicts and wait for visibility (SI-2040) - #9997
Doddanna17 wants to merge 1 commit into
Conversation
npm processes publishes asynchronously (publish-time malware scanning), so a successful `npm publish` only means the upload was accepted and the version appears on the registry minutes-to-hours later. The release pipeline assumed immediate visibility, which produced three failures: a hard-coded sibling wait that expired while the version was still being processed, reruns that collided with versions already published (Rekor 409, npm E403), and runs that reported green while packages were missing. verify-release.ts now treats the registry, not the publish exit code, as the source of truth: it waits for the exact version to appear, reconciles a publish conflict when that version is already published instead of bumping it, classifies npm publish-over failures as recoverable, requires bitgo to carry shrinkwrap metadata, warns when the dist-tag has not moved, and fails the run in every mode when a package is missing. Both release workflows get explicit visibility budgets with job timeouts above them, and Verify Publish regenerates bitgo's shrinkwrap when recovery re-publishes it. Also runs the TypeScript script tests in CI: `test:scripts` only matched `*.test.js`, so the shrinkwrap and prepare-release suites never executed. Ticket: SI-2040
8b7ebf7 to
8351328
Compare
How each change maps to a specific failureDetail behind the diff, so a reviewer can check each claim against the Oct 17 symptoms rather than the summary. The underlying assumption was " 1. Fail closed in every mode — Before, a failed package hit 2. Reconcile instead of bump — Before, any conflict was answered by 3. Recognise npm publish-over — Before, only transparency-log conflicts were recognised and the whole check sat behind 4. Verify visibility, not exit codes — Before, nothing confirmed the version ever appeared, and the sibling wait had a fixed budget a normal tail blew straight through (expired at 19.8 min while 5. A published 6. Budgets sized for npm's tail — both workflows
7. Recovery re-publish keeps the shrinkwrap — Adds 8. The TypeScript suites were never running — Before, 9. New suite — Conflict classification, visibility polling and budget parsing, reconcile-vs-bump, the Why this approach, and why it sometimes passed anywayThe choice of fix. Three alternatives were on the table, and the shape of this PR is what is left after rejecting them:
What remains is the smallest change that makes the pipeline both tolerant of the measured lag and truthful about state: poll the registry for the version, reconcile instead of bumping, size the wait budgets above the observed tail, and fail closed. Why it sometimes worked. This is the important part, and it is consistent with the diagnosis rather than an argument against it. The visibility delay is a random variable, not a constant: 2.9 min typical, 18.3 min and 55 min observed. The old pipeline raced it with fixed budgets — a 20 min sibling wait ( Two further sources of flip-flopping amplify the impression of non-determinism:
The consequence for the fix: changes 1–3 and 5–9 are deterministic — fail-closed, reconcile, conflict classification, the shrinkwrap requirement, and tests that now execute in CI. Change 4 and 6 are tolerance, not elimination: with a budget above the measured tail, the measured distribution no longer fails, but a lag beyond 120 min still would. Eliminating that residue means removing the registry dependency (WCI-1858 fix #1) or moving the liveness check out of the publish job (WCI-1754) — both tracked, both out of scope here. ScopeThis does not change npm's asynchronous processing (impossible) and is not the root-cause elimination — it makes the pipeline tolerant of the tail and truthful about state. The eliminations stay tracked separately: building the Note on the workflow diffs
|
Context
npm processes publishes asynchronously (publish-time malware scanning, changelog 2026-07-28 — https://gh.io/npm-publish-time-scanning). A successful
npm publishnow only means the upload was accepted; the version becomes visible on the registry later — measured 2.9 min typical, 18.3 min and 55 min outliers, up to 24 h per npm's own email. Our release pipeline assumed immediate visibility.Oct 17 (master
91eeaace5), five publish attempts, three failed: a hard-coded sibling wait expired at 19.8 min while@bitgo-beta/sdk-coin-tao@1.0.0-beta.1477was still being processed (its successor surfaced 3 seconds after the poll gave up); reruns collided with already-published versions (TLOG_CREATE_ENTRY_ERROR (409), npmE403 cannot publish over the previously published versions); and runs reported green while packages were missing. Root cause confirmed with Infra in INF-4737 — the fix belongs in our publish scripts.Change
scripts/verify-release.ts— the registry is now the source of truth, not the publish exit code:GET /<pkg>/<version>, bounded byBITGO_PUBLISH_VISIBILITY_TIMEOUT_MINUTES, default 60) after publishing;E403 cannot publish over the previously published versions,EPUBLISHCONFLICT) as recoverable conflicts;bitgoto carry shrinkwrap metadata (_hasShrinkwrap) before counting it as published;@<preid>dist-tag has not moved to the published version;package.json—test:scriptsruns the TypeScript suites via--import=tsx. It previously matched only*.test.js, so the shrinkwrap and prepare-release suites never executed in CI (the new suite would have been equally dead).scripts/tests/verify-release.test.ts— new, 22 cases: conflict classification, visibility polling and budget, reconcile-vs-bump, bitgo shrinkwrap requirement, dist-tag warning, private-module skip, fail-closed.Both release workflows — explicit visibility budgets (
BITGO_SIBLING_AVAILABILITY_TIMEOUT_MINUTES=120,BITGO_PUBLISH_VISIBILITY_TIMEOUT_MINUTES=120) withtimeout-minutes: 180above them (the stable release job was hard-capped at 60, so a larger wait could not have been used), andBITGO_GENERATE_SHRINKWRAP=trueon Verify Publish so a recovery re-publish ofbitgois not shipped without a shrinkwrap.Why reconcile rather than bump
Bumping republishes identical content under a new version — that produced
@bitgo-beta/sdk-coin-stxbeta.2334 → .2335 → .2336in 50 minutes, and it invalidates the shrinkwrap built forbitgo's previous version. When the registry already holds the intended version (npmE403), waiting for it to become visible is the correct resolution; a bump is only justified when the version is genuinely taken by different content (aTLOGconflict whose upload never landed).Point-by-point rationale in this comment: how each change maps to the failure it fixes, why this approach was chosen over raising the timeouts / continuing to bump versions / removing the registry from the path, and why the pipeline sometimes passed anyway (the lag is a heavy-tailed race against a fixed 20 min wait inside a 60 min job cap).
Commits
8351328b63fix(scripts): reconcile publish conflicts and wait for visibilityverify-release.ts,verify-release.test.ts,package.json,publish.yml,npmjs-release.ymlTests
scripts/tests/verify-release.test.ts(22 cases).yarn test:scriptsnow also executes the previously-deadgenerate-bitgo-shrinkwrap.test.ts(14 cases) andprepare-commit-msg.test.js(12).Verification
yarn test:scripts→ 48 passing / 0 failingnpx tsc -p scripts/tsconfig.json→ no errors in the changed files (only pre-existing key-card / sdk-core project errors)npx prettier --checkclean;npx eslint0 errors; commitlint passed; commit GPG-signed8351328b63:code-quality (scripts),(format),(lint),(dependencies),(commit-lint)all pass — the previously-dead TypeScript suites now execute and are greentimeout-minutes: 60; everything else is additionsReview follow-ups
None yet.
Related
Not in this PR (follow-ups)
bitgoshrinkwrap from local sibling tarballs so it never consults the registry (WCI-1858 fix Add README for examples directory #1).scripts/tests/prepareRelease/prepare-release-main.test.tsneeds full git history (git clone --local+ checkout of a historical commit); the CI job that runstest:scriptschecks out shallow, so it stays excluded from the glob — it should move to a job with full history.npmjs-release.yml("Verify recovery published the missing versions" — single pass, no retry; "Verify bitgo package has shrinkwrap metadata" — 5 × 5 s) still need a real budget (WCI-1754).