Files
jpmschweitzerandClaude Opus 4.8 9a5f5b2ab5 build(hooks): widen pre-push fast path to test/ci/.githooks + guard test (T-393)
The fast-path regex matched only `^(lib/|pubspec\.)`, so a push that ONLY
changed a test, a ci/ gate script, or the pre-push hook itself skipped the
entire ~2min suite — exactly the paths most able to break the suite or the
gate went unchecked. Widen the trigger to include test/, ci/, and
.githooks/, and extract it to a single `trigger_re` variable. Also drop the
header comment's false claim that "release CI runs it forced on a tagged
version" — there is no release CI.

Add test/tooling/pre_push_hook_test.dart: it reads the live trigger_re from
the hook and asserts the load-bearing dirs force the full gate while
docs/assets ride along — so the regex can't be silently narrowed again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:26:52 +02:00

66 lines
2.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# Pre-push gate. Blocks the push if any quality check fails.
#
# Install: `make hooks` (points git core.hooksPath at .githooks/).
# Bypass: never. If this runs slowly, fix the slow test; don't reach
# for --no-verify (git-commit skill forbids it).
#
# Fast path (T-348, widened T-393): run the full ~2min test suite when the push
# touches lib/ (app + runtime Dart source), pubspec.* (deps / version), or the
# things that can themselves break the suite or this gate — test/, ci/, and
# .githooks/. (The old regex matched only lib/ and pubspec.*, so a push that
# ONLY changed a test, a ci/ gate script, or this hook skipped the whole suite.)
# Pure assets/docs changes still ride along with the next lib-touching push.
# There is no release CI — the full suite is only ever run here or via
# `make push-check`. So a push touching none of the above runs just the instant
# decisions + changelog gates. A state we can't classify (unfetched remote, new
# branch) runs the full gate.
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
z40=0000000000000000000000000000000000000000
# Collect every file changed across the commits being pushed. git feeds the
# hook one line per ref on stdin: <local-ref> <local-sha> <remote-ref> <remote-sha>.
changed=""
force_full=0
while read -r _local_ref local_sha _remote_ref remote_sha; do
[[ "$local_sha" == "$z40" ]] && continue # branch deletion — nothing to test
if [[ "$remote_sha" == "$z40" ]]; then
# New remote branch: diff from its merge-base with main, else play it safe.
base="$(git merge-base "$local_sha" origin/main 2>/dev/null || true)"
else
base="$remote_sha"
fi
# If we can't resolve a base locally (e.g. the remote advanced and we haven't
# fetched its objects), we can't classify the diff — run the full gate.
if [[ -z "$base" ]] || ! git cat-file -e "$base^{commit}" 2>/dev/null; then
force_full=1
break
fi
changed+=$'\n'"$(git diff --name-only "$base" "$local_sha")"
done
# Paths that force the full gate: source (lib/), deps/version (pubspec.*), and
# the dirs that can themselves break the suite or this gate (test/, ci/,
# .githooks/). Single source of truth — test/tooling/pre_push_hook_test.dart
# reads this exact pattern, so narrowing it fails that test (the T-393 guard).
trigger_re='^(lib/|test/|ci/|\.githooks/|pubspec\.)'
# Run the full gate when a trigger path is touched, or when we couldn't classify
# above.
needs_gate=1
if [[ "$force_full" -eq 0 ]]; then
trigger_files="$(printf '%s\n' "$changed" | grep -E "$trigger_re" || true)"
[[ -z "$trigger_files" ]] && needs_gate=0
fi
if [[ "$needs_gate" -eq 0 ]]; then
echo "==> pre-push: no source/test/ci/hook/pubspec change — decisions + changelog gates, skipping tests"
make decisions-validate changelog-gate
else
echo "==> pre-push: make push-check"
make push-check
fi