diff options
Diffstat (limited to '.githooks/pre-commit')
| -rwxr-xr-x | .githooks/pre-commit | 94 |
1 files changed, 94 insertions, 0 deletions
diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..389280a --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Quality gate -- see CLAUDE.md "Quality gate -- every commit" for what each step is and why. +# Activate once per clone with: git config core.hooksPath .githooks +set -euo pipefail +cd "$(git rev-parse --show-toplevel)" + +fail() { + echo "pre-commit: $1" >&2 + exit 1 +} + +echo "== [1/8] Build ==" +./gradlew :app:compileDebugKotlin -q + +echo "== [2/8] Tests ==" +./gradlew :app:testDebugUnitTest -q + +echo "== [3/8] Static analysis: lint ==" +if ! ./gradlew :app:lintDebug -q; then + fail "lint failed -- fix all lint ERRORS before committing (warnings are fine to accumulate, see CLAUDE.md)." +fi + +echo "== [4/8] Static analysis: detekt (code-quality auditor) ==" +if ! ./gradlew :app:detekt -q; then + fail "detekt found new issues beyond the tracked baseline. Fix them, or if they're genuinely pre-existing debt being touched incidentally, regenerate the baseline with './gradlew :app:detektBaseline' and explain why in the commit message." +fi + +echo "== [5/8] Coverage must not decrease ==" +./gradlew :app:jacocoTestReport -q +REPORT="app/build/reports/jacoco/report.xml" +BASELINE_FILE=".coverage-baseline" +if [ -f "$REPORT" ]; then + CURRENT=$(python3 - "$REPORT" <<'PY' +import sys, xml.etree.ElementTree as ET +root = ET.parse(sys.argv[1]).getroot() +line = next(c for c in root.findall('counter') if c.get('type') == 'LINE') +missed, covered = int(line.get('missed')), int(line.get('covered')) +total = missed + covered +print(f"{(covered / total * 100) if total else 0.0:.2f}") +PY +) +else + CURRENT="0.00" +fi +BASELINE=$(cat "$BASELINE_FILE" 2>/dev/null || echo "0.00") + +DROPPED=$(python3 -c "print(1 if float('$CURRENT') < float('$BASELINE') else 0)") +if [ "$DROPPED" = "1" ]; then + if [ -n "${COVERAGE_EXCEPTION:-}" ]; then + echo "pre-commit: coverage dropped ${BASELINE}% -> ${CURRENT}% -- allowed via COVERAGE_EXCEPTION: ${COVERAGE_EXCEPTION}" + echo "$CURRENT" > "$BASELINE_FILE" + git add "$BASELINE_FILE" + else + fail "coverage dropped ${BASELINE}% -> ${CURRENT}%. If this drop is the correct outcome (e.g. deleting dead covered code), commit with COVERAGE_EXCEPTION=\"reason\" git commit ... Otherwise, add tests." + fi +else + echo "$CURRENT" > "$BASELINE_FILE" + git add "$BASELINE_FILE" + echo "pre-commit: coverage ${BASELINE}% -> ${CURRENT}%" +fi + +echo "== [6/8] WebView JavaScript justification ==" +STAGED_KT=$(git diff --cached --name-only --diff-filter=ACM -- '*.kt' || true) +if [ -n "$STAGED_KT" ]; then + for f in $STAGED_KT; do + [ -f "$f" ] || continue + ADDED_JS_LINES=$(git diff --cached -U0 -- "$f" | grep -E '^\+.*(javaScriptEnabled\s*=\s*true|setJavaScriptEnabled\(true\))' || true) + if [ -n "$ADDED_JS_LINES" ]; then + if ! grep -q '@SuppressLint("SetJavaScriptEnabled")' "$f"; then + fail "$f enables JavaScript on a WebView with no @SuppressLint(\"SetJavaScriptEnabled\") + justification comment nearby (see ReaderExtractor.kt, LinkReaderScreen.kt, BrowserScreen.kt for the pattern). This app's security posture depends on every JS-enabled site being deliberate and explained." + fi + fi + done +fi + +echo "== [7/8] Manifest permission diff ==" +if git diff --cached --name-only | grep -q '^app/src/main/AndroidManifest.xml$'; then + if git diff --cached -- app/src/main/AndroidManifest.xml | grep -qE '^\+.*<uses-permission'; then + echo "pre-commit: WARNING -- AndroidManifest.xml adds a new <uses-permission>. Make sure the commit message says why." + fi +fi + +echo "== [8/8] Secret scan ==" +if [ -z "${SECRET_SCAN_OVERRIDE:-}" ]; then + SECRET_HIT=$(git diff --cached -U0 -- . ':!*.lock' | grep -E '^\+' | grep -viE '^\+\+\+' | grep -E '(api[_-]?key|secret|password|token)["'\'' ]*[:=]["'\'' ]*[A-Za-z0-9_\-]{16,}' || true) + if [ -n "$SECRET_HIT" ]; then + echo "$SECRET_HIT" >&2 + fail "possible hardcoded secret in the staged diff (see above). If this is a false positive (e.g. an obviously-fake test fixture key), commit with SECRET_SCAN_OVERRIDE=1 git commit ..." + fi +else + echo "pre-commit: secret scan skipped (SECRET_SCAN_OVERRIDE set)" +fi + +echo "pre-commit: all checks passed." |
