From b7e0835b746fbf2df1f5403bac8c72660601207b Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 10 Jun 2026 19:21:37 +0200 Subject: [PATCH] pre-push: skip the test suite unless lib/ or pubspec changed (T-348) The hook ran the full ~2min push-check on every push, even one touching only docs, changelog, pql data, tests, or assets. Diff the pushed range (from the hook's stdin) and run the full gate only when lib/ (app/runtime source) or pubspec.* (deps/version) changed; otherwise run just the instant decisions + changelog gates. Other changes ride along with a lib change in practice, the full suite stays available via `make push-check`, and the release CI runs it forced on a tagged version. A state we can't classify (unfetched remote sha, new branch) falls back to the full gate. Co-Authored-By: Claude Opus 4.8 (1M context) --- .githooks/pre-push | 49 ++++++++++++++++++++++- .pql/changelog/ticket_history/2026-06.sql | 1 + .pql/changelog/ticket_idmap/2026-06.sql | 1 + .pql/changelog/tickets/2026-06.sql | 6 +++ 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/.githooks/pre-push b/.githooks/pre-push index 7319b82a..409ea1fa 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -4,9 +4,54 @@ # 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): run the full ~2min test suite only when the push touches +# lib/ (app + runtime Dart source) or pubspec.* (deps / version). test/, +# assets/, docs, and tooling changes ride along with a lib change in practice, +# and an otherwise-skipped push is covered by the next one that does touch lib. +# The full suite is always available via `make push-check`, and the release CI +# runs it forced on a tagged version. So a lib/pubspec-free push 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)" -echo "==> pre-push: make push-check" -make push-check +z40=0000000000000000000000000000000000000000 + +# Collect every file changed across the commits being pushed. git feeds the +# hook one line per ref on stdin: . +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 + +# Run the full gate when lib/ (app + runtime source) or pubspec.* (deps / +# version) 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 '^(lib/|pubspec\.)' || true)" + [[ -z "$trigger_files" ]] && needs_gate=0 +fi + +if [[ "$needs_gate" -eq 0 ]]; then + echo "==> pre-push: no lib/ or pubspec change — decisions + changelog gates, skipping tests" + make decisions-validate changelog-gate +else + echo "==> pre-push: make push-check" + make push-check +fi diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 89a0d16a..3245d28a 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -3508,3 +3508,4 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2T11GCV1EV07DYD5BZENTM', 'status', 'backlog', 'ready', NULL, '2026-06-10 16:22:37', '2026-06-10 16:22:37', '2026-06-10 16:22:37', NULL, 'c62c5ac18a88ba763b5194f89a2a7482', 2) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4RD1DDSYM4J7WYEGTXARB4', 'status', 'backlog', 'done', NULL, '2026-06-10 16:34:51', '2026-06-10 16:34:51', '2026-06-10 16:34:51', NULL, '56cde33409eb796ad4fb420ed3033c70', 2) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4XCM5KBXDDSCWJ37GPYG3R', 'status', 'backlog', 'done', NULL, '2026-06-10 16:57:07', '2026-06-10 16:57:07', '2026-06-10 16:57:07', NULL, '1ce291d54831d3ede687384037c379ff', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB50YE6S6YWNP2ZSFWES9B2W', 'status', 'backlog', 'done', NULL, '2026-06-10 17:13:53', '2026-06-10 17:13:53', '2026-06-10 17:13:53', NULL, '87b069d3f25c91622c9720147d359360', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/ticket_idmap/2026-06.sql b/.pql/changelog/ticket_idmap/2026-06.sql index 4895e174..a6215aba 100644 --- a/.pql/changelog/ticket_idmap/2026-06.sql +++ b/.pql/changelog/ticket_idmap/2026-06.sql @@ -172,3 +172,4 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4RD1DDSYM4J7WYEGTXARB4', 'T-345', '2026-06-10 16:33:23', '2026-06-10 16:33:23', NULL, '9f26d5036c05057d4bd0fc53ddaddafd', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4VG3N3YJSV8G7M1HFSYW2W', 'T-346', '2026-06-10 16:46:54', '2026-06-10 16:46:54', NULL, 'a64940cb0b4ec4fb489eac27065dc447', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4XCM5KBXDDSCWJ37GPYG3R', 'T-347', '2026-06-10 16:55:10', '2026-06-10 16:55:10', NULL, '9d40226dbc6136072d2d5d0eda71f141', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); +INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB50YE6S6YWNP2ZSFWES9B2W', 'T-348', '2026-06-10 17:10:42', '2026-06-10 17:10:42', NULL, '8bb5ad92551f272417840869a0774668', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 30fbc9ff..cd4cb6a9 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -3197,3 +3197,9 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat A GUI/desktop-launched app inherits a minimal PATH (e.g. /usr/bin:/bin) with no ~/.local/bin, so _findOnPath(''pql'') returns null, clide falls back to spawning the literal ''pql'', and Process.start fails with ENOENT — the pql pane errors. pql is installed to ~/.local/bin. Reproduced: ''env -i PATH=/usr/bin:/bin command -v pql'' → not found. Works under ''make run'' only because the terminal PATH includes ~/.local/bin. Fix: extend _expandedPath() to also augment on Linux — prepend ~/.local/bin and /usr/local/bin (keep the homebrew dirs macOS-only). Affects pql, tmux, and PATH-resolved git alike. User running the installed build needs a rebuild+reinstall after the fix; immediate workaround is launching clide from a terminal.', 'done', 'high', NULL, NULL, NULL, '2026-06-10 16:55:10', '2026-06-10 16:57:07', NULL, '98d93fef85789907f6bf6efc52a3fcdb', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB50YE6S6YWNP2ZSFWES9B2W', 'task', NULL, 'Pre-push gate: skip the test suite for docs/changelog/pql/meta-only pushes', 'The .githooks/pre-push hook runs ''make push-check'' unconditionally — the full ~2min coverage + test-core suite even when a push touches only documentation, the changelog, pql ticket data, or other meta files that cannot affect the Flutter/Dart build or tests. + +Make the hook path-aware: diff the commits being pushed (remote_sha..local_sha from the hook''s stdin). If every changed file is under an allowlist that can''t affect tests — docs/, governance/, .pql/, .claude/, CHANGELOG.md, README.md, CLAUDE.md — run only the instant gates (decisions-validate + changelog-gate) and skip test-coverage/coverage-gate/test-core. Any code-ish path (lib/, test/, assets/, pubspec.*, native/, ci/, .githooks/, Makefile, platform dirs) → full gate. Fail safe: unknown/new-branch states run the full gate.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 17:10:42', '2026-06-10 17:10:42', NULL, 'edb52a1ac5c6a0e01ebaaf5ceff5e96c', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB50YE6S6YWNP2ZSFWES9B2W', 'task', NULL, 'Pre-push gate: skip the test suite for docs/changelog/pql/meta-only pushes', 'The .githooks/pre-push hook runs ''make push-check'' unconditionally — the full ~2min coverage + test-core suite even when a push touches only documentation, the changelog, pql ticket data, or other meta files that cannot affect the Flutter/Dart build or tests. + +Make the hook path-aware: diff the commits being pushed (remote_sha..local_sha from the hook''s stdin). If every changed file is under an allowlist that can''t affect tests — docs/, governance/, .pql/, .claude/, CHANGELOG.md, README.md, CLAUDE.md — run only the instant gates (decisions-validate + changelog-gate) and skip test-coverage/coverage-gate/test-core. Any code-ish path (lib/, test/, assets/, pubspec.*, native/, ci/, .githooks/, Makefile, platform dirs) → full gate. Fail safe: unknown/new-branch states run the full gate.', 'done', 'medium', NULL, NULL, NULL, '2026-06-10 17:10:42', '2026-06-10 17:13:53', NULL, 'f846b01b35bda1b8e247f360c46d472f', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);