Skip to content

Commit 8e42b11

Browse files
committed
Guard local checks against low disk space
Signed-off-by: Tal Weiss <major.tal@gmail.com>
1 parent f53bbd1 commit 8e42b11

5 files changed

Lines changed: 208 additions & 6 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ jobs:
8686
run: scripts/test-mobile-worktree-overrides.sh
8787
- name: File size ratchet unit tests
8888
run: node --test scripts/check-file-sizes-core.test.mjs
89+
- name: Disk space preflight unit tests
90+
run: scripts/test-check-disk-space.sh
8991

9092
rust-lint:
9193
name: Rust Lint

CONTRIBUTING.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,26 @@ Adminer on `:8082`, Keycloak on `:8180` for local OAuth/OIDC testing, MinIO on
157157
`:9000` for media storage, and Prometheus on `:9090` for metrics) and runs all
158158
pending database migrations.
159159

160+
#### Disk space for local checks
161+
162+
Rust test, clippy, and Tauri builds can add roughly 15 GiB to a cold Cargo
163+
target. Before build-heavy pre-push jobs start, Buzz reserves that headroom and
164+
requires at least 10 GiB to remain afterward. With the defaults, the guard
165+
therefore blocks below 25 GiB free. Documentation-only pushes skip the check.
166+
If the preflight blocks, free space or run `just clean` before retrying.
167+
168+
Developers with a warm shared target can tune the estimate for one push without
169+
skipping the remaining hooks:
170+
171+
```bash
172+
BUZZ_DISK_BUILD_RESERVE_GIB=8 git push # default: 15
173+
BUZZ_DISK_MIN_FREE_GIB=5 git push # default: 10
174+
BUZZ_SKIP_DISK_PREFLIGHT=1 git push # bypass this guard only
175+
```
176+
177+
Use the escape hatch only after checking available disk space yourself. CI is
178+
unaffected by this local pre-push guard.
179+
160180
### Running the Relay and Desktop App
161181

162182
```bash

lefthook.yml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,31 +47,34 @@ commit-msg:
4747
run: 'git interpret-trailers --if-exists doNothing --trailer "Signed-off-by: $(git var GIT_COMMITTER_IDENT | sed ''s/ [0-9]* [+-][0-9]*$//'')" --in-place {1}'
4848

4949
pre-push:
50+
# Guard each build-heavy job before it reaches Cargo, Node, or Flutter. Keep
51+
# branch-skew unguarded and let the existing globs skip documentation-only
52+
# pushes. A cold local gate can otherwise consume the disk safety margin.
5053
parallel: true
5154
commands:
5255
branch-skew:
5356
run: ./scripts/check-branch-skew.sh
5457
rust-tests:
5558
glob: ["crates/**", "migrations/**", "schema/**", "Cargo.toml", "Cargo.lock", "rust-toolchain.toml", "deny.toml", "scripts/run-tests.sh", "justfile"]
56-
run: just test-unit
59+
run: ./scripts/check-disk-space.sh && just test-unit
5760
desktop-check:
5861
glob: ["desktop/**", "pnpm-lock.yaml"]
5962
exclude: ["desktop/src-tauri/**"]
60-
run: just desktop-check
63+
run: ./scripts/check-disk-space.sh && just desktop-check
6164
desktop-typecheck:
6265
glob: ["desktop/**", "pnpm-lock.yaml"]
6366
exclude: ["desktop/src-tauri/**"]
64-
run: just desktop-typecheck
67+
run: ./scripts/check-disk-space.sh && just desktop-typecheck
6568
desktop-test:
6669
glob: ["desktop/**", "pnpm-lock.yaml"]
6770
exclude: ["desktop/src-tauri/**"]
68-
run: just desktop-test
71+
run: ./scripts/check-disk-space.sh && just desktop-test
6972
desktop-tauri-checks:
7073
# Keep local lint parity with Desktop Core CI for every path that can
7174
# affect the Tauri crate or its path dependencies. Run clippy and tests
7275
# serially so parallel pre-push hooks do not contend for Cargo's lock.
7376
glob: ["desktop/src-tauri/**", "crates/**", "migrations/**", "schema/**", "Cargo.toml", "Cargo.lock", "rust-toolchain.toml", "deny.toml", "scripts/run-tests.sh", "justfile"]
74-
run: just desktop-tauri-clippy && just desktop-tauri-test
77+
run: ./scripts/check-disk-space.sh && just desktop-tauri-clippy && just desktop-tauri-test
7578
mobile-test:
7679
glob: ["mobile/**"]
77-
run: just mobile-test
80+
run: ./scripts/check-disk-space.sh && just mobile-test

scripts/check-disk-space.sh

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
5+
6+
case "${BUZZ_SKIP_DISK_PREFLIGHT:-}" in
7+
1|true|TRUE|yes|YES)
8+
echo "Disk preflight skipped (BUZZ_SKIP_DISK_PREFLIGHT=1)."
9+
exit 0
10+
;;
11+
esac
12+
13+
reserve_gib=${BUZZ_DISK_BUILD_RESERVE_GIB:-15}
14+
min_free_gib=${BUZZ_DISK_MIN_FREE_GIB:-10}
15+
16+
if [[ ! "$reserve_gib" =~ ^[0-9]+$ ]]; then
17+
echo "BUZZ_DISK_BUILD_RESERVE_GIB must be a non-negative integer." >&2
18+
exit 2
19+
fi
20+
if [[ ! "$min_free_gib" =~ ^[0-9]+$ ]]; then
21+
echo "BUZZ_DISK_MIN_FREE_GIB must be a non-negative integer." >&2
22+
exit 2
23+
fi
24+
25+
if ! available_kib=$(df -Pk "$repo_root" 2>/dev/null | awk 'NR == 2 { print $4 }'); then
26+
echo "Disk preflight warning: could not determine free space; continuing." >&2
27+
exit 0
28+
fi
29+
if [[ ! "${available_kib:-}" =~ ^[0-9]+$ ]]; then
30+
echo "Disk preflight warning: could not determine free space; continuing." >&2
31+
exit 0
32+
fi
33+
34+
reserve_kib=$((reserve_gib * 1024 * 1024))
35+
min_free_kib=$((min_free_gib * 1024 * 1024))
36+
post_build_kib=$((available_kib - reserve_kib))
37+
38+
available_gib=$(awk -v kib="$available_kib" 'BEGIN { printf "%.1f", kib / 1048576 }')
39+
post_build_gib=$(awk -v kib="$post_build_kib" 'BEGIN { printf "%.1f", kib / 1048576 }')
40+
41+
if ((post_build_kib < min_free_kib)); then
42+
cat >&2 <<EOF
43+
Disk preflight blocked: ${available_gib} GiB is free now, but reserving ${reserve_gib} GiB for Buzz's local checks would leave ${post_build_gib} GiB.
44+
The configured minimum is ${min_free_gib} GiB. Free disk space before pushing, or adjust BUZZ_DISK_BUILD_RESERVE_GIB / BUZZ_DISK_MIN_FREE_GIB if this checkout is already warm.
45+
To bypass only this guard while keeping the other hooks, set BUZZ_SKIP_DISK_PREFLIGHT=1.
46+
EOF
47+
exit 1
48+
fi
49+
50+
echo "Disk preflight passed: ${available_gib} GiB free; estimated post-check free space ${post_build_gib} GiB (minimum ${min_free_gib} GiB)."

scripts/test-check-disk-space.sh

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
5+
check_disk="${repo_root}/scripts/check-disk-space.sh"
6+
tmp=$(mktemp -d)
7+
trap 'rm -rf "$tmp"' EXIT
8+
9+
mkdir -p "$tmp/bin"
10+
cat >"$tmp/bin/df" <<'MOCK'
11+
#!/usr/bin/env bash
12+
printf '%s\n' \
13+
'Filesystem 1024-blocks Used Available Capacity Mounted on' \
14+
"/dev/mock ${MOCK_DISK_TOTAL_KIB} 1 ${MOCK_DISK_AVAILABLE_KIB} 1% /"
15+
MOCK
16+
chmod +x "$tmp/bin/df"
17+
18+
run_check() {
19+
PATH="$tmp/bin:$PATH" \
20+
BUZZ_DISK_BUILD_RESERVE_GIB=15 \
21+
BUZZ_DISK_MIN_FREE_GIB=10 \
22+
"$check_disk" "$@"
23+
}
24+
25+
# A 15 GiB build reserve must still leave the configured 10 GiB minimum.
26+
MOCK_DISK_TOTAL_KIB=$((100 * 1024 * 1024)) \
27+
MOCK_DISK_AVAILABLE_KIB=$((25 * 1024 * 1024)) \
28+
run_check >/dev/null
29+
30+
if MOCK_DISK_TOTAL_KIB=$((100 * 1024 * 1024)) \
31+
MOCK_DISK_AVAILABLE_KIB=$((24 * 1024 * 1024)) \
32+
run_check >"$tmp/low.out" 2>&1; then
33+
echo "disk preflight accepted insufficient post-build free space" >&2
34+
exit 1
35+
fi
36+
grep -Fq 'Disk preflight blocked' "$tmp/low.out"
37+
grep -Fq 'minimum is 10 GiB' "$tmp/low.out"
38+
grep -Fq 'BUZZ_SKIP_DISK_PREFLIGHT=1' "$tmp/low.out"
39+
40+
if PATH="$tmp/bin:$PATH" \
41+
MOCK_DISK_TOTAL_KIB=$((100 * 1024 * 1024)) \
42+
MOCK_DISK_AVAILABLE_KIB=$((40 * 1024 * 1024)) \
43+
BUZZ_DISK_BUILD_RESERVE_GIB=15 \
44+
BUZZ_DISK_MIN_FREE_GIB=invalid \
45+
"$check_disk" >"$tmp/invalid.out" 2>&1; then
46+
echo "disk preflight accepted an invalid absolute minimum" >&2
47+
exit 1
48+
fi
49+
grep -Fq 'BUZZ_DISK_MIN_FREE_GIB must be a non-negative integer' \
50+
"$tmp/invalid.out"
51+
52+
# The escape hatch bypasses only this guard, leaving the other hooks intact.
53+
MOCK_DISK_TOTAL_KIB=$((100 * 1024 * 1024)) \
54+
MOCK_DISK_AVAILABLE_KIB=1 \
55+
BUZZ_SKIP_DISK_PREFLIGHT=1 \
56+
run_check >/dev/null
57+
58+
# Verify the behavior through a real Git pre-push, not only config inspection.
59+
fixture="$tmp/hook-repo"
60+
remote="$tmp/remote.git"
61+
git init --bare -q "$remote"
62+
git init -q -b main "$fixture"
63+
git -C "$fixture" config user.name test
64+
git -C "$fixture" config user.email test@example.com
65+
git -C "$fixture" remote add origin "$remote"
66+
mkdir -p "$fixture/scripts"
67+
cp "$check_disk" "$fixture/scripts/check-disk-space.sh"
68+
cp "$repo_root/scripts/check-branch-skew.sh" "$fixture/scripts/check-branch-skew.sh"
69+
cp "$repo_root/lefthook.yml" "$fixture/lefthook.yml"
70+
git -C "$fixture" add .
71+
git -C "$fixture" -c core.hooksPath=/dev/null commit -qm baseline
72+
git -C "$fixture" -c core.hooksPath=/dev/null push -q -u origin main
73+
(
74+
# Use the Hermit stub rather than a bare `lefthook`: CI's `changes` job runs
75+
# this script without activating Hermit, so only the pinned repo binary is
76+
# guaranteed to resolve.
77+
cd "$fixture"
78+
"$repo_root/bin/lefthook" install --force >/dev/null
79+
)
80+
81+
# Documentation-only pushes skip all guarded jobs.
82+
printf '%s\n' '# docs' >"$fixture/README.md"
83+
git -C "$fixture" add .
84+
git -C "$fixture" -c core.hooksPath=/dev/null commit -qm docs-change
85+
PATH="$tmp/bin:$PATH" \
86+
MOCK_DISK_TOTAL_KIB=$((100 * 1024 * 1024)) \
87+
MOCK_DISK_AVAILABLE_KIB=1 \
88+
git -C "$fixture" push -q
89+
90+
mkdir -p "$fixture/crates/example/src"
91+
printf '%s\n' 'pub fn example() {}' >"$fixture/crates/example/src/lib.rs"
92+
git -C "$fixture" add .
93+
git -C "$fixture" -c core.hooksPath=/dev/null commit -qm heavy-change
94+
if PATH="$tmp/bin:$PATH" \
95+
MOCK_DISK_TOTAL_KIB=$((100 * 1024 * 1024)) \
96+
MOCK_DISK_AVAILABLE_KIB=$((24 * 1024 * 1024)) \
97+
git -C "$fixture" push >"$tmp/hook.out" 2>&1; then
98+
echo "real pre-push hook bypassed the disk preflight" >&2
99+
exit 1
100+
fi
101+
if ! grep -Fq 'Disk preflight blocked' "$tmp/hook.out"; then
102+
echo "real pre-push failed for an unexpected reason:" >&2
103+
cat "$tmp/hook.out" >&2
104+
exit 1
105+
fi
106+
107+
# Unsupported df output fails open so the hook remains portable.
108+
cat >"$tmp/bin/df" <<'MOCK'
109+
#!/usr/bin/env bash
110+
echo unsupported
111+
MOCK
112+
chmod +x "$tmp/bin/df"
113+
run_check >"$tmp/unsupported.out" 2>&1
114+
grep -Fq 'could not determine free space' "$tmp/unsupported.out"
115+
116+
# One guarded pre-push job each for rust-tests, desktop-check, desktop-typecheck,
117+
# desktop-test, desktop-tauri-checks, and mobile-test. Bump when a build-heavy
118+
# job is added to lefthook.yml.
119+
[[ "$(grep -Fc 'run: ./scripts/check-disk-space.sh &&' "$repo_root/lefthook.yml")" -eq 6 ]]
120+
if grep -Fq 'setup:' "$repo_root/lefthook.yml"; then
121+
echo "disk preflight must not use Lefthook setup on pinned version 2.1.3" >&2
122+
exit 1
123+
fi
124+
grep -Fq 'scripts/test-check-disk-space.sh' \
125+
"$repo_root/.github/workflows/ci.yml"
126+
127+
echo "disk-space preflight tests passed"

0 commit comments

Comments
 (0)