- Both hotspot (≥9.5) and average (≥8.9) gates now block commit/push/CI - Current average: 8.97 — threshold set at 8.9 to block regressions without blocking the current state; aspirational target remains 9.5 - pre-commit: replaced phantom pre_commit_code_health_safeguard with real inline check - pre-push: CodeScene step is now blocking (was informational-only) - CLAUDE.md: explicit instructions to monitor both scores via MCP CodeScene
157 lines
6.4 KiB
Bash
Executable File
157 lines
6.4 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..."
|
|
pnpm playwright:smoke
|
|
echo " ✅ Smoke tests OK"
|
|
else
|
|
echo "⏭️ [4/5] Playwright smoke tests — skipped (no tests/smoke/*.spec.ts)"
|
|
fi
|
|
|
|
# ── 5. CodeScene code health gate ────────────────────────────────────────
|
|
# Blocks push if Hotspot < 9.5 OR Average < 9.0.
|
|
# Remote scores reflect state after last push — this catches regressions
|
|
# introduced in a previous push that weren't caught yet.
|
|
echo ""
|
|
echo "🏥 [5/5] CodeScene code health (Hotspot ≥9.5 + Average ≥9.0)..."
|
|
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: 9.5)"
|
|
echo " Remote Average Code Health: $AVERAGE_SCORE (threshold: 8.9)"
|
|
python3 -c "
|
|
import sys
|
|
hotspot = float('$HOTSPOT_SCORE')
|
|
average = float('$AVERAGE_SCORE')
|
|
failed = False
|
|
if hotspot < 9.5:
|
|
print(f'FAIL: Hotspot Code Health {hotspot:.2f} < 9.5')
|
|
failed = True
|
|
else:
|
|
print(f'OK: Hotspot {hotspot:.2f} >= 9.5')
|
|
if average < 8.9:
|
|
print(f'FAIL: Average Code Health {average:.2f} < 9.0 — regressions detected, fix before pushing')
|
|
failed = True
|
|
else:
|
|
print(f'OK: Average {average:.2f} >= 9.0')
|
|
if failed:
|
|
sys.exit(1)
|
|
" || 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 ""
|