Skip to content

Commit c3e51ca

Browse files
zackeesclaude
andauthored
fix(hooks): lower Stop timeout + add opt-in pre-push workspace gate (#462) (#469)
Final umbrella items from #462 once #464 (PEP 723) and #465 (per-crate scoping) have shipped: - **Stop hook timeout 600s → 120s.** Anything past 2 min on Stop is treated as a hang now, not silently accepted. With #465's scoped clippy/test, a typical single-crate edit completes well under that. - **`ci/git-hooks/pre-push`** runs the workspace-wide gate (`cargo fmt --check`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace`) before any branch leaves the machine. Opt in once per clone with `git config core.hooksPath ci/git-hooks`. - Auto-skips when no `.rs`/`Cargo.toml`/`Cargo.lock`/toolchain change is in the push range, so doc-only pushes are free. - Bypass via `git push --no-verify` or `FBUILD_SKIP_PRE_PUSH=1`. Rationale: Stop hook is per-conversation interactive feedback (fast, scoped). Push is the boundary where workspace correctness actually matters. CI still runs the same workspace verification — pre-push just catches escapes locally. Items from #462 that are already covered: - *Skip-gate by file-extension diff* → #465 (no-Rust-changes returns early; doc-only edits skip entirely). - *Scope clippy + test to affected crates* → #465. - *Serialize lint then test* → #465 keeps the concurrent-with-early-exit pattern; the early-exit on lint failure already captures the "don't waste time testing if lint is broken" win. Closes #462. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 02dd530 commit c3e51ca

3 files changed

Lines changed: 128 additions & 1 deletion

File tree

.claude/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
{
6363
"type": "command",
6464
"command": "cd \"$(git rev-parse --show-toplevel)\" && uv run --script ci/hooks/check-on-stop.py",
65-
"timeout": 600
65+
"timeout": 120
6666
}
6767
]
6868
}

ci/git-hooks/README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Local git hooks
2+
3+
Tracked git hooks for fbuild. Opt in **once per clone**:
4+
5+
```bash
6+
git config core.hooksPath ci/git-hooks
7+
```
8+
9+
After that, every hook in this directory is wired up automatically. The
10+
directory is on the repo's normal source path so updates flow with
11+
`git pull`.
12+
13+
## Hooks
14+
15+
- **`pre-push`** — workspace-wide safety net. Runs `cargo fmt --check`,
16+
`cargo clippy --workspace --all-targets -- -D warnings`, and
17+
`cargo test --workspace` before any branch is pushed. Skipped when
18+
the push touches no Rust files. Bypass with `git push --no-verify`
19+
or `FBUILD_SKIP_PRE_PUSH=1 git push`.
20+
21+
Why here and not in the Stop hook: the Claude Code Stop hook
22+
(`ci/hooks/check-on-stop.py`) is scoped to changed crates for fast
23+
iteration (#465). The workspace-wide gate belongs at the
24+
*push* boundary, where it actually prevents an escape, not at the
25+
conversation-stop boundary, where it just adds latency. See #462.
26+
27+
## Opting out per-push
28+
29+
```bash
30+
git push --no-verify # bypass all hooks
31+
FBUILD_SKIP_PRE_PUSH=1 git push # bypass only pre-push, keep others
32+
```
33+
34+
## Adding a new hook
35+
36+
1. Drop an executable script named exactly the git event
37+
(`pre-commit`, `commit-msg`, `pre-push`, `post-merge`, ...).
38+
2. Make it skip cleanly when there's no work to do — `pre-push` here
39+
filters by changed-file extensions so non-Rust pushes are free.
40+
3. Document the bypass mechanism in the script header.

ci/git-hooks/pre-push

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/usr/bin/env sh
2+
# Pre-push gate: full workspace lint + tests before code leaves this machine.
3+
#
4+
# Stop hook (`ci/hooks/check-on-stop.py`) is scoped to the changed crates
5+
# for fast iteration feedback — see #465. This hook is the workspace-wide
6+
# safety net. CI also runs `cargo clippy --workspace` + `cargo test
7+
# --workspace` on every push; this just catches escapes locally so you
8+
# don't need to wait on a remote runner to find out.
9+
#
10+
# Opt in once per clone:
11+
#
12+
# git config core.hooksPath ci/git-hooks
13+
#
14+
# Bypass for a single push (e.g. emergency fix where CI must arbitrate):
15+
#
16+
# git push --no-verify
17+
#
18+
# Skipped automatically when:
19+
# - the cargo workspace has no `.rs`, `Cargo.toml`, or `Cargo.lock`
20+
# differences vs the upstream HEAD being pushed to
21+
# - the env var `FBUILD_SKIP_PRE_PUSH=1` is set
22+
#
23+
# Exit codes:
24+
# 0 - all checks passed (or skipped)
25+
# 1 - lint or test failed; push is rejected
26+
27+
set -e
28+
29+
if [ "${FBUILD_SKIP_PRE_PUSH:-0}" = "1" ]; then
30+
echo "pre-push: FBUILD_SKIP_PRE_PUSH=1 — skipping workspace gate"
31+
exit 0
32+
fi
33+
34+
REPO_ROOT="$(git rev-parse --show-toplevel)"
35+
cd "$REPO_ROOT"
36+
37+
# Read git's per-ref input from stdin: <local ref> <local sha> <remote ref> <remote sha>
38+
# When pushing a new branch, remote sha is 40 zeros — fall back to upstream/HEAD.
39+
ZERO="0000000000000000000000000000000000000000"
40+
RUST_TOUCHED=0
41+
while read -r _local_ref local_sha _remote_ref remote_sha; do
42+
if [ -z "$local_sha" ] || [ "$local_sha" = "$ZERO" ]; then
43+
# Deletion — nothing to lint.
44+
continue
45+
fi
46+
if [ -z "$remote_sha" ] || [ "$remote_sha" = "$ZERO" ]; then
47+
# New branch — diff against upstream's default branch.
48+
base="$(git merge-base "$local_sha" origin/HEAD 2>/dev/null || echo "")"
49+
if [ -z "$base" ]; then
50+
# No upstream HEAD known — be conservative and run the gate.
51+
RUST_TOUCHED=1
52+
break
53+
fi
54+
else
55+
base="$remote_sha"
56+
fi
57+
if git diff --name-only "$base" "$local_sha" 2>/dev/null \
58+
| grep -Eq '(\.rs$|Cargo\.toml$|Cargo\.lock$|rust-toolchain\.toml$|^\.cargo/)'; then
59+
RUST_TOUCHED=1
60+
break
61+
fi
62+
done
63+
64+
if [ "$RUST_TOUCHED" = "0" ]; then
65+
echo "pre-push: no Rust changes in this push — skipping workspace gate"
66+
exit 0
67+
fi
68+
69+
echo "pre-push: running workspace lint + tests"
70+
echo " (set FBUILD_SKIP_PRE_PUSH=1 to bypass)"
71+
72+
soldr cargo fmt --all -- --check || {
73+
echo "pre-push: rustfmt --check failed; run \`soldr cargo fmt --all\`" >&2
74+
exit 1
75+
}
76+
77+
soldr cargo clippy --workspace --all-targets -- -D warnings || {
78+
echo "pre-push: clippy --workspace failed" >&2
79+
exit 1
80+
}
81+
82+
soldr cargo test --workspace || {
83+
echo "pre-push: cargo test --workspace failed" >&2
84+
exit 1
85+
}
86+
87+
echo "pre-push: workspace gate passed"

0 commit comments

Comments
 (0)