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:
@@ -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([]);
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user