Skip to content

Commit a0fb1e9

Browse files
committed
fix: tolerate npm registry propagation delays [policy]
Change-Id: I2fda5eedb254b1b2caa0a624b45a96d496db530d
1 parent d213ad3 commit a0fb1e9

4 files changed

Lines changed: 97 additions & 13 deletions

File tree

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ jobs:
121121
registry-ready:
122122
needs: [preflight, publish]
123123
runs-on: ubuntu-latest
124+
timeout-minutes: 20
124125
permissions:
125126
contents: read
126127
steps:

scripts/open-source.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ describe("open-source repository invariants", () => {
150150
expect(workflow).toContain('git config user.name "github-actions[bot]"');
151151
expect(workflow).toContain("github-actions[bot]@users.noreply.github.com");
152152
expect(workflow).toContain("registry-ready:");
153+
expect(workflow).toMatch(/registry-ready:[\s\S]*?timeout-minutes: 20/);
153154
expect(workflow).toContain("post-release-consumer:");
154155
expect(workflow).toContain("os: [ubuntu-latest, windows-latest, macos-latest]");
155156
expect(workflow).toContain("node: [22, 24]");

scripts/release/consumer-smoke.test.ts

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, test } from "bun:test";
2-
import { commonRegistryVersion, packageName, registryVersion } from "./consumer-smoke.ts";
2+
import { commonRegistryVersion, packageName, registryVersion, waitForRegistry } from "./consumer-smoke.ts";
33

44
describe("published consumer smoke", () => {
55
test("builds canonical package names", () => {
@@ -8,7 +8,9 @@ describe("published consumer smoke", () => {
88

99
test("reads an exact registry version", () => {
1010
expect(registryVersion('"1.2.3"', "@openagentpack/sdk")).toBe("1.2.3");
11-
expect(() => registryVersion("[]", "@openagentpack/sdk")).toThrow("invalid version");
11+
const invalidVersion = () => registryVersion("[]", "@openagentpack/sdk");
12+
expect(invalidVersion).toThrow("invalid version for @openagentpack/sdk");
13+
expect(invalidVersion).toThrow("received: []");
1214
});
1315

1416
test("requires the fixed release group to resolve one version", () => {
@@ -25,4 +27,71 @@ describe("published consumer smoke", () => {
2527
]),
2628
).toThrow("versions must match");
2729
});
30+
31+
test("waits through transient registry propagation", async () => {
32+
let queries = 0;
33+
const delays: number[] = [];
34+
const version = await waitForRegistry(
35+
"1.2.3",
36+
{ attempts: 3, delayMs: 25 },
37+
{
38+
resolve: () => {
39+
queries++;
40+
if (queries < 3) throw new Error("@openagentpack/sdk is not visible yet");
41+
return "1.2.3";
42+
},
43+
sleep: async (delayMs) => {
44+
delays.push(delayMs);
45+
},
46+
log: () => {},
47+
},
48+
);
49+
50+
expect(version).toBe("1.2.3");
51+
expect(queries).toBe(3);
52+
expect(delays).toEqual([25, 25]);
53+
});
54+
55+
test("reports the transient registry error while waiting", async () => {
56+
let queries = 0;
57+
const messages: string[] = [];
58+
await waitForRegistry(
59+
"1.2.3",
60+
{ attempts: 2, delayMs: 0 },
61+
{
62+
resolve: () => {
63+
queries++;
64+
if (queries === 1) throw new Error("@openagentpack/sdk returned []");
65+
return "1.2.3";
66+
},
67+
sleep: async () => {},
68+
log: (message) => messages.push(message),
69+
},
70+
);
71+
72+
expect(messages[0]).toContain("@openagentpack/sdk returned []");
73+
});
74+
75+
test("allows fifteen minutes for registry propagation by default", async () => {
76+
let queries = 0;
77+
let delays = 0;
78+
const waiting = waitForRegistry(
79+
"1.2.3",
80+
{},
81+
{
82+
resolve: () => {
83+
queries++;
84+
throw new Error("not visible");
85+
},
86+
sleep: async () => {
87+
delays++;
88+
},
89+
log: () => {},
90+
},
91+
);
92+
93+
await expect(waiting).rejects.toThrow("not visible");
94+
expect(queries).toBe(90);
95+
expect(delays).toBe(89);
96+
});
2897
});

scripts/release/consumer-smoke.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export function registryVersion(raw: string, name: string): string {
2525
} catch {
2626
// Fall through to the stable error below.
2727
}
28-
throw new Error(`npm registry returned an invalid version for ${name}`);
28+
const summary = raw.trim().replaceAll(/\s+/g, " ").slice(0, 200) || "<empty>";
29+
throw new Error(`npm registry returned an invalid version for ${name}; received: ${summary}`);
2930
}
3031

3132
export function commonRegistryVersion(entries: ReadonlyArray<{ name: string; version: string }>): string {
@@ -46,11 +47,14 @@ function run(command: string[], cwd: string, stdout: "inherit" | "pipe" = "inher
4647
}
4748

4849
function npmVersion(name: string, requested: string): string {
49-
const result = Bun.spawnSync(["npm", "view", `${name}@${requested}`, "version", "--json", "--registry", REGISTRY], {
50-
cwd: root,
51-
stdout: "pipe",
52-
stderr: "pipe",
53-
});
50+
const result = Bun.spawnSync(
51+
["npm", "view", `${name}@${requested}`, "version", "--json", "--prefer-online", "--registry", REGISTRY],
52+
{
53+
cwd: root,
54+
stdout: "pipe",
55+
stderr: "pipe",
56+
},
57+
);
5458
if (result.exitCode !== 0) {
5559
throw new Error(result.stderr.toString().trim() || `${name}@${requested} is not visible in the npm registry`);
5660
}
@@ -72,20 +76,29 @@ export function resolvePublishedVersion(requested: string): string {
7276
export async function waitForRegistry(
7377
requested: string,
7478
options: { attempts?: number; delayMs?: number } = {},
79+
dependencies: {
80+
resolve?: (requested: string) => string;
81+
sleep?: (delayMs: number) => Promise<void>;
82+
log?: (message: string) => void;
83+
} = {},
7584
): Promise<string> {
76-
const attempts = options.attempts ?? 30;
85+
const attempts = options.attempts ?? 90;
7786
const delayMs = options.delayMs ?? 10_000;
87+
const resolve = dependencies.resolve ?? resolvePublishedVersion;
88+
const sleep = dependencies.sleep ?? Bun.sleep;
89+
const log = dependencies.log ?? console.log;
7890
let lastError: unknown;
7991
for (let attempt = 1; attempt <= attempts; attempt++) {
8092
try {
81-
const version = resolvePublishedVersion(requested);
82-
console.log(`✓ All published packages are visible at ${version}`);
93+
const version = resolve(requested);
94+
log(`✓ All published packages are visible at ${version}`);
8395
return version;
8496
} catch (error) {
8597
lastError = error;
8698
if (attempt === attempts) break;
87-
console.log(`Registry not ready (${attempt}/${attempts}); retrying in ${delayMs / 1000}s...`);
88-
await Bun.sleep(delayMs);
99+
const reason = error instanceof Error ? error.message : String(error);
100+
log(`Registry not ready (${attempt}/${attempts}): ${reason}; retrying in ${delayMs / 1000}s...`);
101+
await sleep(delayMs);
89102
}
90103
}
91104
throw lastError;

0 commit comments

Comments
 (0)