fix(release): tolerate npm propagation in smoke

This commit is contained in:
Andrey Avtomonov 2026-05-17 01:08:58 +02:00
parent ceb578e0f6
commit 8d1837f26e
2 changed files with 73 additions and 3 deletions

View file

@ -2,7 +2,7 @@
import assert from 'node:assert/strict';
import { execFile } from 'node:child_process';
import { mkdtemp, rm } from 'node:fs/promises';
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
@ -22,6 +22,8 @@ export {
const execFileAsync = promisify(execFile);
const SMOKE_TIMEOUT_MS = 180_000;
const TRANSIENT_LOOKUP_RETRY_ATTEMPTS = 6;
const TRANSIENT_LOOKUP_RETRY_DELAY_MS = 10_000;
const VERSION_LABELS = new Set([
'published package npx version',
@ -33,6 +35,18 @@ export function isPublishedPackageVersionLabel(label) {
return VERSION_LABELS.has(label);
}
export function publishedPackageSmokePnpmWorkspaceYaml() {
return ['packages:', ' - "."', 'allowBuilds:', ' better-sqlite3: true', ''].join('\n');
}
export function isTransientPublishedPackageLookupFailure(result) {
return (
result.code !== 0 &&
(result.stderr.includes('npm error code ETARGET') ||
result.stderr.includes('No matching version found for @kaelio/ktx@'))
);
}
function scriptRootDir() {
return resolve(dirname(fileURLToPath(import.meta.url)), '..');
}
@ -61,6 +75,24 @@ async function runCommand(command, args, options = {}) {
}
}
function delay(ms) {
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
}
async function runCommandWithRegistryRetry(command, args, options = {}) {
const attempts = options.retryAttempts ?? TRANSIENT_LOOKUP_RETRY_ATTEMPTS;
const retryDelayMs = options.retryDelayMs ?? TRANSIENT_LOOKUP_RETRY_DELAY_MS;
let result = await runCommand(command, args, options);
for (let attempt = 2; attempt <= attempts && isTransientPublishedPackageLookupFailure(result); attempt += 1) {
process.stdout.write(`npm registry has not exposed the package yet; retrying smoke command (${attempt}/${attempts})\n`);
await delay(retryDelayMs);
result = await runCommand(command, args, options);
}
return result;
}
function requireSuccess(label, result) {
assert.equal(
result.code,
@ -74,15 +106,17 @@ export async function runPublishedPackageSmoke(config) {
try {
const projectDir = join(root, 'demo-project');
await writeFile(join(root, 'pnpm-workspace.yaml'), publishedPackageSmokePnpmWorkspaceYaml());
const commands = buildPublishedPackageSmokeCommands(config, projectDir);
const pnpmHome = join(root, 'pnpm-home');
const globalEnv = {
PNPM_HOME: pnpmHome,
PATH: `${pnpmHome}${process.platform === 'win32' ? ';' : ':'}${process.env.PATH ?? ''}`,
PATH: [join(pnpmHome, 'bin'), pnpmHome, process.env.PATH ?? ''].join(process.platform === 'win32' ? ';' : ':'),
};
for (const command of commands) {
const isGlobalCommand = command.label.includes('global');
const result = await runCommand(command.command, command.args, {
const result = await runCommandWithRegistryRetry(command.command, command.args, {
cwd: command.label.includes('local') || isGlobalCommand ? root : undefined,
env: isGlobalCommand ? { ...globalEnv, ...command.env } : command.env,
});

View file

@ -6,6 +6,8 @@ import {
buildPublishedPackageNpxCommand,
buildPublishedPackageSmokeCommands,
isPublishedPackageVersionLabel,
isTransientPublishedPackageLookupFailure,
publishedPackageSmokePnpmWorkspaceYaml,
publishedPackageSpec,
readPublishedPackageSmokeConfig,
} from './published-package-smoke.mjs';
@ -156,6 +158,33 @@ describe('published package smoke output validation labels', () => {
});
});
describe('published package smoke registry retry classification', () => {
it('recognizes npm propagation misses as transient lookup failures', () => {
assert.equal(
isTransientPublishedPackageLookupFailure({
code: 1,
stdout: '',
stderr: [
'npm error code ETARGET',
'npm error notarget No matching version found for @kaelio/ktx@0.1.0-rc.4.',
].join('\n'),
}),
true,
);
});
it('does not retry unrelated command failures', () => {
assert.equal(
isTransientPublishedPackageLookupFailure({
code: 1,
stdout: '',
stderr: 'npm error code EOTP',
}),
false,
);
});
});
describe('published package smoke command construction', () => {
const config = {
enabled: true,
@ -244,6 +273,13 @@ describe('published package smoke command construction', () => {
);
});
it('allows native dependency build scripts in clean pnpm smoke installs', () => {
assert.equal(
publishedPackageSmokePnpmWorkspaceYaml(),
['packages:', ' - "."', 'allowBuilds:', ' better-sqlite3: true', ''].join('\n'),
);
});
it('exposes the smoke through the package release script', async () => {
const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));