build: the gate is scripts/check.sh, and CI goes
The Gitea Actions gate repeated, after the merge, what a release already runs before it, and its runner shared the git host's container: one run there starved Gitea of memory until it stopped answering. The lanes now live in scripts/check.sh, run the same by the release dispatcher and by hand, and the gate-contract test holds its lines (the ruff and pip-audit checks read it instead of the workflow). The two audits only CI ran join the gate: pip-audit over the lock (pinned in the test image, blocking at any severity) and npm audit of the production dependencies. The engine contracts drop the CI containers; the register records that pushes have no server-side gate.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env bash
|
||||
# The gate — every lane, in one place, run from the repo root (E32, after 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
|
||||
#
|
||||
# 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="${1:-all}"
|
||||
case "$mode" in all | lint) ;; *) echo "usage: scripts/check.sh [lint]" >&2; exit 2 ;; esac
|
||||
|
||||
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
|
||||
echo ">> $name…"
|
||||
"$@" || failed+=("$name")
|
||||
}
|
||||
|
||||
# --- 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, and unused by what ships)" 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
|
||||
# --coverage prints the summary: read, never a threshold (E32 D4)
|
||||
lane "vitest (+ coverage report)" frontend npm run --silent test -- --coverage
|
||||
# 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
|
||||
Reference in New Issue
Block a user