chore: add CLAUDE.md, SPDX-header hook, and preflight skill
- CLAUDE.md: build gotchas (JDK 17-21, AGP built-in Kotlin, KSP-not-KAPT), test stack, the SPDX header rule, Conventional Commits, and commands. - .claude/settings.json + hooks/check-spdx.py: PostToolUse hook that warns (non-blocking) when a .kt/.kts file lacks the SPDX license header. - .claude/skills/preflight: runs the fast CI gate (assembleDebug + testDebugUnitTest + lintDebug) locally. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""PostToolUse (Write|Edit) hook: warn when a Kotlin source file is missing the
|
||||
SPDX license header that every LibreMail source file must carry.
|
||||
|
||||
Non-blocking: prints a JSON warning (systemMessage + additionalContext so Claude
|
||||
adds the header) and always exits 0. Any error is swallowed so the tool flow is
|
||||
never broken by this check.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
REQUIRED = "SPDX-License-Identifier"
|
||||
HEADER_LINE = "// SPDX-License-Identifier: GPL-3.0-or-later"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
path = (data.get("tool_response") or {}).get("filePath") \
|
||||
or (data.get("tool_input") or {}).get("file_path")
|
||||
if not path:
|
||||
return 0
|
||||
|
||||
lower = path.lower()
|
||||
if not (lower.endswith(".kt") or lower.endswith(".kts")):
|
||||
return 0
|
||||
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
||||
content = fh.read()
|
||||
except OSError:
|
||||
# File missing (e.g. a delete) or unreadable — nothing to check.
|
||||
return 0
|
||||
|
||||
if REQUIRED in content:
|
||||
return 0
|
||||
|
||||
msg = f"SPDX header missing in {path}"
|
||||
out = {
|
||||
"systemMessage": msg,
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PostToolUse",
|
||||
"additionalContext": (
|
||||
f'{path} is missing the license header. Add "{HEADER_LINE}" as the '
|
||||
"first line — every LibreMail source file carries it."
|
||||
),
|
||||
},
|
||||
}
|
||||
print(json.dumps(out))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python .claude/hooks/check-spdx.py",
|
||||
"timeout": 30,
|
||||
"statusMessage": "Checking SPDX header"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: preflight
|
||||
description: Run LibreMail's fast CI gate locally (assembleDebug + testDebugUnitTest + lintDebug) before pushing or opening a PR. Mirrors the merge gate; does NOT run emulator E2E. Use before treating a change as done.
|
||||
---
|
||||
|
||||
# /preflight
|
||||
|
||||
Run the same fast checks CI enforces on every PR, in order, and report the outcome.
|
||||
|
||||
## Preconditions
|
||||
|
||||
- The Gradle daemon must run on **JDK 17–21**. AGP 9.2 fails on JDK 25+. If a build errors
|
||||
with a JDK/AGP version mismatch, check `java -version` / `JAVA_HOME` and point it at a 17–21
|
||||
JDK (e.g. Android Studio's bundled JBR) before retrying.
|
||||
- PowerShell: invoke the wrapper as `.\gradlew`. Git Bash / the Bash tool: `./gradlew`.
|
||||
|
||||
## Steps
|
||||
|
||||
Run these three, stopping at the first failure:
|
||||
|
||||
```bash
|
||||
./gradlew :app:assembleDebug
|
||||
./gradlew :app:testDebugUnitTest
|
||||
./gradlew :app:lintDebug
|
||||
```
|
||||
|
||||
To keep going and collect every failure in one pass, add `--continue`.
|
||||
|
||||
## Reporting
|
||||
|
||||
- If all three pass, say so plainly (e.g. "preflight green: build, unit tests, lint").
|
||||
- On failure, surface the actual Gradle error. For test failures, point at the report under
|
||||
`app/build/reports/tests/testDebugUnitTest/`; for lint, `app/build/reports/lint-results-debug.html`.
|
||||
- Do **not** run emulator/E2E (`connectedDebugAndroidTest`) here — that's CI's job unless the
|
||||
user explicitly asks.
|
||||
@@ -41,3 +41,6 @@ captures/
|
||||
|
||||
# Kotlin
|
||||
.kotlin/
|
||||
|
||||
# Claude Code — personal settings (the shared settings.json IS committed)
|
||||
.claude/settings.local.json
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
LibreMail is a GPL-3.0 Android email client (Kotlin, Jetpack Compose, Material 3).
|
||||
See `@README.md` for OAuth/account setup (Gmail, Outlook), the tech stack, and the
|
||||
offline-first architecture — this file only covers what isn't obvious from the code.
|
||||
|
||||
## Build, test, lint
|
||||
|
||||
Use a **JDK 17–21** for the Gradle daemon. AGP 9.2 does **not** support JDK 25 — if
|
||||
`JAVA_HOME` points at 25+, builds fail. Commands (PowerShell: use `.\gradlew`):
|
||||
|
||||
```bash
|
||||
./gradlew :app:assembleDebug # build debug APK
|
||||
./gradlew :app:testDebugUnitTest # JVM unit tests
|
||||
./gradlew :app:lintDebug # Android lint
|
||||
# single unit test:
|
||||
./gradlew :app:testDebugUnitTest --tests "org.libremail.data.SomeClassTest"
|
||||
```
|
||||
|
||||
E2E/instrumented tests need a booted emulator: `./gradlew :app:connectedDebugAndroidTest`,
|
||||
or via Gradle Managed Devices `./gradlew e2eGroupDebugAndroidTest` (whole matrix) /
|
||||
`./gradlew api29DebugAndroidTest` (one API level). The managed-device list in
|
||||
`app/build.gradle.kts` must stay in lockstep with the E2E matrix in `.github/workflows/ci.yml`.
|
||||
|
||||
**Before treating a change as done**, run the fast CI gate: `assembleDebug` +
|
||||
`testDebugUnitTest` + `lintDebug` (the `/preflight` skill does this). Emulator E2E is
|
||||
left to CI unless asked.
|
||||
|
||||
## Build-config gotchas
|
||||
|
||||
- **Built-in Kotlin (AGP 9.x).** Kotlin compilation is handled by AGP's built-in Kotlin;
|
||||
the Kotlin version (2.4.0) is pinned via the root `build.gradle.kts` buildscript classpath.
|
||||
**Never apply the `org.jetbrains.kotlin.android` plugin** — it throws a ClassCastException
|
||||
against AGP 9's DSL. (The `kotlin-android` alias in `libs.versions.toml` exists but must
|
||||
not be used.) `libs.versions.toml` still supplies all *library* versions.
|
||||
- **KSP, not KAPT** for all annotation processing (Hilt, Room).
|
||||
- Room schemas are exported to `app/schemas` and validated by migration tests — commit
|
||||
schema changes.
|
||||
- OAuth client IDs come from `secrets.properties` (git-ignored) via `BuildConfig`; the
|
||||
build works without it (empty/placeholder values).
|
||||
|
||||
## Code conventions
|
||||
|
||||
- Sources live under `app/src/{main,test,androidTest}/kotlin/`; package root `org.libremail`
|
||||
(applicationId `org.libremail.app`).
|
||||
- **Every source file starts with** `// SPDX-License-Identifier: GPL-3.0-or-later` (or the
|
||||
`<!-- ... -->` form for XML/Markdown). All 117 current `.kt` files follow this.
|
||||
- `kotlin.code.style=official`.
|
||||
|
||||
## Testing
|
||||
|
||||
JVM unit tests use JUnit4 + `kotlin.test`, **Turbine** for `Flow`, **MockK** for mocks,
|
||||
**GreenMail** for a real in-process IMAP/SMTP server, and coroutines-test. `org.json` is
|
||||
pulled in as a real dependency for unit tests because `android.jar`'s version is a no-op stub.
|
||||
|
||||
## Repo etiquette
|
||||
|
||||
- Branch off `main`; branch names like `feat-…` / `fix-…`. PRs target `main` and must pass
|
||||
the `CI passed` gate.
|
||||
- **Conventional Commits** for commit subjects and PR titles: `type(scope): summary`
|
||||
(`feat`, `fix`, `chore`, …), matching existing history.
|
||||
Reference in New Issue
Block a user