Files
siftlode/scripts/check.sh
T
peter d1bbe4524c fix(gate): close the gaps in the plan-code guard, the button lists and the tooling
- planCodes: the quote and slash lookarounds let the commonest form through (a docstring or test
  title opening with a code, a slash pair); the two real exceptions (an episode titled "E1", ruff's
  E4/E7/E9) are now named and cut first, with cases that fail on the old pattern. audit_metrics
  counts by the same definition.
- button-primitive: both file lists live in the rule body, out of sight of the snapshot and the
  register; each is now pinned to its exact set of ui/ files (red when either is widened).
- check.sh --brief shows a short log whole and keeps the line before each hit, so ruff's rule and
  eslint's file path survive the excerpt.
- lint-changed runs prettier over every changed frontend file, as the gate does, not only code.
- CLAUDE.md and gates.md no longer claim the basedpyright baseline was found harmless: it was never
  triaged, and the reportOptional* entries are the likeliest real bugs.
2026-10-05 00:19:50 +02:00

148 lines
6.9 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# The gate — every lane, in one place, run from the repo root (since CI went: the lab runner
# duplicated what this already ran before every release, and one CI run starved the git host of
# memory until it stopped answering). The release dispatcher calls this; so can anyone with Docker
# and the pinned toolchain. `frontend/src/lib/gates.contract.test.ts` holds its lines, so a lane
# dropped or narrowed here fails a test like any other gate change.
#
# bash scripts/check.sh every lane (what a release requires, before e2e and the CVE scan)
# 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
#
# Each lane runs even when an earlier one failed, and the summary names every red one: a gate that
# stops at the first failure hides the second until the first is fixed. The backend lanes run in the
# test image (backend/Dockerfile.test): requirements.lock will not install on Windows (uvloop), and a
# type checker or auditor without the real packages reports nonsense.
set -u
cd "$(dirname "$0")/.." || exit 2
mode=all
brief=0
for arg in "$@"; do
case "$arg" in
all | lint) mode="$arg" ;;
--brief) brief=1 ;;
*) echo "usage: scripts/check.sh [lint] [--brief]" >&2; exit 2 ;;
esac
done
export MSYS_NO_PATHCONV=1 # Git Bash: do not rewrite the in-container paths below
TESTCOMPOSE=backend/docker-compose.test.yml
failed=()
lane() { # lane <name> <command…> — run it, remember a failure, never stop
local name="$1"
shift
if [ "$brief" = 0 ]; then
echo ">> $name…"
"$@" || failed+=("$name")
return 0
fi
local out
out="$(mktemp)"
if "$@" >"$out" 2>&1; then
echo ">> $name: ok"
else
failed+=("$name")
echo ">> $name: FAILED"
brief_excerpt "$out"
fi
rm -f "$out"
}
# What broke, in a log's own words. A short log is shown whole: a keyword filter keeps ruff's
# summary line and drops the rule and file above it, and eslint's file path sits on a line of its
# own above its errors. A long one shows its first 20 error-looking lines, each with the line before
# it and the two after (knip names the file on the line after "Unused files"); a long log with none
# of them shows its tail, which is where a summary sits. The test database's own log is left out:
# the DB lane provokes constraint errors on purpose, and its `ERROR:` lines filled the excerpt ahead
# of the failing test.
brief_excerpt() {
local log hits
log="$(grep -v -E '^testdb-[0-9]+ ' "$1")"
if [ "$(printf '%s\n' "$log" | wc -l)" -le 60 ]; then
printf '%s\n' "$log"
return
fi
hits="$(printf '%s\n' "$log" |
grep -E -i -B 1 -A 2 -m 20 'error|fail|✗|×|warn|would reformat|unused|vulnerab|found [0-9]')"
if [ -n "$hits" ]; then printf '%s\n' "$hits"; else printf '%s\n' "$log" | tail -n 20; fi
}
# --- static lanes ------------------------------------------------------------------------------------
# Host ruff, held to the pin in requirements-dev.txt: a newer ruff silently changes what "formatted"
# means. `--config backend/ruff.toml` on both: it makes that file the WHOLE configuration (ruff then
# ignores every other config it would discover) and gives scripts/ the backend's rule set.
ruff_pinned() {
local pin host
pin="$(sed -n 's/^ruff==\([0-9.]*\).*/\1/p' backend/requirements-dev.txt)"
host="$(python -m ruff --version 2>/dev/null | awk '{print $2}')"
[ -n "$pin" ] && [ "$host" = "$pin" ] && return 0
echo "ruff ${host:-missing} on the host, ${pin:-nothing} pinned — python -m pip install ruff==$pin" >&2
return 1
}
ruff_check() { ruff_pinned && python -m ruff check --config backend/ruff.toml backend scripts; }
ruff_format() { ruff_pinned && python -m ruff format --check --config backend/ruff.toml backend scripts; }
# New errors only: the pre-existing ones are in backend/.basedpyright/baseline.json. basedpyright
# TIGHTENS that file by itself when errors go, and the tree is bind-mounted read-write, so a green run
# can leave it rewritten: that fails here until it is committed, or the next run's stack reads stale.
basedpyright() {
local before
before="$(git hash-object backend/.basedpyright/baseline.json)"
docker compose -f "$TESTCOMPOSE" run --rm --no-deps --build test \
python -m basedpyright --project pyrightconfig.json || return 1
if [ "$(git hash-object backend/.basedpyright/baseline.json)" != "$before" ]; then
echo "this run rewrote backend/.basedpyright/baseline.json (errors were fixed) — commit it" >&2
return 1
fi
git diff HEAD --quiet -- backend/.basedpyright/baseline.json && return 0
echo "backend/.basedpyright/baseline.json is uncommitted — commit it" >&2
return 1
}
frontend() { (cd frontend && "$@"); }
lane "ruff" ruff_check
lane "ruff format --check" ruff_format
lane "basedpyright (new errors only)" basedpyright
# every frontend lane judges the HOST node_modules: this proves it is the tree the lock pins
lane "lock check (frontend/node_modules == package-lock.json)" node scripts/check-node-modules.mjs
lane "tsc (src + configs + e2e)" frontend npm run --silent typecheck
lane "eslint (warnings fail too)" frontend npm run --silent lint
lane "prettier --check" frontend npm run --silent format:check
lane "knip (unused files, exports, dependencies)" frontend npm run --silent knip
# --- tests, build, audits --------------------------------------------------------------------------
pytest_lane() { # the full suite against a throwaway Postgres; --pull keeps the base image current
docker compose -f "$TESTCOMPOSE" build --pull -q test || return 1
local rc=0
docker compose -f "$TESTCOMPOSE" up --build --abort-on-container-exit --exit-code-from test || rc=1
docker compose -f "$TESTCOMPOSE" down -v >/dev/null 2>&1
return $rc
}
# Every pinned backend package against the advisory databases, at any severity — the lock is a full
# `pip freeze`, so it is audited as written, without a resolver. An advisory with no fix is answered
# with `--ignore-vuln <id>` here and a row in docs/gates.md (the register test reads this line).
pip_audit() {
docker compose -f "$TESTCOMPOSE" run --rm --no-deps test \
pip-audit -r requirements.lock --no-deps --disable-pip
}
if [ "$mode" = all ]; then
lane "vitest" frontend npm run --silent test
# the production bundle, and devOnlyGuard, which fails it when dev-only code reaches it
lane "vite build" frontend npx vite build --logLevel warn
lane "pytest (pure-logic + DB lane)" pytest_lane
lane "pip-audit (backend lock)" pip_audit
# what ships to the browser, at high or above; the dev toolchain is burned down by hand
lane "npm audit (production dependencies)" frontend npm audit --omit=dev --audit-level=high
fi
if [ ${#failed[@]} -eq 0 ]; then
echo ">> gate ($mode): green ✓"
exit 0
fi
echo ">> gate ($mode): RED — ${failed[*]}" >&2
exit 1