fix(cli): report failed tag installations to scripts and agents (#6086)

* fix(cli): report failed tag installations to scripts and agents

* fix(cli): restore status markers in tag install summaries
This commit is contained in:
Akshar Patel
2026-10-24 07:18:42 +00:00
committed by GitHub
parent 8b1bf6b914
commit 3596a7f22b
3 changed files with 101 additions and 5 deletions
+4
View File
@@ -150,6 +150,10 @@ The positional name is a registry item, or a tag — a tag installs every block
carrying it. `--dir` picks the project directory, `--json` prints the written
files and paste snippet, and `--no-clipboard` skips the clipboard copy for CI.
If any block in a tag fails to install, `add` keeps the successful installs and
exits with code 1. With `--json`, `ok` is `false` and `errors` lists each failed
block's `name` and `error`; `installed` lists the blocks that succeeded.
Every install produces files plus a **paste snippet**: the `<iframe>` tag for a
block, or the fragment path for a component. It goes to your clipboard by
default. Naming an example instead of a registry item (`add warm-grain`) gets
+83
View File
@@ -618,6 +618,89 @@ describe("tagAddJson", () => {
tag: "lower-thirds",
installed: ["a", "b"],
warnings: ["b: no id"],
errors: [],
});
});
});
describe("tag installation failures", () => {
let dir: string;
beforeEach(() => {
dir = tmp();
writeRegistryConfig(dir);
});
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
rmSync(dir, { recursive: true, force: true });
});
it.each([
{ names: ["my-block"], failed: ["my-block"], installed: [], json: true },
{
names: ["my-block", "deprecated-block"],
failed: ["my-block"],
installed: ["deprecated-block"],
json: true,
},
{ names: ["my-block"], failed: [], installed: ["my-block"], json: true },
{ names: ["my-block"], failed: ["my-block"], installed: [], json: false },
{ names: ["my-block"], failed: [], installed: ["my-block"], json: false },
])("reports failed downloads and keeps successful installs: %j", async (scenario) => {
vi.stubGlobal(
"fetch",
vi.fn(async (input: string | URL) => {
const url = new URL(input);
if (url.pathname.endsWith("/registry.json")) {
return Response.json({
...MANIFEST,
items: MANIFEST.items.filter((item) => scenario.names.includes(item.name)),
});
}
const name = scenario.names.find((item) => url.pathname.includes(`/blocks/${item}/`));
if (!name) return new Response("not found", { status: 404 });
if (url.pathname.endsWith("/registry-item.json")) {
return Response.json({ ...ITEM_BY_NAME[name], tags: ["test-tag"] });
}
if (scenario.failed.includes(name)) return new Response("not found", { status: 404 });
return new Response(FILE_BODIES[`${name}.html`]);
}),
);
const log = vi.spyOn(console, "log").mockImplementation(() => {});
const run = runCittyCommand(addCommand, {
rawArgs: ["test-tag", "--dir", dir, "--no-clipboard", ...(scenario.json ? ["--json"] : [])],
});
if (scenario.failed.length > 0) {
await expect(run).rejects.toMatchObject({ result: { exitCode: 1, presented: true } });
} else {
await expect(run).resolves.toBeDefined();
}
if (scenario.json) {
expect(log).toHaveBeenCalledTimes(1);
expect(JSON.parse(String(log.mock.calls[0]?.[0]))).toMatchObject({
ok: scenario.failed.length === 0,
tag: "test-tag",
installed: scenario.installed,
errors: scenario.failed.map((name) => ({
name,
error: expect.stringContaining("HTTP 404"),
})),
});
} else {
expect(log).toHaveBeenLastCalledWith(
`${scenario.failed.length > 0 ? "✗" : "✓"} Installed ${scenario.installed.length}/${scenario.names.length} blocks`,
);
if (scenario.failed.length > 0) {
expect(log.mock.calls.flat().join(" ")).toContain("HTTP 404");
}
}
for (const name of scenario.names) {
expect(existsSync(join(dir, "compositions", `${name}.html`))).toBe(
scenario.installed.includes(name),
);
}
});
});
+14 -5
View File
@@ -146,13 +146,18 @@ export function buildSnippet(
return "";
}
/** `add <tag> --json` output: each item's warnings, prefixed with its name. */
export function tagAddJson(tag: string, results: RunAddResult[]) {
/** `add <tag> --json` output: successful installs and each item's warnings or failure. */
export function tagAddJson(
tag: string,
results: RunAddResult[],
errors: Array<{ name: string; error: string }> = [],
) {
return {
ok: true,
ok: errors.length === 0,
tag,
installed: results.map((r) => r.name),
warnings: results.flatMap((r) => r.warnings.map((w) => `${r.name}: ${w}`)),
errors,
};
}
@@ -584,6 +589,7 @@ export default defineCommand({
}
const results: RunAddResult[] = [];
const errors: Array<{ name: string; error: string }> = [];
for (const item of items) {
try {
const result = await runAdd({
@@ -600,16 +606,19 @@ export default defineCommand({
if (!json) console.log(` ${c.success("✓")} ${result.name}`);
} catch (err) {
const reason = err instanceof Error ? err.message : String(err);
errors.push({ name: item.name, error: reason });
if (!json) console.log(` ${c.error("✗")} ${item.name} (skipped: ${reason})`);
}
}
if (json) {
console.log(JSON.stringify(tagAddJson(args.name, results)));
console.log(JSON.stringify(tagAddJson(args.name, results, errors)));
} else {
console.log("");
console.log(`${c.success("✓")} Installed ${results.length}/${items.length} blocks`);
const summary = `Installed ${results.length}/${items.length} blocks`;
console.log(`${errors.length > 0 ? c.error("✗") : c.success("✓")} ${summary}`);
}
if (errors.length > 0) failCommand();
}
},
});