mirror of
https://github.com/CopilotKit/CopilotKit.git
synced 2026-09-14 16:26:20 +08:00
fix(release): retry npm registry propagation
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { loadPublishedChannelsManifest } from "./channels-registry.js";
|
||||
|
||||
function npm404(): Error & { stderr: string } {
|
||||
return Object.assign(new Error("npm view failed"), {
|
||||
stderr: "npm error code E404",
|
||||
});
|
||||
}
|
||||
|
||||
describe("loadPublishedChannelsManifest", () => {
|
||||
it("waits for a just-published package to become visible", async () => {
|
||||
const manifest = {
|
||||
name: "@copilotkit/channels-whatsapp",
|
||||
version: "0.9.1",
|
||||
};
|
||||
const lookup = vi
|
||||
.fn<() => string>()
|
||||
.mockImplementationOnce(() => {
|
||||
throw npm404();
|
||||
})
|
||||
.mockReturnValueOnce(JSON.stringify(manifest));
|
||||
const sleep = vi.fn<() => Promise<void>>().mockResolvedValue(undefined);
|
||||
const onRetry = vi.fn();
|
||||
|
||||
await expect(
|
||||
loadPublishedChannelsManifest("@copilotkit/channels-whatsapp", "0.9.1", {
|
||||
lookup,
|
||||
sleep,
|
||||
onRetry,
|
||||
maxAttempts: 3,
|
||||
retryDelayMs: 10_000,
|
||||
}),
|
||||
).resolves.toEqual(manifest);
|
||||
|
||||
expect(lookup).toHaveBeenCalledTimes(2);
|
||||
expect(sleep).toHaveBeenCalledOnce();
|
||||
expect(sleep).toHaveBeenCalledWith(10_000);
|
||||
expect(onRetry).toHaveBeenCalledWith(
|
||||
"@copilotkit/channels-whatsapp@0.9.1 is not visible on npm yet; retrying in 10s (1/3).",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not retry non-404 registry failures", async () => {
|
||||
const failure = Object.assign(new Error("npm view failed"), {
|
||||
stderr: "npm error code E429",
|
||||
});
|
||||
const lookup = vi.fn<() => string>(() => {
|
||||
throw failure;
|
||||
});
|
||||
const sleep = vi.fn<() => Promise<void>>();
|
||||
|
||||
await expect(
|
||||
loadPublishedChannelsManifest("@copilotkit/channels-core", "0.9.1", {
|
||||
lookup,
|
||||
sleep,
|
||||
}),
|
||||
).rejects.toBe(failure);
|
||||
expect(sleep).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("fails with release guidance after the visibility window expires", async () => {
|
||||
const lookup = vi.fn<() => string>(() => {
|
||||
throw npm404();
|
||||
});
|
||||
|
||||
await expect(
|
||||
loadPublishedChannelsManifest("@copilotkit/channels-core", "0.9.1", {
|
||||
lookup,
|
||||
sleep: async () => {},
|
||||
onRetry: () => {},
|
||||
maxAttempts: 2,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"registry is missing @copilotkit/channels-core@0.9.1 after 2 attempts; publish channels-core and every adapter before publishing @copilotkit/channels",
|
||||
);
|
||||
expect(lookup).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { PackedManifest } from "./channels-umbrella.js";
|
||||
|
||||
const DEFAULT_MAX_ATTEMPTS = 31;
|
||||
const DEFAULT_RETRY_DELAY_MS = 10_000;
|
||||
|
||||
interface LoadPublishedChannelsManifestOptions {
|
||||
lookup: () => string;
|
||||
maxAttempts?: number;
|
||||
retryDelayMs?: number;
|
||||
sleep?: (delayMs: number) => Promise<void>;
|
||||
onRetry?: (message: string) => void;
|
||||
}
|
||||
|
||||
function isRegistryMissing(error: unknown): boolean {
|
||||
const stderr =
|
||||
typeof error === "object" && error !== null && "stderr" in error
|
||||
? String(error.stderr)
|
||||
: "";
|
||||
return stderr.includes("E404");
|
||||
}
|
||||
|
||||
function sleep(delayMs: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
}
|
||||
|
||||
export async function loadPublishedChannelsManifest(
|
||||
name: string,
|
||||
version: string,
|
||||
{
|
||||
lookup,
|
||||
maxAttempts = DEFAULT_MAX_ATTEMPTS,
|
||||
retryDelayMs = DEFAULT_RETRY_DELAY_MS,
|
||||
sleep: wait = sleep,
|
||||
onRetry = console.warn,
|
||||
}: LoadPublishedChannelsManifestOptions,
|
||||
): Promise<PackedManifest> {
|
||||
const spec = `${name}@${version}`;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
return JSON.parse(lookup()) as PackedManifest;
|
||||
} catch (error) {
|
||||
if (!isRegistryMissing(error)) throw error;
|
||||
if (attempt === maxAttempts) {
|
||||
throw new Error(
|
||||
`registry is missing ${spec} after ${maxAttempts} attempts; publish channels-core and every adapter before publishing @copilotkit/channels`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
|
||||
onRetry(
|
||||
`${spec} is not visible on npm yet; retrying in ${retryDelayMs / 1000}s (${attempt}/${maxAttempts}).`,
|
||||
);
|
||||
await wait(retryDelayMs);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`unreachable registry lookup state for ${spec}`);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
packPackage,
|
||||
workspaceDependencyClosure,
|
||||
} from "./lib/pack-workspace.js";
|
||||
import { loadPublishedChannelsManifest } from "./lib/channels-registry.js";
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
|
||||
@@ -80,30 +81,10 @@ function packLocalFamily(tarballDir: string): {
|
||||
return { manifests, tarballs };
|
||||
}
|
||||
|
||||
function loadRegistryManifest(name: string, version: string): PackedManifest {
|
||||
try {
|
||||
return JSON.parse(
|
||||
capture("npm", ["view", `${name}@${version}`, "--json"]),
|
||||
) as PackedManifest;
|
||||
} catch (error) {
|
||||
const stderr =
|
||||
typeof error === "object" && error !== null && "stderr" in error
|
||||
? String(error.stderr)
|
||||
: "";
|
||||
if (stderr.includes("E404")) {
|
||||
throw new Error(
|
||||
`registry is missing ${name}@${version}; publish channels-core and every adapter before publishing @copilotkit/channels`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function loadRegistrySnapshot(tarballDir: string): {
|
||||
async function loadRegistrySnapshot(tarballDir: string): Promise<{
|
||||
manifests: Map<string, PackedManifest>;
|
||||
tarballs: Map<string, string>;
|
||||
} {
|
||||
}> {
|
||||
const umbrellaName = "@copilotkit/channels";
|
||||
const { manifest: umbrella, tarball } = packPackage(umbrellaName, tarballDir);
|
||||
const manifests = new Map<string, PackedManifest>([[umbrellaName, umbrella]]);
|
||||
@@ -115,7 +96,12 @@ function loadRegistrySnapshot(tarballDir: string): {
|
||||
throw new Error(`packed umbrella is missing ${name}`);
|
||||
}
|
||||
|
||||
manifests.set(name, loadRegistryManifest(name, version));
|
||||
manifests.set(
|
||||
name,
|
||||
await loadPublishedChannelsManifest(name, version, {
|
||||
lookup: () => capture("npm", ["view", `${name}@${version}`, "--json"]),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -237,7 +223,7 @@ void [createChannel, slack, teams, discord, telegram, whatsapp, view];
|
||||
);
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
async function main(): Promise<void> {
|
||||
const registryMode = process.argv.includes("--registry");
|
||||
const temp = mkdtempSync(join(tmpdir(), "channels-umbrella-"));
|
||||
const tarballDir = join(temp, "tarballs");
|
||||
@@ -247,7 +233,7 @@ function main(): void {
|
||||
|
||||
try {
|
||||
const { manifests, tarballs } = registryMode
|
||||
? loadRegistrySnapshot(tarballDir)
|
||||
? await loadRegistrySnapshot(tarballDir)
|
||||
: packLocalFamily(tarballDir);
|
||||
const problems = validatePackedManifests(manifests);
|
||||
if (problems.length) {
|
||||
@@ -277,9 +263,7 @@ function main(): void {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user