chore(gate): check.sh can list its lanes and run one of them

--list prints the lane names, --only "<lane>" runs that one lane, so a runner can schedule the lanes itself and a red lane can be re-run on its own. An --only run reports that lane, never the gate, and an unknown name exits 2 instead of passing with nothing run; the contract test holds both.
This commit is contained in:
2026-10-16 02:53:30 +02:00
parent 9251918e08
commit 926e5e9899
4 changed files with 88 additions and 14 deletions
+33 -1
View File
@@ -1,6 +1,7 @@
import { execFileSync, spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
import { relative } from "node:path";
import { relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { ESLint, type Linter } from "eslint";
import prettierConfig from "eslint-config-prettier";
@@ -375,6 +376,37 @@ describe("the gate snapshot (gates.snapshot.json)", () => {
}, 120_000);
});
// Git for Windows' own bash: on a Windows PATH `bash` is often WSL's, which cannot run the script.
function bash(): string {
if (process.platform !== "win32") return "bash";
const exec = execFileSync("git", ["--exec-path"], { encoding: "utf8" }).trim();
return resolve(exec, "../../../bin/bash.exe");
}
const checkSh = (...args: string[]) =>
spawnSync(bash(), ["scripts/check.sh", ...args], { cwd: REPO, encoding: "utf8" });
// A runner that schedules lanes one by one learns them from `--list` and runs each with `--only`; a
// lane missing from the list would be a lane it never runs, and an `--only` that passed with nothing
// run would be a green about nothing.
describe("the gate script's lane list (check.sh --list / --only)", () => {
it("lists every lane the script runs, in order", () => {
const declared = [...read("scripts/check.sh").matchAll(/^\s*lane "([^"]+)"/gm)].map(
(m) => m[1],
);
const listed = checkSh("--list");
expect(listed.status).toBe(0);
expect(listed.stdout.trim().split("\n")).toEqual(declared);
const lint = checkSh("lint", "--list").stdout.trim().split("\n");
expect(lint.length).toBeGreaterThan(0);
expect(declared.slice(0, lint.length)).toEqual(lint);
});
it("refuses an unknown lane instead of passing with nothing run", () => {
const r = checkSh("--only", "no such lane");
expect(r.status).toBe(2);
expect(r.stdout).not.toContain("green");
});
});
describe("the gate register (docs/gates.md)", () => {
it("lists every ESLint rule narrowed, by scope", async () => {
expect(missingFrom(eslintNarrowings(await linted()))).toEqual([]);
+24 -6
View File
@@ -1299,16 +1299,24 @@
"cd \"$(dirname \"$0\")/..\" || exit 2",
"mode=all",
"brief=0",
"for arg in \"$@\"; do",
" case \"$arg\" in",
" all | lint) mode=\"$arg\" ;;",
"list=0",
"only=\"\"",
"usage() { echo 'usage: scripts/check.sh [lint] [--brief] [--list | --only \"<lane>\"]' >&2; exit 2; }",
"while [ $# -gt 0 ]; do",
" case \"$1\" in",
" all | lint) mode=\"$1\" ;;",
" --brief) brief=1 ;;",
" --list) list=1 ;;",
" --only) [ $# -ge 2 ] || usage; only=\"$2\"; shift ;;",
" esac",
" shift",
"done",
"[ \"$list\" = 1 ] && [ -n \"$only\" ] && usage",
"export MSYS_NO_PATHCONV=1 # Git Bash: do not rewrite the in-container paths below",
"TESTCOMPOSE=backend/docker-compose.test.yml",
"failed=()",
"timings=()",
"ran=0",
"ms() { date +%s%3N; }",
"secs() { awk -v t=\"$1\" 'BEGIN { printf \"%.1f\", t / 1000 }'; }",
"took=\"\"",
@@ -1316,7 +1324,7 @@
" took=\"$(secs $(($(ms) - $3)))\"",
" timings+=(\"$(printf '%8s s %s' \"$took\" \"$1\")$([ \"$2\" = red ] && echo ' (red)')\")",
" if [ -n \"${GATE_TIMES_FILE:-}\" ]; then",
" printf '%s\\t%s\\t%s\\t%s\\t%s\\t%s\\n' \"$(date -u +%FT%TZ)\" \"$mode\" \"$1\" \"$took\" \"$2\" \\",
" printf '%s\\t%s\\t%s\\t%s\\t%s\\t%s\\n' \"$(date -u +%FT%TZ)\" \"${only:+only}${only:-$mode}\" \"$1\" \"$took\" \"$2\" \\",
" \"$(git rev-parse --short 'HEAD^{tree}' 2>/dev/null)\" >>\"$GATE_TIMES_FILE\"",
" fi",
" return 0",
@@ -1324,6 +1332,9 @@
"lane() { # lane <name> <command…> — run it, remember a failure, never stop",
" local name=\"$1\" t0 result=ok",
" shift",
" if [ \"$list\" = 1 ]; then echo \"$name\"; return 0; fi",
" [ -n \"$only\" ] && [ \"$name\" != \"$only\" ] && return 0",
" ran=$((ran + 1))",
" t0=\"$(ms)\"",
" if [ \"$brief\" = 0 ]; then",
" echo \">> $name…\"",
@@ -1407,14 +1418,21 @@
" lane \"pip-audit (backend lock)\" pip_audit",
" lane \"npm audit (production dependencies)\" frontend npm audit --omit=dev --audit-level=high",
"fi",
"[ \"$list\" = 1 ] && exit 0",
"if [ -n \"$only\" ] && [ \"$ran\" = 0 ]; then",
" echo \"no lane named '$only' in mode $mode — see: scripts/check.sh $mode --list\" >&2",
" exit 2",
"fi",
"scope=\"gate ($mode)\"",
"[ -n \"$only\" ] && scope=\"lane '$only' only (not the gate)\"",
"echo \">> lane times:\"",
"printf '%s\\n' \"${timings[@]}\"",
"echo \"$(printf '%8s' \"$(secs $(($(ms) - gate_t0)))\") s total\"",
"if [ ${#failed[@]} -eq 0 ]; then",
" echo \">> gate ($mode): green ✓\"",
" echo \">> $scope: green ✓\"",
" exit 0",
"fi",
"echo \">> gate ($mode): RED — ${failed[*]}\" >&2",
"echo \">> $scope: RED — ${failed[*]}\" >&2",
"exit 1"
]
}