summaryrefslogtreecommitdiff
path: root/.githooks
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-09-21 08:13:49 +0000
committerClaude <noreply@anthropic.com>2026-09-21 08:13:49 +0000
commit5876025559ba0ad287d3a7a4d9fa0246509e668e (patch)
treec918d01e2b74d205ee00fe96926232475840ff97 /.githooks
Scaffold Listen, with a podcast-native feed parser
Sibling to FeeDreader, not a shared module. That was a considered call: its FeedParser only inspects <enclosure> while hunting for a thumbnail, and its FilterRuleEngine is 46 lines of title substring matching that doesn't generalise to queue rules, so only Opml.kt would genuinely have transferred. The parser is the piece everything else sits on, so it goes first, test-first: 18 tests covering audio/video enclosure selection, iTunes durations in both second and clock form, season/episode numbers, podcast:chapters for the skip feature, RFC 5005 archive paging for drip subscriptions, and episode identity falling back to the enclosure URL when a guid is missing. Two things worth recording: - Built on XmlPullParserFactory rather than android.util.Xml. The latter returns null off-device, which is why FeeDreader's 297-line parser has no tests at all. Both resolve to the same KXmlParser on Android. - SimpleDateFormat with isLenient=false rejects "Tue, 02 Sep 2026 06:00:00 GMT" -- an entirely ordinary RFC 822 date. Left strict, every pubDate in the app would have been null and episode ordering would have been silently wrong everywhere. minSdk is 29, deliberately not FeeDreader's 36, which would restrict the app to Android 16 and newer. Permissions: INTERNET and ACCESS_NETWORK_STATE for feeds; FOREGROUND_SERVICE, FOREGROUND_SERVICE_MEDIA_PLAYBACK, POST_NOTIFICATIONS and WAKE_LOCK because Android requires a foreground service with a notification to play audio in the background. That notification is the transport control, not an alert -- Listen posts no new-episode notifications by design. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
Diffstat (limited to '.githooks')
-rwxr-xr-x.githooks/pre-commit94
1 files changed, 94 insertions, 0 deletions
diff --git a/.githooks/pre-commit b/.githooks/pre-commit
new file mode 100755
index 0000000..32b492a
--- /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/jacocoTestReport/jacocoTestReport.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."