Skip to content

Commit 1fe9bd3

Browse files
aspiersclaude
andcommitted
fix(scripts): run lex codegen via a Node runner, not a shell pipe
`set -o pipefail` is NOT portable: GitHub's Ubuntu `sh` (dash) rejects it (`set: Illegal option -o pipefail`), which broke CI. And no `xargs` option replaces it — `-r` only skips empty input, it does not propagate a producer's non-zero exit, so a crashed list-generator still yields a "successful" pipeline. Replace the pipe entirely with scripts/run-lex-codegen.js: a single Node process that builds the permission-set-excluded file list and spawns `lex <subcommand> <args> <files…>` with the list as argv, forwarding lex's exit code. One authoritative exit code, no shell builtins, no ARG_MAX (argv, not a command line), no pipefail. Empty list or spawn failure → non-zero exit. (Renamed from codegen-lexicon-files.js, which only printed the list.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c327ada commit 1fe9bd3

3 files changed

Lines changed: 89 additions & 67 deletions

File tree

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@
4242
"check": "npm run gen-api && npm run lint && npm run typecheck && npm run build && npm run test",
4343
"build": "rollup -c && npm run build:types",
4444
"build:types": "tsc --project tsconfig.build.json",
45-
"//permissions": "permission-set lexicons are published as-is but have no TS shape; lex gen-* cannot codegen them. scripts/codegen-lexicon-files.js emits the NUL-delimited lexicon list to codegen, excluding permission-sets by content (single source of truth shared with generate-exports.js). Piped into `xargs -0 -r` under `set -o pipefail`: pipefail propagates a producer failure (xargs alone cannot — it would run lex on partial/empty input and report success); -r skips lex if the list is somehow empty; xargs avoids ARG_MAX.",
46-
"gen-api": "set -o pipefail; node ./scripts/codegen-lexicon-files.js | xargs -0 -r lex gen-api --yes ./generated && npm run gen-index",
47-
"gen-md": "set -o pipefail; node ./scripts/codegen-lexicon-files.js | xargs -0 -r lex gen-md --yes ./lexicons.md",
45+
"//permissions": "permission-set lexicons are published as-is but have no TS shape; lex gen-* cannot codegen them. scripts/run-lex-codegen.js runs a lex subcommand over all lexicons EXCEPT permission-sets (excluded by content — the same check generate-exports.js uses). It is a single Node process that spawns lex with the file list as argv and forwards lex's exit code, so a producer failure propagates without `set -o pipefail` (not portable across npm's sh) and there is no ARG_MAX limit.",
46+
"gen-api": "node ./scripts/run-lex-codegen.js gen-api --yes ./generated && npm run gen-index",
47+
"gen-md": "node ./scripts/run-lex-codegen.js gen-md --yes ./lexicons.md",
4848
"gen-schemas-md": "node ./scripts/generate-schemas.js",
4949
"//gen-ts": "UNUSED - use gen-api instead",
50-
"gen-ts": "set -o pipefail; node ./scripts/codegen-lexicon-files.js | xargs -0 -r lex gen-ts-obj > generated/DO-NOT-USE-lexicons.ts",
50+
"gen-ts": "node ./scripts/run-lex-codegen.js gen-ts-obj > generated/DO-NOT-USE-lexicons.ts",
5151
"gen-index": "node ./scripts/generate-exports.js",
5252
"lex": "lex",
5353
"test": "vitest run",

scripts/codegen-lexicon-files.js

Lines changed: 0 additions & 63 deletions
This file was deleted.

scripts/run-lex-codegen.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Run a `lex` codegen subcommand over every lexicon under `lexicons/` EXCEPT
5+
* permission-set lexicons.
6+
*
7+
* `lex gen-api` / `gen-md` / `gen-ts` cannot codegen a `type: "permission-set"`
8+
* def — it has no TypeScript shape and the CLI throws on it. Permission sets are
9+
* published as-is, so they are excluded from codegen. The exclusion is by
10+
* **content** (`defs.main.type === "permission-set"`), the same check
11+
* `scripts/generate-exports.js` uses — so the two cannot drift as files move.
12+
*
13+
* Why a Node runner rather than a shell pipeline:
14+
* - `lex … $(node …)` swallows a producer failure (command substitution discards
15+
* its exit code) and can hit ARG_MAX.
16+
* - `node … | xargs -0 lex …` needs `set -o pipefail` to propagate a producer
17+
* failure, and `pipefail` is NOT portable across the `sh` npm runs scripts
18+
* with (rejected by the dash on GitHub's Ubuntu runners).
19+
* This runner is a single process with one authoritative exit code, passes the
20+
* file list to `lex` as argv (no shell, no ARG_MAX), and forwards lex's exit
21+
* code. No shell builtins involved.
22+
*
23+
* Usage (in package.json):
24+
* node ./scripts/run-lex-codegen.js gen-api --yes ./generated
25+
* node ./scripts/run-lex-codegen.js gen-ts-obj > generated/DO-NOT-USE-lexicons.ts
26+
*/
27+
28+
import { readdirSync, readFileSync } from "node:fs";
29+
import { join } from "node:path";
30+
import { fileURLToPath } from "node:url";
31+
import { spawnSync } from "node:child_process";
32+
33+
const lexiconsDir = fileURLToPath(new URL("../lexicons", import.meta.url));
34+
35+
function findJsonFiles(dir) {
36+
const out = [];
37+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
38+
const full = join(dir, entry.name);
39+
if (entry.isDirectory()) out.push(...findJsonFiles(full));
40+
else if (entry.isFile() && entry.name.endsWith(".json")) out.push(full);
41+
}
42+
return out;
43+
}
44+
45+
function isPermissionSet(file) {
46+
try {
47+
return (
48+
JSON.parse(readFileSync(file, "utf-8"))?.defs?.main?.type ===
49+
"permission-set"
50+
);
51+
} catch {
52+
return false; // let lex surface a parse error rather than silently skip
53+
}
54+
}
55+
56+
const lexArgs = process.argv.slice(2);
57+
if (lexArgs.length === 0) {
58+
process.stderr.write(
59+
"run-lex-codegen: expected a lex subcommand, e.g. `gen-api --yes ./generated`\n",
60+
);
61+
process.exit(2);
62+
}
63+
64+
const files = findJsonFiles(lexiconsDir)
65+
.filter((f) => !isPermissionSet(f))
66+
.sort();
67+
68+
// Fail loudly rather than run lex on an empty list (which could "succeed"
69+
// producing nothing).
70+
if (files.length === 0) {
71+
process.stderr.write("run-lex-codegen: no lexicons found\n");
72+
process.exit(1);
73+
}
74+
75+
// `shell: false` (the default) — args are passed as argv, so there is no shell
76+
// word-splitting, no ARG_MAX limit from a command line, and lex's exit code is
77+
// ours. stdio inherited so `> file` redirection in the npm script still works.
78+
const result = spawnSync("lex", [...lexArgs, ...files], { stdio: "inherit" });
79+
if (result.error) {
80+
process.stderr.write(
81+
`run-lex-codegen: failed to spawn lex: ${result.error.message}\n`,
82+
);
83+
process.exit(1);
84+
}
85+
process.exit(result.status ?? 1);

0 commit comments

Comments
 (0)