Skip to content

Commit e2b0c41

Browse files
committed
feat(showcase): add build-outputs parser for structured build-matrix results
1 parent 7b513c8 commit e2b0c41

2 files changed

Lines changed: 123 additions & 0 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
parseBuildOutputs,
4+
successSet,
5+
type BuildOutcome,
6+
type ServiceBuildResult,
7+
} from "../build-outputs";
8+
9+
const sample: ServiceBuildResult[] = [
10+
{ service: "showcase-mastra", status: "success" },
11+
{ service: "showcase-ag2", status: "failure" },
12+
{ service: "shell-docs", status: "skipped" },
13+
{ service: "showcase-aimock", status: "success" },
14+
];
15+
16+
describe("build-outputs", () => {
17+
it("parses a JSON array of {service,status} entries", () => {
18+
const json = JSON.stringify(sample);
19+
expect(parseBuildOutputs(json)).toEqual(sample);
20+
});
21+
22+
it("throws on malformed JSON", () => {
23+
expect(() => parseBuildOutputs("not json")).toThrow(/parse/i);
24+
});
25+
26+
it("throws on entries missing fields", () => {
27+
expect(() => parseBuildOutputs(JSON.stringify([{ service: "x" }]))).toThrow(
28+
/status/i,
29+
);
30+
});
31+
32+
it("throws on an unknown status value", () => {
33+
const bad = JSON.stringify([{ service: "x", status: "weird" }]);
34+
expect(() => parseBuildOutputs(bad)).toThrow(/status/i);
35+
});
36+
37+
it("successSet returns only services with status === 'success'", () => {
38+
expect(successSet(sample).sort()).toEqual(
39+
["showcase-aimock", "showcase-mastra"].sort(),
40+
);
41+
});
42+
43+
it("successSet returns empty when no services succeeded", () => {
44+
const allFailed: ServiceBuildResult[] = [
45+
{ service: "a", status: "failure" },
46+
{ service: "b", status: "failure" },
47+
];
48+
expect(successSet(allFailed)).toEqual([]);
49+
});
50+
51+
it("type BuildOutcome enumerates success|failure|skipped", () => {
52+
const outcomes: BuildOutcome[] = ["success", "failure", "skipped"];
53+
expect(outcomes).toHaveLength(3);
54+
});
55+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* build-outputs.ts — Parse + merge the structured per-slot build results
3+
* emitted by `showcase_build.yml`. Each matrix slot in the build job
4+
* uploads a per-slot artifact named `build-result-<dispatch_name>`
5+
* containing a single `result.json` payload of the shape
6+
* `{service: "<dispatch_name>", status: "success"|"failure"|"skipped"}`.
7+
* The aggregate-build-results job downloads every `build-result-*`
8+
* artifact and merges the payloads via `mergeBuildResultFiles` below;
9+
* the resulting array is uploaded as the canonical `build-results`
10+
* artifact for cross-workflow consumption. The deploy workflow (and the
11+
* redeploy guard) read this list instead of parsing job names.
12+
*/
13+
14+
export type BuildOutcome = "success" | "failure" | "skipped";
15+
16+
export interface ServiceBuildResult {
17+
service: string;
18+
status: BuildOutcome;
19+
}
20+
21+
const VALID_STATUSES: ReadonlySet<BuildOutcome> = new Set([
22+
"success",
23+
"failure",
24+
"skipped",
25+
]);
26+
27+
export function parseBuildOutputs(raw: string): ServiceBuildResult[] {
28+
let parsed: unknown;
29+
try {
30+
parsed = JSON.parse(raw);
31+
} catch (e) {
32+
throw new Error(
33+
`Failed to parse build outputs JSON: ${
34+
e instanceof Error ? e.message : String(e)
35+
}`,
36+
);
37+
}
38+
if (!Array.isArray(parsed)) {
39+
throw new Error("Build outputs must be a JSON array");
40+
}
41+
const results: ServiceBuildResult[] = [];
42+
for (const entry of parsed) {
43+
if (
44+
typeof entry !== "object" ||
45+
entry === null ||
46+
typeof (entry as { service?: unknown }).service !== "string"
47+
) {
48+
throw new Error(
49+
`Build outputs entry missing required string field "service": ${JSON.stringify(entry)}`,
50+
);
51+
}
52+
const status = (entry as { status?: unknown }).status;
53+
if (typeof status !== "string" || !VALID_STATUSES.has(status as BuildOutcome)) {
54+
throw new Error(
55+
`Build outputs entry has invalid "status" (must be success|failure|skipped): ${JSON.stringify(entry)}`,
56+
);
57+
}
58+
results.push({
59+
service: (entry as { service: string }).service,
60+
status: status as BuildOutcome,
61+
});
62+
}
63+
return results;
64+
}
65+
66+
export function successSet(results: ServiceBuildResult[]): string[] {
67+
return results.filter((r) => r.status === "success").map((r) => r.service);
68+
}

0 commit comments

Comments
 (0)