mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-23 11:38:09 +09:00
A name used before it exists only fails when that line runs, so a rarely-taken branch ships broken. Two routes 500'd on every call and Stop crashed on compose-slot apps, both found by pyflakes in seconds. The check now runs in the ratchet job and in preflight.
166 lines
6.8 KiB
Bash
166 lines
6.8 KiB
Bash
#!/usr/bin/env bash
|
|
# Run the checks that fail CI without running the whole suite.
|
|
#
|
|
# Every red backend-CI round on 2026-08-21 was a ratchet, not a broken feature:
|
|
# a floor measured on a dev box, a fingerprint baseline left unregenerated after
|
|
# a refactor, a ceiling that moved with the code, a generated doc gone stale,
|
|
# and a text-grep guard that matched a docstring. None of them need the 4-shard
|
|
# suite to catch -- they are AST and text scans over the tree, and they all run
|
|
# here in about two minutes.
|
|
#
|
|
# Usage (from anywhere):
|
|
#
|
|
# bash scripts/preflight.sh
|
|
# PYTHON=~/sk-venv/bin/python bash scripts/preflight.sh
|
|
#
|
|
# The interpreter needs the backend's dependencies (flask, sqlalchemy, pytest).
|
|
# On the Windows dev box that means the WSL venv, which also reaches Windows
|
|
# node through interop, so one invocation covers both halves:
|
|
#
|
|
# wsl -e bash -lc "cd /mnt/c/Users/<you>/Documents/GitHub/ServerKit && \
|
|
# PYTHON=~/sk-venv/bin/python bash scripts/preflight.sh"
|
|
#
|
|
# Exit code is 0 only when every check that ran passed.
|
|
|
|
set -uo pipefail
|
|
|
|
cd "$(dirname "$0")/.." || exit 1
|
|
REPO=$(pwd)
|
|
|
|
PYTHON=${PYTHON:-python3}
|
|
command -v "$PYTHON" >/dev/null 2>&1 || PYTHON=python
|
|
if ! command -v "$PYTHON" >/dev/null 2>&1; then
|
|
echo "preflight: no python interpreter found (set PYTHON=/path/to/python)" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# node, or Windows node.exe reached from WSL. In the latter case every path
|
|
# handed to it has to be a Windows path or it resolves against C:\.
|
|
NODE=''
|
|
NODE_NEEDS_WINPATH=0
|
|
if command -v node >/dev/null 2>&1; then
|
|
NODE=node
|
|
elif command -v node.exe >/dev/null 2>&1 && command -v wslpath >/dev/null 2>&1; then
|
|
NODE=node.exe
|
|
NODE_NEEDS_WINPATH=1
|
|
elif [ -x /mnt/c/nvm4w/nodejs/node.exe ] && command -v wslpath >/dev/null 2>&1; then
|
|
NODE=/mnt/c/nvm4w/nodejs/node.exe
|
|
NODE_NEEDS_WINPATH=1
|
|
fi
|
|
|
|
node_path() {
|
|
if [ "$NODE_NEEDS_WINPATH" -eq 1 ]; then
|
|
wslpath -w "$1"
|
|
else
|
|
printf '%s' "$1"
|
|
fi
|
|
}
|
|
|
|
FAILED=''
|
|
SKIPPED=''
|
|
|
|
note_fail() { FAILED="${FAILED}${1}"$'\n'; }
|
|
note_skip() { SKIPPED="${SKIPPED}${1}"$'\n'; }
|
|
banner() { printf '\n\033[1m== %s\033[0m\n' "$1"; }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. Test-count ratchet -- the floor CI compares against.
|
|
# ---------------------------------------------------------------------------
|
|
banner 'test-count ratchet'
|
|
if ! (cd "$REPO/backend" && "$PYTHON" tests/check_test_count.py); then
|
|
note_fail 'test-count ratchet'
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1b. Undefined names (same check as the CI ratchet job).
|
|
# ---------------------------------------------------------------------------
|
|
banner 'undefined names'
|
|
if command -v ruff >/dev/null 2>&1 || "$PYTHON" -m ruff --version >/dev/null 2>&1; then
|
|
ruff_cmd='ruff'
|
|
command -v ruff >/dev/null 2>&1 || ruff_cmd="$PYTHON -m ruff"
|
|
# shellcheck disable=SC2086
|
|
if ! (cd "$REPO/backend" && $ruff_cmd check --select F821,F823 app tests); then
|
|
note_fail 'undefined names'
|
|
fi
|
|
else
|
|
echo 'ruff not installed (pip install ruff==0.14.14) -- CI runs this check' >&2
|
|
note_skip 'undefined names (ruff not installed)'
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. Backend ratchets, censuses, and guards.
|
|
#
|
|
# Matched by filename rather than listed, so a new ratchet is covered the day it
|
|
# lands instead of the day someone remembers to add it here.
|
|
# ---------------------------------------------------------------------------
|
|
banner 'backend ratchets and guards'
|
|
guard_files=$(cd "$REPO/backend" && ls tests/test_*ratchet*.py tests/test_*census*.py \
|
|
tests/test_*guard*.py tests/test_*boundar*.py tests/test_*inventory*.py \
|
|
tests/test_*drift*.py tests/test_*spec*.py 2>/dev/null | tr '\n' ' ')
|
|
if [ -n "$guard_files" ]; then
|
|
# shellcheck disable=SC2086
|
|
if ! (cd "$REPO/backend" && PYTHONPATH=. FLASK_ENV=testing "$PYTHON" -m pytest \
|
|
$guard_files -q -p no:cacheprovider -p no:warnings); then
|
|
note_fail 'backend ratchets and guards'
|
|
fi
|
|
else
|
|
echo 'no matching test files'
|
|
note_skip 'backend ratchets and guards (no matching test files)'
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. Frontend boundary check + the generated inventory that reads it.
|
|
#
|
|
# test_migration_inventory.py SKIPS when node is missing, which is how the
|
|
# inventory reached CI stale: the check that would have caught it never ran.
|
|
# ---------------------------------------------------------------------------
|
|
if [ -n "$NODE" ]; then
|
|
banner 'frontend boundaries'
|
|
if ! (cd "$REPO/frontend" && "$NODE" "$(node_path "$REPO/frontend/scripts/check-frontend-boundaries.mjs")"); then
|
|
note_fail 'frontend boundaries'
|
|
fi
|
|
|
|
banner 'builtin frontend drift'
|
|
# Extensions CI red on 2026-08-30: a builtin-extensions/ manifest changed
|
|
# without re-running the sync, and nothing local caught it. Same contract
|
|
# as the inventory below: a fixable failure is fixed, then flagged so the
|
|
# diff gets reviewed and committed.
|
|
if (cd "$REPO" && "$NODE" "$(node_path "$REPO/scripts/sync-builtin-frontends.mjs")" --check >/dev/null 2>&1); then
|
|
echo 'pre-bundled copies match builtin-extensions/ sources'
|
|
elif (cd "$REPO" && "$NODE" "$(node_path "$REPO/scripts/sync-builtin-frontends.mjs")"); then
|
|
echo 'pre-bundled copies were stale and have been re-synced -- review and commit the diff' >&2
|
|
note_fail 'builtin frontend drift (re-synced; commit the diff)'
|
|
else
|
|
note_fail 'builtin frontend drift (sync script failed)'
|
|
fi
|
|
|
|
banner 'migration inventory is regenerated'
|
|
# Compared against a snapshot of the file, NOT against HEAD: an uncommitted
|
|
# but correct regeneration is a pass, and only a number that actually moved
|
|
# is a failure.
|
|
doc="$REPO/docs/MIGRATION_INVENTORY.md"
|
|
before=$(mktemp)
|
|
cp "$doc" "$before" 2>/dev/null
|
|
if ! "$PYTHON" "$REPO/scripts/generate-migration-inventory.py" >/dev/null; then
|
|
note_fail 'migration inventory (generator failed)'
|
|
elif cmp -s "$doc" "$before"; then
|
|
echo 'docs/MIGRATION_INVENTORY.md matches the measured state'
|
|
else
|
|
echo 'docs/MIGRATION_INVENTORY.md was stale and has been regenerated -- review and commit the diff' >&2
|
|
note_fail 'migration inventory (regenerated; commit the diff)'
|
|
fi
|
|
rm -f "$before"
|
|
else
|
|
note_skip 'frontend boundaries + builtin frontend drift + migration inventory (no node on PATH)'
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
banner 'preflight summary'
|
|
[ -n "$SKIPPED" ] && printf '%s' "$SKIPPED" | sed 's/^/ SKIPPED /'
|
|
if [ -z "$FAILED" ]; then
|
|
echo ' PASS everything CI checks outside the sharded suite'
|
|
exit 0
|
|
fi
|
|
printf '%s' "$FAILED" | sed 's/^/ FAILED /'
|
|
exit 1
|