Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions packages/registry/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
listDefinitions,
} from "./definition.js";
import { checkPackageExists, publishPackage } from "./publish.js";
import { discoverVersions } from "./version-check.js";
import { type AvailableVersion, discoverVersions } from "./version-check.js";

const DEFAULT_REGISTRY_DIR = resolve(
import.meta.dirname,
Expand Down Expand Up @@ -225,10 +225,25 @@ program
const failures: { id: string; error: string }[] = [];

for (const def of definitions) {
const versions = await discoverVersions(def, {
since: opts.since ? Number(opts.since) : undefined,
latest: opts.latest ? Number(opts.latest) : undefined,
});
// Discovery talks to a third-party registry API, so it can fail for
// reasons that have nothing to do with this package — a 404 for a renamed
// module, a 429, an upstream outage. Record it like a build failure
// instead of letting it throw out of the loop: definitions are processed
// in sorted order, so an unguarded throw here silently abandons every
// package after this one and prints no summary at all.
let versions: AvailableVersion[];
try {
versions = await discoverVersions(def, {
since: opts.since ? Number(opts.since) : undefined,
latest: opts.latest ? Number(opts.latest) : undefined,
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const id = `${def.registry}/${def.name}`;
console.error(` FAILED ${id} (version discovery): ${message}`);
failures.push({ id, error: message });
continue;
}

for (const ver of versions) {
const id = `${def.registry}/${def.name}@${ver.version}`;
Expand Down
88 changes: 88 additions & 0 deletions packages/registry/src/publish-all.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { execFileSync } from "node:child_process";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";

/**
* `publish-all` walks every definition in sorted order. A throw that escapes the
* loop abandons every package after it, so these run the real CLI and assert on
* what the nightly job would actually see.
*
* Definitions use a registry with no version fetcher, which makes discovery
* throw before any network call.
*/
describe("publish-all — a failing definition must not abandon the rest", () => {
let dir: string;
let out: string;

const define = (registry: string, name: string): void => {
mkdirSync(join(dir, registry), { recursive: true });
writeFileSync(
join(dir, registry, `${name}.yaml`),
[
`name: ${name}`,
`description: "test"`,
`versions:`,
` - min_version: "1.0.0"`,
` source:`,
` type: git`,
` url: https://example.invalid/${name}`,
` docs_path: docs`,
"",
].join("\n"),
);
};

const runPublishAll = (): { status: number; output: string } => {
try {
const stdout = execFileSync(
"npx",
["tsx", "src/cli.ts", "publish-all", "--dir", dir, "--output", out],
{ cwd: process.cwd(), encoding: "utf8", stdio: "pipe" },
);
return { status: 0, output: stdout };
} catch (err) {
const e = err as { status?: number; stdout?: string; stderr?: string };
return {
status: e.status ?? 1,
output: `${e.stdout ?? ""}${e.stderr ?? ""}`,
};
}
};

beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "registry-defs-"));
out = mkdtempSync(join(tmpdir(), "registry-out-"));
});

afterEach(() => {
rmSync(dir, { recursive: true, force: true });
rmSync(out, { recursive: true, force: true });
});

it("records a discovery failure and still reaches the summary", () => {
define("unsupported", "aaa");
define("unsupported", "zzz");

const { status, output } = runPublishAll();

// Both are recorded, not thrown — and the run reports rather than crashing.
expect(output).toContain("--- Summary ---");
expect(output).toContain("Failed: 2");
expect(output).toContain("unsupported/aaa");
expect(output).toContain("unsupported/zzz");
expect(status).not.toBe(0);
}, 60_000);

it("does not abandon definitions sorted after a failing one", () => {
define("unsupported", "aaa");
define("unsupported", "mmm");
define("unsupported", "zzz");

const { output } = runPublishAll();

// The bug this guards: `aaa` throwing meant `mmm` and `zzz` were never seen.
expect(output).toContain("Failed: 3");
}, 60_000);
});