From 2862c44ac1f6659a0f7da465eaa5b303bced59bf Mon Sep 17 00:00:00 2001 From: Tal Weiss Date: Mon, 3 Aug 2026 16:17:06 +0200 Subject: [PATCH] Guard local checks against low disk space Signed-off-by: Tal Weiss --- .github/workflows/ci.yml | 2 + CONTRIBUTING.md | 20 +++++ lefthook.yml | 13 ++-- scripts/check-disk-space.sh | 50 +++++++++++++ scripts/test-check-disk-space.sh | 121 +++++++++++++++++++++++++++++++ 5 files changed, 201 insertions(+), 5 deletions(-) create mode 100755 scripts/check-disk-space.sh create mode 100755 scripts/test-check-disk-space.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60507182d5..9228a45050 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,6 +86,8 @@ jobs: run: scripts/test-mobile-worktree-overrides.sh - name: File size ratchet unit tests run: node --test scripts/check-file-sizes-core.test.mjs + - name: Disk space preflight unit tests + run: scripts/test-check-disk-space.sh rust-lint: name: Rust Lint diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index db0aea637f..3dce44b2e7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -157,6 +157,26 @@ Adminer on `:8082`, Keycloak on `:8180` for local OAuth/OIDC testing, MinIO on `:9000` for media storage, and Prometheus on `:9090` for metrics) and runs all pending database migrations. +#### Disk space for local checks + +Rust test, clippy, and Tauri builds can add roughly 15 GiB to a cold Cargo +target. Before build-heavy pre-push jobs start, Buzz reserves that headroom and +requires at least 10 GiB to remain afterward. With the defaults, the guard +therefore blocks below 25 GiB free. Documentation-only pushes skip the check. +If the preflight blocks, free space or run `just clean` before retrying. + +Developers with a warm shared target can tune the estimate for one push without +skipping the remaining hooks: + +```bash +BUZZ_DISK_BUILD_RESERVE_GIB=8 git push # default: 15 +BUZZ_DISK_MIN_FREE_GIB=5 git push # default: 10 +BUZZ_SKIP_DISK_PREFLIGHT=1 git push # bypass this guard only +``` + +Use the escape hatch only after checking available disk space yourself. CI is +unaffected by this local pre-push guard. + ### Running the Relay and Desktop App ```bash diff --git a/lefthook.yml b/lefthook.yml index 75d205722f..fe39479a55 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -46,27 +46,30 @@ commit-msg: 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}' pre-push: + # Guard each build-heavy job before it reaches Cargo, Node, or Flutter. Keep + # branch-skew unguarded and let the existing globs skip documentation-only + # pushes. A cold local gate can otherwise consume the disk safety margin. parallel: true commands: branch-skew: run: ./scripts/check-branch-skew.sh rust-tests: glob: ["crates/**", "migrations/**", "schema/**", "Cargo.toml", "Cargo.lock", "rust-toolchain.toml", "deny.toml", "scripts/run-tests.sh", "justfile"] - run: just test-unit + run: ./scripts/check-disk-space.sh && just test-unit desktop-check: glob: ["desktop/**", "pnpm-lock.yaml"] exclude: ["desktop/src-tauri/**"] - run: just desktop-check + run: ./scripts/check-disk-space.sh && just desktop-check desktop-test: glob: ["desktop/**", "pnpm-lock.yaml"] exclude: ["desktop/src-tauri/**"] - run: just desktop-test + run: ./scripts/check-disk-space.sh && just desktop-test desktop-tauri-checks: # Keep local lint parity with Desktop Core CI for every path that can # affect the Tauri crate or its path dependencies. Run clippy and tests # serially so parallel pre-push hooks do not contend for Cargo's lock. glob: ["desktop/src-tauri/**", "crates/**", "migrations/**", "schema/**", "Cargo.toml", "Cargo.lock", "rust-toolchain.toml", "deny.toml", "scripts/run-tests.sh", "justfile"] - run: just desktop-tauri-clippy && just desktop-tauri-test + run: ./scripts/check-disk-space.sh && just desktop-tauri-clippy && just desktop-tauri-test mobile-test: glob: ["mobile/**"] - run: just mobile-test + run: ./scripts/check-disk-space.sh && just mobile-test diff --git a/scripts/check-disk-space.sh b/scripts/check-disk-space.sh new file mode 100755 index 0000000000..319ee230c1 --- /dev/null +++ b/scripts/check-disk-space.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) + +case "${BUZZ_SKIP_DISK_PREFLIGHT:-}" in + 1|true|TRUE|yes|YES) + echo "Disk preflight skipped (BUZZ_SKIP_DISK_PREFLIGHT=1)." + exit 0 + ;; +esac + +reserve_gib=${BUZZ_DISK_BUILD_RESERVE_GIB:-15} +min_free_gib=${BUZZ_DISK_MIN_FREE_GIB:-10} + +if [[ ! "$reserve_gib" =~ ^[0-9]+$ ]]; then + echo "BUZZ_DISK_BUILD_RESERVE_GIB must be a non-negative integer." >&2 + exit 2 +fi +if [[ ! "$min_free_gib" =~ ^[0-9]+$ ]]; then + echo "BUZZ_DISK_MIN_FREE_GIB must be a non-negative integer." >&2 + exit 2 +fi + +if ! available_kib=$(df -Pk "$repo_root" 2>/dev/null | awk 'NR == 2 { print $4 }'); then + echo "Disk preflight warning: could not determine free space; continuing." >&2 + exit 0 +fi +if [[ ! "${available_kib:-}" =~ ^[0-9]+$ ]]; then + echo "Disk preflight warning: could not determine free space; continuing." >&2 + exit 0 +fi + +reserve_kib=$((reserve_gib * 1024 * 1024)) +min_free_kib=$((min_free_gib * 1024 * 1024)) +post_build_kib=$((available_kib - reserve_kib)) + +available_gib=$(awk -v kib="$available_kib" 'BEGIN { printf "%.1f", kib / 1048576 }') +post_build_gib=$(awk -v kib="$post_build_kib" 'BEGIN { printf "%.1f", kib / 1048576 }') + +if ((post_build_kib < min_free_kib)); then + cat >&2 <"$tmp/bin/df" <<'MOCK' +#!/usr/bin/env bash +printf '%s\n' \ + 'Filesystem 1024-blocks Used Available Capacity Mounted on' \ + "/dev/mock ${MOCK_DISK_TOTAL_KIB} 1 ${MOCK_DISK_AVAILABLE_KIB} 1% /" +MOCK +chmod +x "$tmp/bin/df" + +run_check() { + PATH="$tmp/bin:$PATH" \ + BUZZ_DISK_BUILD_RESERVE_GIB=15 \ + BUZZ_DISK_MIN_FREE_GIB=10 \ + "$check_disk" "$@" +} + +# A 15 GiB build reserve must still leave the configured 10 GiB minimum. +MOCK_DISK_TOTAL_KIB=$((100 * 1024 * 1024)) \ +MOCK_DISK_AVAILABLE_KIB=$((25 * 1024 * 1024)) \ + run_check >/dev/null + +if MOCK_DISK_TOTAL_KIB=$((100 * 1024 * 1024)) \ + MOCK_DISK_AVAILABLE_KIB=$((24 * 1024 * 1024)) \ + run_check >"$tmp/low.out" 2>&1; then + echo "disk preflight accepted insufficient post-build free space" >&2 + exit 1 +fi +grep -Fq 'Disk preflight blocked' "$tmp/low.out" +grep -Fq 'minimum is 10 GiB' "$tmp/low.out" +grep -Fq 'BUZZ_SKIP_DISK_PREFLIGHT=1' "$tmp/low.out" + +if PATH="$tmp/bin:$PATH" \ + MOCK_DISK_TOTAL_KIB=$((100 * 1024 * 1024)) \ + MOCK_DISK_AVAILABLE_KIB=$((40 * 1024 * 1024)) \ + BUZZ_DISK_BUILD_RESERVE_GIB=15 \ + BUZZ_DISK_MIN_FREE_GIB=invalid \ + "$check_disk" >"$tmp/invalid.out" 2>&1; then + echo "disk preflight accepted an invalid absolute minimum" >&2 + exit 1 +fi +grep -Fq 'BUZZ_DISK_MIN_FREE_GIB must be a non-negative integer' \ + "$tmp/invalid.out" + +# The escape hatch bypasses only this guard, leaving the other hooks intact. +MOCK_DISK_TOTAL_KIB=$((100 * 1024 * 1024)) \ +MOCK_DISK_AVAILABLE_KIB=1 \ +BUZZ_SKIP_DISK_PREFLIGHT=1 \ + run_check >/dev/null + +# Verify the behavior through a real Git pre-push, not only config inspection. +fixture="$tmp/hook-repo" +remote="$tmp/remote.git" +git init --bare -q "$remote" +git init -q -b main "$fixture" +git -C "$fixture" config user.name test +git -C "$fixture" config user.email test@example.com +git -C "$fixture" remote add origin "$remote" +mkdir -p "$fixture/scripts" +cp "$check_disk" "$fixture/scripts/check-disk-space.sh" +cp "$repo_root/scripts/check-branch-skew.sh" "$fixture/scripts/check-branch-skew.sh" +cp "$repo_root/lefthook.yml" "$fixture/lefthook.yml" +git -C "$fixture" add . +git -C "$fixture" -c core.hooksPath=/dev/null commit -qm baseline +git -C "$fixture" -c core.hooksPath=/dev/null push -q -u origin main +( + cd "$fixture" + lefthook install --force >/dev/null +) + +# Documentation-only pushes skip all guarded jobs. +printf '%s\n' '# docs' >"$fixture/README.md" +git -C "$fixture" add . +git -C "$fixture" -c core.hooksPath=/dev/null commit -qm docs-change +PATH="$tmp/bin:$PATH" \ +MOCK_DISK_TOTAL_KIB=$((100 * 1024 * 1024)) \ +MOCK_DISK_AVAILABLE_KIB=1 \ + git -C "$fixture" push -q + +mkdir -p "$fixture/crates/example/src" +printf '%s\n' 'pub fn example() {}' >"$fixture/crates/example/src/lib.rs" +git -C "$fixture" add . +git -C "$fixture" -c core.hooksPath=/dev/null commit -qm heavy-change +if PATH="$tmp/bin:$PATH" \ + MOCK_DISK_TOTAL_KIB=$((100 * 1024 * 1024)) \ + MOCK_DISK_AVAILABLE_KIB=$((24 * 1024 * 1024)) \ + git -C "$fixture" push >"$tmp/hook.out" 2>&1; then + echo "real pre-push hook bypassed the disk preflight" >&2 + exit 1 +fi +if ! grep -Fq 'Disk preflight blocked' "$tmp/hook.out"; then + echo "real pre-push failed for an unexpected reason:" >&2 + cat "$tmp/hook.out" >&2 + exit 1 +fi + +# Unsupported df output fails open so the hook remains portable. +cat >"$tmp/bin/df" <<'MOCK' +#!/usr/bin/env bash +echo unsupported +MOCK +chmod +x "$tmp/bin/df" +run_check >"$tmp/unsupported.out" 2>&1 +grep -Fq 'could not determine free space' "$tmp/unsupported.out" + +[[ "$(grep -Fc 'run: ./scripts/check-disk-space.sh &&' "$repo_root/lefthook.yml")" -eq 5 ]] +if grep -Fq 'setup:' "$repo_root/lefthook.yml"; then + echo "disk preflight must not use Lefthook setup on pinned version 2.1.3" >&2 + exit 1 +fi +grep -Fq 'scripts/test-check-disk-space.sh' \ + "$repo_root/.github/workflows/ci.yml" + +echo "disk-space preflight tests passed"