Skip to content

Commit 91da631

Browse files
authored
fix(catalog): surface real extraction errors and self-heal CI labels (#68)
extractModelCatalog kept the first candidate failure private and threw a generic 'Could not evaluate model catalog', so a new minified bundle shape (e.g. the $R is not defined regression) required manual reproduction to diagnose. The error now names the missing binding. catalog-sync.yml failed to open catalog-break issues whenever the repo labels were missing (issues disabled at the time); the workflow now force-creates catalog-break and automation labels before every sync run.
1 parent d053bc5 commit 91da631

4 files changed

Lines changed: 44 additions & 3 deletions

File tree

‎.github/workflows/catalog-sync.yml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ jobs:
3131
- name: Install dependencies
3232
run: bun install --frozen-lockfile
3333

34+
- name: Ensure automation labels
35+
env:
36+
GH_TOKEN: ${{ secrets.RELEASE_SYNC_TOKEN || secrets.CATALOG_PUSH_TOKEN || secrets.GITHUB_TOKEN }}
37+
run: |
38+
gh label create catalog-break --force --color B60205 \
39+
--description "Catalog model extraction failed in CI" || true
40+
gh label create automation --force --color 0E8A16 \
41+
--description "Automated bot activity" || true
42+
3443
- name: Open catalog PR
3544
env:
3645
CI: "true"

‎src/catalog.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -396,16 +396,20 @@ export function extractModelCatalog(
396396
}
397397
if (candidates.length === 0) throw new Error("Could not locate model catalog object");
398398

399+
let lastError: unknown = null;
399400
for (const raw of candidates) {
400401
try {
401402
const value = evaluateWithContext(normalizeForEval(raw), ctx);
402403
if (isModelCatalog(value)) return value;
403-
} catch {
404-
// try next span
404+
} catch (err) {
405+
// keep the first real failure (e.g. "$R is not defined") — it names the
406+
// missing binding; later candidates usually fail on the same cause
407+
lastError ??= err;
405408
}
406409
}
407410

408-
throw new Error("Could not evaluate model catalog");
411+
const detail = lastError instanceof Error ? `: ${lastError.message}` : "";
412+
throw new Error(`Could not evaluate model catalog${detail}`);
409413
}
410414

411415
function isCostMap(value: unknown): value is Record<string, CostEntry[]> {

‎tests/unit/catalog.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,17 @@ describe("loadCatalogFromBundle", () => {
273273
expect(sonnet!.reasoningEfforts).toEqual(["low", "high"]);
274274
});
275275

276+
test("surfaces the real eval error when no candidate evaluates", () => {
277+
// Bundle shape where the catalog references an unbindable identifier:
278+
// the thrown error must name it, not the generic extraction failure.
279+
const source = [
280+
'var KR="chatComplete",qR="responses";',
281+
'var Sn={SONNET_4_6:{id:"claude-sonnet-4-6",provider:$TOTALLY_MISSING,spec:KR,label:"Sonnet",name:"Claude Sonnet 4.6",description:"d"},GPT_X:{id:"gpt-5.5",provider:"openai",spec:KR,label:"GPT",name:"GPT-5.5",description:"d"}};',
282+
].join("");
283+
284+
expect(() => loadCatalogFromBundle(source)).toThrow(/\$TOTALLY_MISSING/);
285+
});
286+
276287
test("returns models when cost extraction fails", () => {
277288
const source = [
278289
'(Wt={ANTHROPIC:"anthropic",OPENAI:"openai",VERCEL_AI_GATEWAY:"vercel-ai-gateway"});',

‎tests/unit/release-workflow.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,23 @@ describe("catalog-sync.yml", () => {
153153
expect(blob).not.toContain("semantic-release");
154154
expect(blob).not.toContain("publish-if-needed");
155155
});
156+
157+
test("creates the catalog-break labels before syncing so break issues never fail", () => {
158+
const wf = Bun.YAML.parse(read(".github/workflows/catalog-sync.yml")) as {
159+
jobs: { sync: { steps: Array<{ name?: string; run?: string }> } };
160+
};
161+
const labelStep = wf.jobs.sync.steps.find((s) => s.name === "Ensure automation labels");
162+
expect(labelStep).toBeDefined();
163+
const run = labelStep!.run ?? "";
164+
expect(run).toContain("gh label create catalog-break");
165+
expect(run).toContain("gh label create automation");
166+
expect(run).toContain("--force");
167+
// must run before the sync step that opens break issues
168+
const steps = wf.jobs.sync.steps;
169+
expect(steps.findIndex((s) => s.name === "Ensure automation labels")).toBeLessThan(
170+
steps.findIndex((s) => (s.run ?? "").includes("catalog-sync-ci.ts")),
171+
);
172+
});
156173
});
157174

158175
describe("release.config.cjs", () => {

0 commit comments

Comments
 (0)