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:
@@ -44,6 +44,8 @@ A change is done when this is green, from the repo root:
|
||||
bash scripts/check.sh # every lane
|
||||
bash scripts/check.sh lint # the static lanes only (no tests, build or audits) — the quick loop
|
||||
bash scripts/check.sh --brief # either mode: one line per lane, output only for a red one
|
||||
bash scripts/check.sh --list # the lane names, one per line (runs nothing)
|
||||
bash scripts/check.sh --only "vitest" # one lane, by its exact name — never "the gate"
|
||||
bash scripts/lint-changed.sh # ruff + eslint + prettier on the uncommitted files only
|
||||
```
|
||||
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
+29
-7
@@ -9,6 +9,10 @@
|
||||
# bash scripts/check.sh lint the static lanes only: no tests, no build, no audits
|
||||
# … --brief (either mode) a lane's output only when it fails, and only its first
|
||||
# error lines — for an agent, whose context the full log would flood
|
||||
# … --list the lane names of that mode, one per line — runs nothing
|
||||
# … --only "<lane>" that one lane (an exact name from --list): a re-run of a red lane, or a
|
||||
# runner that schedules lanes itself. Its green is that lane's, never "the
|
||||
# gate"; an unknown name exits 2 rather than passing with nothing run
|
||||
# GATE_TIMES_FILE=<file> … also append each lane's time as a TSV row (UTC, mode, lane, seconds,
|
||||
# ok|red, HEAD's tree) — to compare runs; every run prints the times anyway
|
||||
#
|
||||
@@ -20,18 +24,26 @@ set -u
|
||||
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 ;;
|
||||
*) echo "usage: scripts/check.sh [lint] [--brief]" >&2; exit 2 ;;
|
||||
--list) list=1 ;;
|
||||
--only) [ $# -ge 2 ] || usage; only="$2"; shift ;;
|
||||
*) usage ;;
|
||||
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 }'; }
|
||||
# How long each lane took: a line after the lane, a table at the end, and — when GATE_TIMES_FILE names a
|
||||
@@ -42,7 +54,7 @@ timed() { # timed <name> <ok|red> <start-ms> — sets $took (no subshell: it mus
|
||||
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
|
||||
@@ -50,6 +62,9 @@ timed() { # timed <name> <ok|red> <start-ms> — sets $took (no subshell: it mus
|
||||
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…"
|
||||
@@ -163,12 +178,19 @@ if [ "$mode" = all ]; then
|
||||
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