Files
tolaria/.husky/pre-push
Test cebeca678f fix: use floor instead of round in CodeScene ratchet
round(9.8457, 2) → 9.85 which exceeds the actual score, causing
the threshold to be unreachable. Use math.floor to truncate instead:
9.8457 → 9.84, 9.3884 → 9.38.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:01:03 +02:00

189 lines
7.9 KiB
Bash
Executable File

#!/bin/sh
# Pre-push: full CI checks run locally before any push.
# This replaces remote CI for normal task pushes.
# DO NOT skip with --no-verify (Claude Code is configured to never do this).
#
# ── Optimizations (Feb 2026) ─────────────────────────────────────────────
#
# 1. --no-clean on cargo llvm-cov: reuses previous instrumented build
# artifacts for incremental compilation. Reduces Rust coverage from
# ~8 min (full recompile) to ~30-60s (incremental rebuild).
#
# 2. Change detection: skips Rust checks entirely when no files under
# src-tauri/ changed. Saves ~1-2 min on frontend-only pushes.
#
# 3. Merged redundant test runs: frontend coverage (step 2) already runs
# all tests, so the separate test step was removed.
#
# 4. Fast-fail ordering: within Rust checks, fast lints (fmt ~2s, clippy
# ~15s) run before slow coverage (~30-60s) for quicker feedback.
#
# 5. Force full coverage: set LAPUTA_FULL_COVERAGE=1 to run cargo llvm-cov
# without --no-clean (clean rebuild, accurate baseline).
#
# Expected times (warm cache, incremental):
# Frontend only: ~1 min
# Frontend+Rust: ~2-3 min
# Full coverage: ~9-10 min (with LAPUTA_FULL_COVERAGE=1)
# ─────────────────────────────────────────────────────────────────────────
set -e
START_TIME=$(date +%s)
echo ""
echo "🚀 Pre-push checks (replaces CI — do not skip)"
echo "================================================"
# ── Detect what changed ─────────────────────────────────────────────────
PUSH_TARGET=$(git rev-parse @{push} 2>/dev/null || echo "")
RUST_CHANGED=true
if [ -n "$PUSH_TARGET" ]; then
CHANGED=$(git diff --name-only "$PUSH_TARGET"..HEAD)
if ! echo "$CHANGED" | grep -qE '^(src-tauri/|Cargo)'; then
RUST_CHANGED=false
fi
fi
# ── 0. TypeScript + Vite build ──────────────────────────────────────────
echo ""
echo "📦 [0/5] TypeScript + Vite build..."
pnpm exec tsc --noEmit
pnpm build
echo " ✅ Build OK"
# ── 1. Frontend coverage (≥70%) — includes all unit tests ───────────────
echo ""
echo "📊 [1/5] Frontend tests + coverage (≥70%)..."
pnpm test:coverage --silent
echo " ✅ Frontend coverage OK"
# ── 2. Rust lint (clippy + fmt) — fast, run before coverage ─────────────
echo ""
if [ "$RUST_CHANGED" = true ]; then
echo "🔧 [2/5] Clippy + rustfmt..."
cargo clippy --manifest-path=src-tauri/Cargo.toml -- -D warnings
cargo fmt --manifest-path=src-tauri/Cargo.toml -- --check
echo " ✅ Rust lint OK"
else
echo "⏭️ [2/5] Rust lint — skipped (no src-tauri/ changes)"
fi
# ── 3. Rust coverage (≥85% lines) ──────────────────────────────────────
echo ""
if [ "$RUST_CHANGED" = true ]; then
LLVM_COV_FLAGS="--no-clean"
if [ "${LAPUTA_FULL_COVERAGE:-0}" = "1" ]; then
LLVM_COV_FLAGS=""
echo "🦀 [3/5] Rust coverage — FULL (LAPUTA_FULL_COVERAGE=1)..."
else
echo "🦀 [3/5] Rust coverage (≥85%, incremental)..."
fi
# Unset GIT_DIR so git tests create isolated repos without inheriting hook context
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE
# shellcheck disable=SC2086
cargo llvm-cov \
--manifest-path src-tauri/Cargo.toml \
$LLVM_COV_FLAGS \
--ignore-filename-regex "search\.rs|lib\.rs" \
--fail-under-lines 85 \
-- --test-threads=1
echo " ✅ Rust coverage OK"
else
echo "⏭️ [3/5] Rust coverage — skipped (no src-tauri/ changes)"
fi
# ── 4. Playwright smoke tests (if any exist) ──────────────────────────
echo ""
SMOKE_FILES=$(find tests/smoke -name '*.spec.ts' 2>/dev/null | head -1)
if [ -n "$SMOKE_FILES" ]; then
echo "🎭 [4/5] Playwright smoke tests..."
SMOKE_OUTPUT=$(pnpm playwright:smoke 2>&1) || true
echo "$SMOKE_OUTPUT" | tail -20
# Fail only on real failures (not flaky). Flaky = passed on retry.
if echo "$SMOKE_OUTPUT" | grep -qE '^\s+\d+ failed' && ! echo "$SMOKE_OUTPUT" | grep -qE '^\s+\d+ passed'; then
echo " ❌ Smoke tests FAILED"
exit 1
fi
echo " ✅ Smoke tests OK"
else
echo "⏭️ [4/5] Playwright smoke tests — skipped (no tests/smoke/*.spec.ts)"
fi
# ── 5. CodeScene code health gate (ratchet) ──────────────────────────────
# Thresholds live in .codescene-thresholds and only ever go UP (ratchet).
# After every successful push the file is updated if scores improved.
THRESHOLDS_FILE="$(git rev-parse --show-toplevel)/.codescene-thresholds"
HOTSPOT_MIN=9.5
AVERAGE_MIN=9.31
if [ -f "$THRESHOLDS_FILE" ]; then
HOTSPOT_MIN=$(grep HOTSPOT_THRESHOLD "$THRESHOLDS_FILE" | cut -d= -f2)
AVERAGE_MIN=$(grep AVERAGE_THRESHOLD "$THRESHOLDS_FILE" | cut -d= -f2)
fi
echo ""
echo "🏥 [5/5] CodeScene code health (Hotspot ≥${HOTSPOT_MIN} + Average ≥${AVERAGE_MIN})..."
if [ -z "$CODESCENE_PAT" ] || [ -z "$CODESCENE_PROJECT_ID" ]; then
echo " ⚠️ CODESCENE_PAT or CODESCENE_PROJECT_ID not set — skipping"
else
API_RESPONSE=$(curl -sf \
-H "Authorization: Bearer $CODESCENE_PAT" \
-H "Accept: application/json" \
"https://api.codescene.io/v2/projects/$CODESCENE_PROJECT_ID" 2>/dev/null || echo "{}")
HOTSPOT_SCORE=$(echo "$API_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['analysis']['hotspot_code_health']['now'])" 2>/dev/null || echo "")
AVERAGE_SCORE=$(echo "$API_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['analysis']['code_health']['now'])" 2>/dev/null || echo "")
if [ -z "$HOTSPOT_SCORE" ] || [ -z "$AVERAGE_SCORE" ]; then
echo " ⚠️ Could not fetch remote scores — skipping (CI will enforce)"
else
echo " Remote Hotspot Code Health: $HOTSPOT_SCORE (threshold: $HOTSPOT_MIN)"
echo " Remote Average Code Health: $AVERAGE_SCORE (threshold: $AVERAGE_MIN)"
python3 -c "
import sys, os
hotspot = float('$HOTSPOT_SCORE')
average = float('$AVERAGE_SCORE')
hotspot_min = float('$HOTSPOT_MIN')
average_min = float('$AVERAGE_MIN')
failed = False
if hotspot < hotspot_min:
print(f'FAIL: Hotspot Code Health {hotspot:.2f} < {hotspot_min}')
failed = True
else:
print(f'OK: Hotspot {hotspot:.2f} >= {hotspot_min}')
if average < average_min:
print(f'FAIL: Average Code Health {average:.2f} < {average_min} — regressions detected, fix before pushing')
failed = True
else:
print(f'OK: Average {average:.2f} >= {average_min}')
if failed:
sys.exit(1)
# Ratchet: update thresholds file if scores improved
# Truncate (floor) to 2 decimals so the threshold never exceeds the actual score
import math
thresholds_file = '$THRESHOLDS_FILE'
new_hotspot = max(hotspot_min, math.floor(hotspot * 100) / 100)
new_average = max(average_min, math.floor(average * 100) / 100)
if new_hotspot > hotspot_min or new_average > average_min:
with open(thresholds_file, 'w') as f:
f.write(f'HOTSPOT_THRESHOLD={new_hotspot}\nAVERAGE_THRESHOLD={new_average}\n')
print(f' 📈 Ratchet updated: Hotspot {hotspot_min} → {new_hotspot}, Average {average_min} → {new_average}')
# Stage and amend the thresholds update into the last commit would change history — instead just auto-commit it
os.system(f'git add {thresholds_file} && git commit -m \"chore: ratchet CodeScene thresholds to {new_hotspot}/{new_average}\" --no-verify 2>/dev/null || true')
" || exit 1
fi
fi
END_TIME=$(date +%s)
ELAPSED=$((END_TIME - START_TIME))
MINUTES=$((ELAPSED / 60))
SECONDS=$((ELAPSED % 60))
echo ""
echo "================================================"
echo "✅ All checks passed — pushing (${MINUTES}m ${SECONDS}s)"
echo ""