111 lines
4.2 KiB
Bash
Executable File
111 lines
4.2 KiB
Bash
Executable File
#!/bin/sh
|
|
# Pre-commit: fast local gate before commit. Full suite runs in pre-push/CI.
|
|
set -e
|
|
|
|
ensure_node_tooling() {
|
|
if command -v node >/dev/null 2>&1 && command -v pnpm >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
|
|
NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
|
|
if [ -s "$NVM_DIR/nvm.sh" ]; then
|
|
# shellcheck disable=SC1090
|
|
. "$NVM_DIR/nvm.sh" --no-use
|
|
nvm use --silent node >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
if ! command -v node >/dev/null 2>&1 || ! command -v pnpm >/dev/null 2>&1; then
|
|
echo "❌ node and pnpm must be available before committing"
|
|
echo " Install them or make sure your nvm setup is available to git hooks."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
require_main_branch() {
|
|
if ! git symbolic-ref -q HEAD >/dev/null 2>&1; then
|
|
echo "❌ Commits must happen on main. Detached HEAD is not allowed."
|
|
exit 1
|
|
fi
|
|
|
|
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
if [ "$CURRENT_BRANCH" != "main" ]; then
|
|
echo "❌ Commits must happen on main. Current branch: $CURRENT_BRANCH"
|
|
echo " Merge or cherry-pick your work onto main, then commit there."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
require_main_branch
|
|
ensure_node_tooling
|
|
|
|
echo "🔍 Pre-commit checks..."
|
|
|
|
# Lint + types (only if TS files staged)
|
|
STAGED_TS=$(git diff --cached --name-only | grep -E '\.(ts|tsx)$' || true)
|
|
if [ -n "$STAGED_TS" ]; then
|
|
echo " → lint + tsc..."
|
|
pnpm lint --quiet
|
|
npx tsc --noEmit
|
|
fi
|
|
|
|
# Unit tests
|
|
echo " → tests..."
|
|
pnpm exec vitest run --silent
|
|
|
|
echo "✅ Pre-commit passed"
|
|
|
|
# ── CodeScene Code Health gate ────────────────────────────────────────────
|
|
# Uses the remote project score as an early warning signal.
|
|
# Thresholds are a ratchet — only go up.
|
|
# When the remote baseline is already below threshold, allow recovery commits to
|
|
# land; otherwise the stale remote score would block the refactors needed to
|
|
# restore the gate.
|
|
# Never use eslint-disable, #[allow(...)], or `as any`.
|
|
echo "🏥 CodeScene code health check..."
|
|
THRESHOLDS_FILE=".codescene-thresholds"
|
|
if [ -z "$CODESCENE_PAT" ] || [ -z "$CODESCENE_PROJECT_ID" ]; then
|
|
echo " ⚠️ CODESCENE_PAT or CODESCENE_PROJECT_ID not set — skipping (CI will enforce)"
|
|
elif [ ! -f "$THRESHOLDS_FILE" ]; then
|
|
echo " ⚠️ $THRESHOLDS_FILE not found — skipping (CI will enforce)"
|
|
else
|
|
# Read ratchet thresholds
|
|
HOTSPOT_THRESHOLD=$(grep '^HOTSPOT_THRESHOLD=' "$THRESHOLDS_FILE" | cut -d= -f2)
|
|
AVERAGE_THRESHOLD=$(grep '^AVERAGE_THRESHOLD=' "$THRESHOLDS_FILE" | cut -d= -f2)
|
|
if [ -z "$HOTSPOT_THRESHOLD" ] || [ -z "$AVERAGE_THRESHOLD" ]; then
|
|
echo " ⚠️ Could not parse thresholds from $THRESHOLDS_FILE — 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 CodeScene scores — skipping (CI will enforce)"
|
|
else
|
|
echo " Hotspot Code Health: $HOTSPOT_SCORE (threshold: $HOTSPOT_THRESHOLD)"
|
|
echo " Average Code Health: $AVERAGE_SCORE (threshold: $AVERAGE_THRESHOLD)"
|
|
python3 -c "
|
|
import sys
|
|
hotspot = float('$HOTSPOT_SCORE')
|
|
average = float('$AVERAGE_SCORE')
|
|
h_thresh = float('$HOTSPOT_THRESHOLD')
|
|
a_thresh = float('$AVERAGE_THRESHOLD')
|
|
failed = False
|
|
if hotspot < h_thresh:
|
|
print(f'WARN: Hotspot Code Health {hotspot:.2f} < {h_thresh} — remote baseline is currently red')
|
|
failed = True
|
|
else:
|
|
print(f'OK: Hotspot {hotspot:.2f} >= {h_thresh}')
|
|
if average < a_thresh:
|
|
print(f'WARN: Average Code Health {average:.2f} < {a_thresh} — remote baseline is currently red')
|
|
failed = True
|
|
else:
|
|
print(f'OK: Average {average:.2f} >= {a_thresh}')
|
|
if failed:
|
|
print(' ⚠️ Recovery mode: allowing this commit so refactors can land and restore the gate on a later push.')
|
|
" || exit 1
|
|
fi
|
|
fi
|
|
fi
|