143 lines
5.8 KiB
Bash
Executable File
143 lines
5.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# The repo's own guards, before a commit rather than after it.
|
|
#
|
|
# ## Why this exists in the repository and not in .git/hooks
|
|
#
|
|
# `.git/hooks` is not versioned, so a hook living there protects exactly one
|
|
# checkout and silently protects nothing anywhere else. This directory is
|
|
# committed, and `core.hooksPath` points at it:
|
|
#
|
|
# git config core.hooksPath .githooks
|
|
#
|
|
# That one line is the only setup, and it is in the README beside the test
|
|
# command.
|
|
#
|
|
# ## What it checks, and why in this order
|
|
#
|
|
# Typecheck first: it takes about two seconds and catches the class of mistake
|
|
# that is most annoying to discover later — a type change whose consumers were
|
|
# never visited. Then the suite, which is where the real guarantees live: the
|
|
# conformance tests that keep registries total, the sentinel tests that keep
|
|
# secrets out of logs, and the mutation-tested guards.
|
|
#
|
|
# Both are already the rule for finishing work here. A hook is just the version
|
|
# that does not depend on somebody remembering.
|
|
#
|
|
# ## It warns about unstaged changes rather than failing on them
|
|
#
|
|
# Both commands run against the **working tree**, not against the index. So a
|
|
# clean run proves the working tree is good, which is only the same thing as the
|
|
# commit being good when nothing is left unstaged.
|
|
#
|
|
# That distinction matters in this checkout specifically: it is edited by more
|
|
# than one person at a time, and commits are staged by explicit path. A green
|
|
# hook beside three unstaged files has verified something other than what is
|
|
# about to be committed, and the honest thing is to say so rather than imply a
|
|
# guarantee that was not made.
|
|
#
|
|
# ## Escape hatch
|
|
#
|
|
# SKIP_GUARDS=1 git commit ... skips both, loudly
|
|
# git commit --no-verify ... skips the hook entirely, silently
|
|
#
|
|
# The first is preferred: it leaves a line in the terminal saying the guards did
|
|
# not run, which is the difference between a deliberate exception and a habit.
|
|
|
|
set -uo pipefail
|
|
|
|
cd "$(git rev-parse --show-toplevel)" || exit 1
|
|
|
|
say() { printf '\033[1mpre-commit:\033[0m %s\n' "$*" >&2; }
|
|
|
|
if [ -n "${SKIP_GUARDS:-}" ]; then
|
|
say "SKIP_GUARDS set — typecheck and tests did NOT run for this commit."
|
|
exit 0
|
|
fi
|
|
|
|
# Nothing staged is not this hook's problem; git will refuse on its own.
|
|
if git diff --cached --quiet; then
|
|
exit 0
|
|
fi
|
|
|
|
# Only worth running when source or tests changed. A commit that touches docs or
|
|
# migrations alone still gets the typecheck, because a migration can be
|
|
# referenced from a test, but it should not wait on the whole suite.
|
|
#
|
|
# The four files after the `|` are not source, and they are here because of what
|
|
# `tests/version.test.ts` guards: package.json, the Dockerfile, and the two
|
|
# image pins in README.md and docker-compose.example.yml must all name the same
|
|
# version. With `src|tests` alone, a commit that hand-edits only the README pin
|
|
# — precisely the drift that left those pins six versions stale — would get the
|
|
# typecheck and skip the one test that would have caught it. The guard has to
|
|
# run on the commits it exists to police.
|
|
staged=$(git diff --cached --name-only)
|
|
touches_code=$(printf '%s\n' "$staged" \
|
|
| grep -cE '^(src|tests)/.*\.(ts|tsx)$|^(package\.json|Dockerfile|README\.md|docker-compose\.example\.yml)$' || true)
|
|
|
|
say "typecheck…"
|
|
|
|
if ! npx tsc --noEmit; then
|
|
say "typecheck failed — commit refused."
|
|
exit 1
|
|
fi
|
|
|
|
# Credentials, before the commit exists.
|
|
#
|
|
# First, and cheap: it reads the staged diff only. A secret caught here costs a
|
|
# `git reset`; the same secret caught after a push costs a rotation, because
|
|
# deleting the line does not remove it from a commit that already exists.
|
|
if [ -x scripts/secrets.sh ] || [ -f scripts/secrets.sh ]; then
|
|
if ! bash scripts/secrets.sh; then
|
|
say "possible credential in the staged changes — commit refused."
|
|
say "If it is real, rotate it. If it is not, --allow the path or adjust"
|
|
say "the patterns; do not silence the check."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
if [ "$touches_code" -gt 0 ]; then
|
|
say "tests…"
|
|
|
|
# TEST_DATABASE_URL is picked up from the environment when it is set. Without
|
|
# one the database suites skip themselves rather than fail, which is the
|
|
# existing behaviour in tests/setup.ts — so this hook is useful on a machine
|
|
# with no Postgres and stricter on one with it.
|
|
#
|
|
# But "not set" and "not available" are different, and this used to treat them
|
|
# the same: every commit on this machine ran ~900 of 1,600 tests while a test
|
|
# database sat listening on 55432, because nothing looked. A commit hook
|
|
# should not *refuse* over a missing database — that belongs to the release
|
|
# gate, which now does — but it should use one that is there.
|
|
# Set these two together in the repository that adopts this hook — the port
|
|
# it listens on, and the URL that reaches it. Leaving them unset is fine and
|
|
# simply means the database suites skip, which is the right default on a
|
|
# machine with no Postgres.
|
|
TEST_DB_PORT="${TEST_DB_PORT:-}"
|
|
TEST_DB_URL="${TEST_DB_URL:-}"
|
|
|
|
if [ -z "${TEST_DATABASE_URL:-}" ] && [ -n "$TEST_DB_PORT" ] && [ -n "$TEST_DB_URL" ] \
|
|
&& (exec 3<>/dev/tcp/127.0.0.1/"$TEST_DB_PORT") 2>/dev/null; then
|
|
exec 3<&- 2>/dev/null || true
|
|
export TEST_DATABASE_URL="$TEST_DB_URL"
|
|
say "using the local test database on ${TEST_DB_PORT} — the full suite will run."
|
|
elif [ -z "${TEST_DATABASE_URL:-}" ]; then
|
|
say "no test database; the database suites will skip. The release gate is stricter."
|
|
fi
|
|
|
|
if ! npx vitest run --reporter=dot; then
|
|
say "tests failed — commit refused."
|
|
exit 1
|
|
fi
|
|
else
|
|
say "no .ts/.tsx staged — skipping the suite."
|
|
fi
|
|
|
|
# Said last so it is the thing still on screen when the editor opens.
|
|
if ! git diff --quiet; then
|
|
say "NOTE: unstaged changes are present. The guards ran against the working"
|
|
say " tree, so they did not verify this commit in isolation."
|
|
fi
|
|
|
|
exit 0
|