From 473806785e392f0a5eba161834d83cb129543565 Mon Sep 17 00:00:00 2001 From: Qwynn Marcelle Date: Fri, 17 Jul 2026 07:23:28 -0400 Subject: [PATCH] fix(release): retry npm install on registry propagation lag in verify-published MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Release workflow's post-publish verification checked the registry immediately after publish with no retry. That's a race, not a failure: npm registry propagation lags publish by seconds to low minutes. Run 29573402812 (2026-07-17T10:26:22Z, HEAD 5179bed) failed with "ETARGET: No matching version found for @workspacejson/spec@0.4.3" nine minutes before the same version was confirmed installable. The publish itself succeeded; only the immediate check was wrong. Retry the npm install step up to 6 times with linear backoff (5s/10s/.../30s) specifically on ETARGET/E404/"No matching version" errors — any other failure still fails fast, no retry. A gate that cries wolf on a known-benign race trains people to ignore red, which is the same failure mode that let a broken tarball sit on npm for 44 days (0.4.1/0.4.2 uninstallable, caught only by manual smoke test on 0.4.3). Verified against the real registry: @workspacejson/spec@0.4.3 installs on first attempt now, no retry needed, no regression to the happy path. Separate, pre-existing issue surfaced while testing this — NOT fixed here, flagging for a follow-up: `npx workspacejson-spec --help` exits 1 (prints "Usage: workspacejson-spec validate " instead of handling --help), so this verification step can still fail after a clean install for a reason unrelated to registry timing. Needs a decision on intended --help behavior in @workspacejson/spec's CLI, not a guess from here. --- scripts/verify-published.mjs | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/scripts/verify-published.mjs b/scripts/verify-published.mjs index 26ba530..efc8d3f 100644 --- a/scripts/verify-published.mjs +++ b/scripts/verify-published.mjs @@ -13,11 +13,19 @@ const packages = [ { name: "agents-audit", check: ["npx", "--no-install", "agents-audit", "--help"] }, ]; +// npm registry propagation lags publish by seconds to low minutes. A single +// immediate post-publish check has no way to tell "not actually published" +// apart from "not visible here yet" and fails the Release workflow either +// way — training everyone to ignore red, which is worse than no gate at all. +const REGISTRY_PROPAGATION_RETRIES = 6; +const REGISTRY_PROPAGATION_BASE_DELAY_MS = 5000; +const isRegistryPropagationLag = (stderr) => /\bE(TARGET|404)\b|No matching version found/.test(stderr ?? ""); + for (const pkg of packages) { const directory = mkdtempSync(join(tmpdir(), "workspacejson-registry-")); try { writeFileSync(join(directory, "package.json"), JSON.stringify({ private: true, type: "module" })); - run("npm", ["install", "--ignore-scripts", "--no-package-lock", `${pkg.name}@${version}`], directory); + await installWithRetry(pkg, directory); run(pkg.check[0], pkg.check.slice(1), directory); console.log(`Verified registry install and runtime entry point: ${pkg.name}@${version}`); } finally { @@ -25,6 +33,29 @@ for (const pkg of packages) { } } +async function installWithRetry(pkg, directory) { + for (let attempt = 1; attempt <= REGISTRY_PROPAGATION_RETRIES; attempt++) { + const result = spawnSync("npm", ["install", "--ignore-scripts", "--no-package-lock", `${pkg.name}@${version}`], { + cwd: directory, + encoding: "utf8", + env: { ...process.env, npm_config_cache: join(directory, ".npm-cache") }, + }); + if (result.status === 0) { + process.stdout.write(result.stdout); + return; + } + const lastAttempt = attempt === REGISTRY_PROPAGATION_RETRIES; + if (!isRegistryPropagationLag(result.stderr) || lastAttempt) { + process.stdout.write(result.stdout); + process.stderr.write(result.stderr); + process.exit(result.status ?? 1); + } + const delayMs = REGISTRY_PROPAGATION_BASE_DELAY_MS * attempt; + console.log(`${pkg.name}@${version} not yet visible on the registry (attempt ${attempt}/${REGISTRY_PROPAGATION_RETRIES}) — retrying in ${delayMs}ms`); + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } +} + function run(command, args, cwd) { const result = spawnSync(command, args, { cwd,