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
|
zahin-mohammad
left a comment
There was a problem hiding this comment.
Request changes: this improves fail-closed behavior and npm visibility polling, but the recovery path still has four release-blocking correctness problems.
-
Existing-version reconciliation does not verify artifact contents.
VersionInfokeeps onlyfoundandhasShrinkwrap(scripts/verify-release.ts:23-45), and an existing version is accepted after a publish conflict atscripts/verify-release.ts:179-186. In the incident this PR is meant to fix, npm's@bitgo-beta/sdk-coin-eth@4.4.1-beta.2098has shasum6d031ba72e0b2d2b50b33b94cbc6fafc0a795fc2, while run39991955081produced00090cba8bee983846790ec88059432bc20e3c91. The PR would treat that different artifact as reconciled. Please compare the locally packed tarball's integrity with npm'sdist.integrity/dist.shasum; an occupied version with different contents must get a fresh version. -
A stale consumer-facing dist-tag explicitly returns success. When the exact version exists but
betapoints elsewhere, verification warns and returnstrue(scripts/verify-release.ts:225-242). The new test codifies that false green (scripts/tests/verify-release.test.ts:189-194). This is the reported failure mode: consumers using@betado not receive the intended release. Verification must repair and re-check the tag, or fail. For this particular2098, retagging is unsafe because of the digest mismatch above. -
The late recovery bump invalidates the prepared dependency graph and shrinkwrap. Recovery mutates only the failed package's
package.json(scripts/verify-release.ts:196-204). Normal preparation propagates version changes to every internal dependent (scripts/prepare-release.ts:129-145). The workflow has already built and published siblings andbitgobefore this bump (.github/workflows/publish.yml:103-139), so dependents andbitgo's npm shrinkwrap can remain pinned to the unusable old version. A fresh version must return to graph-wide dependency reconciliation, rebuild the affected closure, regenerate shrinkwrap, and publish dependencies beforebitgo. -
Fallback publication cannot publish the private
bitgoworkspace. The fallback calls plainnpm publish(scripts/verify-release.ts:47-49), whilemodules/bitgo/package.json:1-6hasprivate: true. WCI-1650 (1895a4c010) already documented that npm can silently skip this workspace with exit code zero and only a stderr warning; that fix temporarily removed/restoredprivateand surfaced stderr. These tests createbitgowithoutprivate: true(scripts/tests/verify-release.test.ts:43-48,:257-279), so the regression is not covered.
Please also add regression tests for digest mismatch, stale-tag repair/failure, dependency and shrinkwrap propagation after a recovery bump, and a real private: true bitgo fallback. Until these are addressed, the workflow can report success with the wrong artifact/tag or publish an internally inconsistent package set.
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).