mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
chore(claude): Path-scope rules and align with Opus 5 guidance
This commit is contained in:
+27
-10
@@ -29,18 +29,35 @@ Quick build check: `./gradlew assembleFossDebug`
|
||||
|
||||
- Use `assembleFossDebug` as the fastest build variant for iteration
|
||||
- Follow existing patterns — the codebase uses MVVM + Hilt + Coroutines
|
||||
- Always use string resources for user-facing text (see localization rules)
|
||||
- Always use string resources for user-facing text
|
||||
- Check `git log --oneline -20` for commit message style before committing
|
||||
- Ordinary unit tests use JUnit 5 + kotest assertions + mockk and extend `testhelpers.BaseTest` — not
|
||||
the Android defaults. `testFossDebugUnitTest` does not run `testGplay` tests
|
||||
- Changing a production screen that backs a `@PreviewTest` entry in `PlayStoreScreenshots.kt` means
|
||||
regenerating the smoke screenshot set
|
||||
|
||||
## Rules Reference
|
||||
|
||||
Detailed guidelines are in `.claude/rules/`:
|
||||
Always loaded:
|
||||
|
||||
- `architecture.md` — Module structure, key components, data flow, dependencies
|
||||
- `build-commands.md` — Build, test, lint, and release commands
|
||||
- `localization.md` — String resource naming conventions
|
||||
- `commit-guidelines.md` — Commit message format and prefixes
|
||||
- `pull-requests.md` — PR title and description conventions
|
||||
- `agent-instructions.md` — Sub-agent delegation and critical thinking
|
||||
- `screenshots.md` — Play Store screenshot pipeline, commands, adding new screens
|
||||
- `release.md` — Release workflow (`Release prepare` dispatch), inputs, channel mapping, rollback
|
||||
| Rule | Covers |
|
||||
|------|--------|
|
||||
| `.claude/rules/architecture.md` | BLE vs AAP paths, `DeviceMonitor` merge boundary, FOSS pro gating |
|
||||
| `.claude/rules/build-commands.md` | Gradle commands and what CI actually gates |
|
||||
| `.claude/rules/commit-guidelines.md` | Commit message format and prefixes |
|
||||
| `.claude/rules/pull-requests.md` | PR title and description conventions |
|
||||
| `.claude/rules/agent-instructions.md` | Delegation limits and implementation scope |
|
||||
|
||||
Loaded on demand, when a matching file is read (`paths:` frontmatter):
|
||||
|
||||
| Rule | Loads for |
|
||||
|------|-----------|
|
||||
| `.claude/rules/testing.md` | `app/src/test/`, `testFoss/`, `testGplay/` |
|
||||
| `.claude/rules/localization.md` | `**/res/values/strings.xml` (base locale) |
|
||||
| `.claude/rules/screenshots.md` | Screenshot composables, `screenshotTest/`, fastlane scripts |
|
||||
|
||||
Skills, invoked by name:
|
||||
|
||||
| Skill | Purpose |
|
||||
|-------|---------|
|
||||
| `/release` | Release workflow dispatch, inputs, channel mapping, rollback |
|
||||
|
||||
@@ -1,39 +1,32 @@
|
||||
---
|
||||
description: Instructions for Claude Code sub-agents and task delegation
|
||||
globs:
|
||||
- "**"
|
||||
description: Sub-agent delegation limits and implementation scope for this project
|
||||
---
|
||||
|
||||
# Agent Instructions
|
||||
|
||||
## Critical Thinking
|
||||
## Delegation
|
||||
|
||||
- Do not blindly accept information at face value
|
||||
- Verify assumptions against actual code before proceeding
|
||||
- When encountering unexpected behavior, investigate root causes rather than applying workarounds
|
||||
- If something seems wrong, it probably is — dig deeper
|
||||
Delegation adds coordination overhead and multiplies token cost, so it has to earn its place through
|
||||
genuine independence and parallel speedup.
|
||||
|
||||
## Explore vs. Implement
|
||||
- Delegate only for large, genuinely independent work that parallelizes — a wide multi-file
|
||||
investigation across unrelated areas, for example
|
||||
- Don't delegate what you'd finish yourself in a handful of tool calls
|
||||
- Don't spawn a sub-agent to verify or double-check your own work
|
||||
- If one sub-agent can do it, use one rather than several
|
||||
- Sub-agents don't inherit your conversation — state the full task, the relevant paths, and
|
||||
whether you want research only or research plus implementation
|
||||
- `Explore` is the right type for read-only codebase investigation
|
||||
|
||||
- **Explore first**: Before making changes, understand the existing code structure and patterns
|
||||
- **Read before writing**: Always read relevant files before modifying them
|
||||
- **Follow existing patterns**: Match the code style and architecture already in use
|
||||
- **Minimal changes**: Only change what's necessary to accomplish the task
|
||||
Running Gradle through the build-runner agent is a separate standing rule in the user's global
|
||||
CLAUDE.md; it is context isolation, not delegation, and this file does not restate it.
|
||||
|
||||
## Sub-Agent Delegation
|
||||
## Implementation scope
|
||||
|
||||
When using Task tool to spawn sub-agents:
|
||||
|
||||
- Provide complete context — sub-agents don't share your conversation history unless noted
|
||||
- Be specific about what you need: research only, or research + implementation
|
||||
- Use `Explore` agent type for codebase investigation
|
||||
- Use `Bash` agent type for running builds and tests
|
||||
- Parallelize independent sub-agent tasks for efficiency
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Don't create new files when editing existing ones would suffice
|
||||
- Don't add features beyond what was requested
|
||||
- Don't refactor surrounding code when fixing a bug
|
||||
- Don't add comments or documentation to code you didn't change
|
||||
- Don't guess at file paths — use Glob/Grep to find them
|
||||
- Follow existing patterns — match the code style and architecture already in use
|
||||
- Change only what the task needs
|
||||
- When behavior is unexpected, fix the root cause rather than working around it
|
||||
- Don't create new files when editing an existing one would do
|
||||
- Don't refactor surrounding code while fixing a bug
|
||||
- Don't add comments or docs to code you didn't change
|
||||
- Don't guess at file paths — use Glob/Grep
|
||||
|
||||
@@ -1,123 +1,61 @@
|
||||
---
|
||||
description: Architecture overview, module structure, key components, data flow, and dependencies
|
||||
globs:
|
||||
- "app/**/*.kt"
|
||||
- "**/*.gradle.kts"
|
||||
description: Load-bearing architectural invariants that are not obvious from reading the code
|
||||
---
|
||||
|
||||
# Architecture
|
||||
|
||||
## Single-Module Structure
|
||||
Invariants worth knowing before you touch device state, the AAP stack, or the upgrade flow. Class
|
||||
inventories and source-set layout are omitted deliberately — read the tree for those.
|
||||
|
||||
One Gradle module: `app/`. Source sets:
|
||||
## BLE vs AAP
|
||||
|
||||
- `main` — shared code (Compose UI, services, monitor, bluetooth, AAP protocol, widgets)
|
||||
- `foss` / `gplay` — flavor-specific code (e.g. upgrade/billing implementations)
|
||||
- `debug` — debug-only code including screenshot content composables
|
||||
- `test` / `testFoss` / `testGplay` — unit tests
|
||||
- `screenshotTest` — Compose Preview Screenshot tests for Play Store assets
|
||||
|
||||
A previous `app-common/` module was merged into `app/` (commit `be8f4919`).
|
||||
|
||||
## Core Patterns
|
||||
|
||||
- **MVVM**: ViewModels with LiveData/StateFlow for UI state management
|
||||
- **Dependency Injection**: Hilt/Dagger for dependency management
|
||||
- **Coroutines**: Kotlin coroutines for async operations
|
||||
- **Repository Pattern**: Data layer abstraction for monitoring and settings
|
||||
|
||||
## Key Components
|
||||
|
||||
### Device Monitoring
|
||||
|
||||
`monitor/core/` is split into two data-source siblings that `DeviceMonitor` merges:
|
||||
|
||||
- `monitor/core/ble/BlePodMonitor` — passive BLE scanning; reads Apple advertisement beacons (battery, case state, in-ear, etc.). Works for any pod in range; no pairing required
|
||||
- `monitor/core/aap/` — AAP connection lifecycle layer on top of `AapConnectionManager`:
|
||||
- `AapLifecycleManager` — starts/stops the AAP subsystem
|
||||
- `AapAutoConnect` — auto-opens AAP sessions for bonded/known devices
|
||||
- `AapKeyPersister`, `AapLearnedSettingsPersister` — persist session keys and learned pod settings across app restarts
|
||||
- `StemConfigSender`, `StemPressReaction`, `AncGestureResolver` — push config and react to stem/HID events
|
||||
- `monitor/core/cache/DeviceStateCache` — persisted last-known state so profiles still show data when a device is out of range
|
||||
- `DeviceMonitor` — singleton that `combine`s `BlePodMonitor.devices + AapConnectionManager.allStates + DeviceStateCache + profiles` into unified `PodDevice` objects. ViewModels observe `DeviceMonitor.devices`; they do **not** reach into `BlePodMonitor` or the AAP layer directly
|
||||
- `MonitorControl` / `MonitorService` — foreground service lifecycle holding the scan awake
|
||||
- `BluetoothEventReceiver`, `BootCompletedReceiver` — system triggers that wake the service
|
||||
|
||||
**BLE vs AAP — what each path gives you:**
|
||||
Two independent data paths. Which one a feature can use decides whether it is even possible.
|
||||
|
||||
| | BLE (advertisements) | AAP (L2CAP session) |
|
||||
|---|---|---|
|
||||
| Direction | Read-only, passive | Bidirectional commands + events |
|
||||
| Prerequisite | Bluetooth on | Bonded + `BLUETOOTH_CONNECT` + active L2CAP socket |
|
||||
| Data | Battery, case open, in-ear, pod model | Settings, ANC mode control, press controls, stem events, device info |
|
||||
| Prerequisite | `BLUETOOTH_SCAN` on Android 12+, Bluetooth/location permissions below | Bonded + `BLUETOOTH_CONNECT` + active L2CAP socket |
|
||||
| Data | Battery, case open, in-ear, pod model | Settings, ANC control, press controls, stem events, device info |
|
||||
| Availability | Any pod in range | Only your own paired pods |
|
||||
|
||||
### Reaction System
|
||||
A figure BLE never advertises cannot be obtained without a bonded AAP session, and anything
|
||||
requiring a write is AAP-only.
|
||||
|
||||
- `ReactionsCard`: Compose UI for reaction settings, embedded in the device settings screen
|
||||
- `PopUpWindow`: Displays AirPods status when case is opened
|
||||
- `PopUpContent`: Compose pod rendering — model-specific UI branches inline, no factory class
|
||||
## `DeviceMonitor` is the state merge boundary
|
||||
|
||||
### Widget System (Glance)
|
||||
`DeviceMonitor` (singleton) `combine`s four live sources — `BlePodMonitor.devices`,
|
||||
`AapConnectionManager.allStates`, `BluetoothManager2.connectedDevices` (supplies `isSystemConnected`),
|
||||
and `DeviceProfilesRepo.profiles` — then merges `DeviceStateCache` on top, deliberately after the
|
||||
combine so cache writes don't feed back into it.
|
||||
|
||||
- `BatteryGlanceWidget`, `AncGlanceWidget`: Jetpack Glance-based home-screen widgets
|
||||
- `WidgetConfigurationActivity`: Configuration UI launched on widget placement
|
||||
- Lives under `app/src/main/java/eu/darken/capod/main/ui/widget/`
|
||||
The invariant is about **state**, not about the whole AAP layer:
|
||||
|
||||
### Upgrade / Pro Features
|
||||
- Unified device state comes from `DeviceMonitor.devices` — don't assemble your own from `BlePodMonitor`
|
||||
- Commands go **through** `AapConnectionManager.sendCommand(...)`. ViewModels legitimately inject it
|
||||
(`OverviewViewModel`, `DeviceSettingsViewModel`, `PressControlsViewModel` all do)
|
||||
- Nothing outside the AAP engine touches `AapConnection` (the L2CAP socket wrapper) directly
|
||||
- `TroubleShooterViewModel` reaching into `BlePodMonitor` for raw diagnostic scans is an intentional
|
||||
exception, not a pattern to copy
|
||||
|
||||
- `UpgradeRepo` interface with two flavor implementations:
|
||||
- `UpgradeRepoGplay` — billing-client backed, includes grace-period handling for interrupted purchases
|
||||
- `UpgradeControlFoss` — cache/sponsor-backed; users are `isPro = false` until they call `upgrade()`, after which the pro flag is persisted via DataStore
|
||||
- FOSS is **not** "always pro" — it's opt-in via a local sponsor flow
|
||||
Because the cache is merged in, a `PodDevice` may carry data while the device is out of range —
|
||||
presence in the flow does not imply a live connection.
|
||||
|
||||
### AAP (Apple Accessory Protocol) Stack
|
||||
## `AapConnectionManager` owns sessions
|
||||
|
||||
Three-layer structure under `pods/core/apple/aap/`:
|
||||
It holds every open AAP session keyed by `BluetoothAddress`. Consumers call `sendCommand(...)` and
|
||||
observe `allStates`.
|
||||
|
||||
- **`protocol/`** — pure data: `AapMessage`, `AapCommand`, `AapSetting`, `AapDeviceProfile`, `AapDeviceInfo`, `StemPressEvent`, `KeyExchangeResult`. Plus `DefaultAapDeviceProfile` and `Model.Features` capturing per-model capability
|
||||
- **`engine/`** — session state machine for one connection:
|
||||
- `AapConnection` — the L2CAP socket wrapper
|
||||
- `AapSessionEngine` — drives the session lifecycle; tested in `AapSessionEngineTest`
|
||||
- `AapInboundInterpreter` / `AapOutboundController` — decode incoming messages, encode outgoing
|
||||
- `AapSettingsCoordinator`, `AapAncController`, `HidTracker`, `AapDeviceInfoDiagnostics` — feature-specific coordinators that sit on top of the session
|
||||
- **`AapConnectionManager`** (singleton) — owns all open AAP sessions keyed by `BluetoothAddress`, uses `L2capSocketFactory` to create sockets. Consumers don't touch `AapConnection` directly — they call `sendCommand(...)` and observe `allStates`
|
||||
The stack under `pods/core/apple/aap/` splits into `protocol/` (pure data) and `engine/` (per-connection
|
||||
state machine). The glue in `monitor/core/aap/` wires it into the foreground service and persists
|
||||
learned settings and session keys across restarts.
|
||||
|
||||
The monitor-layer glue (`monitor/core/aap/`) described above wires this stack into the foreground service and persists its learned state.
|
||||
## FOSS is not "always pro"
|
||||
|
||||
### Common Utilities
|
||||
`UpgradeRepo` has two flavor implementations. `UpgradeControlFoss` starts users at `isPro = false`
|
||||
and only persists the pro flag after `upgrade()` is called via the local sponsor flow. Do not assume
|
||||
the FOSS flavor bypasses pro gating.
|
||||
|
||||
- `common/compose/InsetsExtensions.kt`: `PaddingValues.plus` operator and `systemBarsAndCutoutInsets` for laying out non-Scaffold screens edge-to-edge
|
||||
## Navigation is mid-migration
|
||||
|
||||
## Build Configuration
|
||||
|
||||
### Flavors
|
||||
|
||||
- **FOSS**: Open-source version without Google Play dependencies
|
||||
- **Google Play (gplay)**: Version with billing client for in-app purchases
|
||||
|
||||
### Build Types
|
||||
|
||||
- **debug**: Unobfuscated, full logging, no minification
|
||||
- **beta**: Obfuscated, production-ready with strict lint checks
|
||||
- **release**: Fully optimized for production distribution
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. `BluetoothEventReceiver` / `BootCompletedReceiver` wake `MonitorService` (foreground)
|
||||
2. `MonitorService` keeps `BlePodMonitor` scanning (passive advertisements) and `AapLifecycleManager` running (active L2CAP sessions via `AapConnectionManager`)
|
||||
3. `DeviceMonitor` merges BLE + AAP + cached state + profiles into `PodDevice` objects
|
||||
4. ViewModels (`OverviewViewModel`, `DeviceSettingsViewModel`, `PressControlsViewModel`, widget view models) observe `DeviceMonitor.devices`; settings/command changes are sent back through `AapConnectionManager.sendCommand(...)`
|
||||
5. Reaction triggers (case-open popup, auto-play, notifications) and widget state updates react to the merged flow
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- **Unit Tests**: `app/src/test/` (shared), `app/src/testFoss/`, `app/src/testGplay/` (flavor-specific — e.g. `UpgradeRepoGplayTest`, `FossUpgradeSerializationTest`)
|
||||
- **Screenshot Tests**: `app/src/screenshotTest/` — Compose Preview Screenshot Testing, powers the Play Store screenshot pipeline
|
||||
|
||||
## Key Dependencies
|
||||
|
||||
- **Hilt**: Dependency injection framework
|
||||
- **Navigation**: Navigation3 (`addNavigation3()`) drives current Compose screen routing. Some legacy `androidx.navigation` helpers still exist (`NavDirectionsExtensions`, `ViewModel3`) — don't assume SafeArgs is fully gone
|
||||
- **kotlinx.serialization**: JSON serialization for configuration and caching
|
||||
- **Material Design 3**: Compose Material3 UI components
|
||||
Navigation3 (`addNavigation3()`) drives current Compose routing, but legacy `androidx.navigation`
|
||||
helpers still exist (`NavDirectionsExtensions`, `ViewModel3`). Don't assume SafeArgs is fully gone.
|
||||
|
||||
@@ -1,71 +1,55 @@
|
||||
---
|
||||
description: Build, test, lint, and release commands for Gradle
|
||||
globs:
|
||||
- "**/*.gradle.kts"
|
||||
- "**/*.gradle"
|
||||
- "gradle/**"
|
||||
description: Gradle build, test, and lint commands, and what CI actually gates
|
||||
---
|
||||
|
||||
# Build Commands
|
||||
|
||||
## Build
|
||||
## Quick local check
|
||||
|
||||
```bash
|
||||
# Build debug version
|
||||
./gradlew assembleDebug
|
||||
|
||||
# Build all variants (FOSS and Google Play flavors)
|
||||
./gradlew assemble
|
||||
|
||||
# Build specific flavor and type
|
||||
./gradlew assembleFossDebug
|
||||
./gradlew assembleGplayRelease
|
||||
|
||||
# Build app bundles for Play Store
|
||||
./gradlew bundleGplayRelease
|
||||
./gradlew assembleFossDebug testFossDebugUnitTest
|
||||
```
|
||||
|
||||
## Testing
|
||||
`assembleFossDebug` is the fastest variant — use it for iteration.
|
||||
|
||||
## What CI gates
|
||||
|
||||
`.github/workflows/code-checks.yml`, on every PR. Core Gradle gates:
|
||||
|
||||
```bash
|
||||
# Run all unit tests
|
||||
./gradlew test
|
||||
# Lint vitals — flavor x variant matrix. Note: Beta/Release only, never Debug.
|
||||
./gradlew lintVitalFossBeta lintVitalFossRelease lintVitalGplayBeta lintVitalGplayRelease
|
||||
|
||||
# Run unit tests for specific variant
|
||||
./gradlew testFossDebugUnitTest
|
||||
# Builds — Debug only
|
||||
./gradlew app:assembleFossDebug app:assembleGplayDebug
|
||||
|
||||
# Run instrumentation tests (requires connected device/emulator)
|
||||
./gradlew connectedAndroidTest
|
||||
./gradlew connectedFossDebugAndroidTest
|
||||
|
||||
# Run all checks (lint + tests)
|
||||
./gradlew check
|
||||
# Unit tests — both flavors
|
||||
./gradlew testFossDebugUnitTest testGplayDebugUnitTest
|
||||
```
|
||||
|
||||
## Code Quality
|
||||
Four non-Gradle checks also run, **unconditionally** — there is no path filter, so they gate your PR
|
||||
even if you didn't touch those areas:
|
||||
|
||||
```bash
|
||||
# Run lint for all variants
|
||||
./gradlew lint
|
||||
|
||||
# Run lint for specific variant
|
||||
./gradlew lintFossDebug
|
||||
|
||||
# Auto-fix lint issues where possible
|
||||
./gradlew lintFix
|
||||
|
||||
# Update lint baseline
|
||||
./gradlew updateLintBaseline
|
||||
bash fastlane/check_metadata_length.sh # Play Store metadata length limits
|
||||
shellcheck tools/release/bump.sh
|
||||
bats tools/release/bump.bats
|
||||
./tools/release/bump.sh --mode=check # version.properties + VERSION consistency
|
||||
```
|
||||
|
||||
## Release
|
||||
Reproducing those locally is usually only worth it when you changed fastlane metadata or release
|
||||
tooling, but a failure there blocks the PR regardless.
|
||||
|
||||
**Do not run `./gradlew check` as a pre-submit gate.** It runs the full non-vital `lint` task, which
|
||||
is already failing on `main` for reasons unrelated to your change — you'll burn time chasing
|
||||
pre-existing findings that CI never looks at. CI gates `lintVital*`, not `lint`.
|
||||
|
||||
## Other commands
|
||||
|
||||
```bash
|
||||
./gradlew assembleFossRelease assembleGplayRelease
|
||||
./gradlew assembleGplayRelease # release build
|
||||
./gradlew bundleGplayRelease # Play Store bundle
|
||||
./gradlew connectedFossDebugAndroidTest # instrumentation, needs a device/emulator
|
||||
./gradlew lintFix # auto-fix where possible
|
||||
./gradlew updateLintBaseline # refresh the baseline
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Use `assembleFossDebug` as the default quick-check build (fastest variant)
|
||||
- Run `./gradlew check` before submitting changes to catch lint and test issues
|
||||
- Instrumentation tests require a connected device or running emulator
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
---
|
||||
description: Git commit message format and conventions
|
||||
globs:
|
||||
- "**"
|
||||
---
|
||||
|
||||
# Commit Guidelines
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
description: Guidelines for adding and naming Android string resources
|
||||
globs:
|
||||
- "**/res/values*/strings.xml"
|
||||
paths:
|
||||
- "**/res/values/strings.xml"
|
||||
---
|
||||
|
||||
# Localization Guidelines
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
---
|
||||
description: Pull request naming and description conventions
|
||||
globs:
|
||||
- "**"
|
||||
description: Pull request title and description conventions
|
||||
---
|
||||
|
||||
# Pull Request Guidelines
|
||||
|
||||
## PR Title Format
|
||||
## Title
|
||||
|
||||
```
|
||||
<Category>: <Short user-facing summary>
|
||||
```
|
||||
|
||||
PR titles appear in auto-generated changelogs and are read by users. Use **ELI5, user-facing language** — no internal class names, library names, or implementation details.
|
||||
Titles appear in auto-generated changelogs and are read by users. Use ELI5, user-facing language —
|
||||
no class names, library names, or implementation details. `refactor(settings): Migrate preferences
|
||||
to DataStore` is the shape to avoid; `General: Remember settings between app restarts` is the shape
|
||||
to use.
|
||||
|
||||
## Category Prefixes
|
||||
|
||||
| Prefix | Covers |
|
||||
| Category | Covers |
|
||||
|--------|--------|
|
||||
| **Widget** | Home screen widget |
|
||||
| **Reaction** | Case-open popup, auto-play/pause, notification triggers |
|
||||
@@ -24,76 +23,32 @@ PR titles appear in auto-generated changelogs and are read by users. Use **ELI5,
|
||||
| **General** | Dashboard, settings, notifications, themes, onboarding, support, app-wide UI |
|
||||
| **Fix** | Bug fixes spanning multiple areas |
|
||||
|
||||
### Title Examples
|
||||
## Description
|
||||
|
||||
- `Widget: Add color themes and transparency slider`
|
||||
- `Reaction: Fix popup appearing twice when opening AirPods case`
|
||||
- `Device: Add support for AirPods 4 with ANC`
|
||||
- `General: Add dark mode and color theme settings`
|
||||
- `Fix: Fix battery display stuck at 0% after reconnecting`
|
||||
|
||||
### Bad Titles (too technical)
|
||||
|
||||
- `refactor(settings): Migrate preferences to AndroidX DataStore`
|
||||
- `feat(widget): Migrate to Jetpack Glance`
|
||||
- `refactor(ui): Migrate from Fragments to Jetpack Compose`
|
||||
|
||||
## PR Description Format
|
||||
|
||||
PRs are reviewed in **GitHub's web UI**, which already shows the file tree, the diff, and the tests. Don't duplicate any
|
||||
of it. The description should answer questions the diff can't — not restate it.
|
||||
|
||||
Only these sections, in this order:
|
||||
PRs are reviewed in GitHub's web UI, which already shows the file tree, the diff, and the tests.
|
||||
The description answers what the diff can't. Use exactly these sections, in this order:
|
||||
|
||||
1. `## What changed`
|
||||
2. `## Technical Context`
|
||||
3. `## Review checklist` *(optional)*
|
||||
|
||||
No `Scope`, `Files changed`, `Tests`, or `Review guidance` sub-sections — GitHub shows the files and tests, and review
|
||||
notes belong in the checklist. Fold anything critical into a Technical Context bullet.
|
||||
No `Scope`, `Files changed`, `Tests`, or `Review guidance` sections.
|
||||
|
||||
### What changed
|
||||
**What changed** — user-facing explanation: the problem fixed or the feature added, from the user's
|
||||
perspective. For refactors, tests, CI, and dependency bumps, write "No user-facing behavior change"
|
||||
followed by a brief internal description.
|
||||
|
||||
User-friendly explanation of what this PR does. Describe the problem that was fixed or the feature that was added from the user's perspective. No internal class or method names.
|
||||
**Technical Context** — one bullet per point, no prose paragraphs, no nested `**Bug 1**` headers.
|
||||
Cover only what the diff can't show:
|
||||
- **Why** this approach, and what was rejected
|
||||
- **Root cause** for bug fixes — the diff shows the fix, not what caused it
|
||||
- **Non-obvious side effects** or behavioral changes
|
||||
|
||||
For non-user-facing PRs (refactors, tests, CI, dependency bumps): write "No user-facing behavior change" followed by a brief internal description.
|
||||
|
||||
### Technical Context
|
||||
|
||||
Explain what's hard to extract from the diff alone. Focus on:
|
||||
|
||||
- **Why** this approach was chosen (and alternatives considered/rejected)
|
||||
- **Root cause** for bug fixes (the diff shows the fix, not what caused it)
|
||||
- **Non-obvious side effects** or behavioral changes not apparent from reading the code
|
||||
|
||||
Format rules:
|
||||
|
||||
- **One bullet per point.** No prose paragraphs, no nested sub-headers like `**Bug 1** / **Bug 2**` — if a PR fixes
|
||||
multiple bugs, one bullet per bug is enough.
|
||||
- **Don't restate the diff.** File paths, class renames, test names, and line-level changes are all visible in the web
|
||||
UI.
|
||||
|
||||
### Review checklist (optional)
|
||||
|
||||
For PRs with multiple non-trivial review points, add a `## Review checklist` section with `- [ ]` tasks the reviewer can
|
||||
tick off as they verify. Skip it for small PRs — a single tricky thing can stay as a Technical Context bullet.
|
||||
|
||||
### Example
|
||||
|
||||
```markdown
|
||||
## What changed
|
||||
|
||||
Fixed a crash that could happen when the AirPods case is opened while Bluetooth is turning off.
|
||||
|
||||
## Technical Context
|
||||
|
||||
- Root cause: `MonitorService` continued processing scan results during Bluetooth adapter state change, hitting a null adapter reference
|
||||
- Chose to gate on adapter state in the scan callback rather than adding a separate BroadcastReceiver, since the service already observes adapter state for restart logic
|
||||
- The timing window is ~200ms between ACTION_STATE_CHANGING and ACTION_STATE_OFF — only reproducible on Pixel devices with aggressive Bluetooth power management
|
||||
```
|
||||
**Review checklist** — `- [ ]` items, only when there are several non-trivial things to verify.
|
||||
A single tricky point stays a Technical Context bullet.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Issue references**: Use "Closes #123", "Fixes #123", or "Resolves #123"
|
||||
- **Breaking changes**: Mark with "BREAKING:" prefix if applicable
|
||||
- **No `Co-authored-by` trailers** (per project convention)
|
||||
- Link issues with "Closes #123" / "Fixes #123" / "Resolves #123"
|
||||
- Prefix breaking changes with "BREAKING:"
|
||||
- No `Co-authored-by` trailers
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
---
|
||||
description: Play Store screenshot pipeline — generation, copying, and adding or removing screens
|
||||
paths:
|
||||
- "app/src/debug/**/screenshots/**"
|
||||
- "app/src/screenshotTest/**"
|
||||
- "fastlane/generate_screenshots.sh"
|
||||
- "fastlane/copy_screenshots.sh"
|
||||
- "fastlane/Fastfile"
|
||||
- "fastlane/metadata/android/*/images/phoneScreenshots/**"
|
||||
---
|
||||
|
||||
# Play Store Screenshot Pipeline
|
||||
|
||||
## Overview
|
||||
@@ -19,7 +30,7 @@ ScreenshotContent.kt (mock data + composables)
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `app/src/debug/java/.../screenshots/ScreenshotContent.kt` | Mock data composables for each screen (7 screens) |
|
||||
| `app/src/debug/java/.../screenshots/ScreenshotContent.kt` | Mock data composables (7 exist; `HomescreenWidgetContent` has an IDE preview only and is **not** in the Play Store pipeline) |
|
||||
| `app/src/screenshotTest/kotlin/.../screenshots/PlayStoreScreenshots.kt` | `@PreviewTest` functions (currently: `DashboardLight`, `DashboardDark`, `CasePopUp`, `DeviceProfiles`, `AddProfile`, `DeviceSettingsReactions`, `WidgetConfiguration`) |
|
||||
| `app/src/screenshotTest/kotlin/.../screenshots/PlayStoreLocales.kt` | Multi-preview annotations (auto-generated by batch script) |
|
||||
| `fastlane/generate_screenshots.sh` | Batched generation; locale list (`ALL_LOCALES`) and `BATCH_SIZE` are defined inside the script |
|
||||
@@ -77,7 +88,7 @@ When modifying a screen that appears in screenshots (check `ScreenshotContent.kt
|
||||
Periodic, manual operation — not per-PR:
|
||||
|
||||
```bash
|
||||
./fastlane/generate_screenshots.sh # full, ~30 min, 477 PNGs
|
||||
./fastlane/generate_screenshots.sh # full, ~30 min, 476 PNGs (68 locales x 7)
|
||||
./fastlane/copy_screenshots.sh --clean
|
||||
bundle exec fastlane screenshots_only # uploads all 68 locales to Play Store
|
||||
git checkout -- fastlane/metadata/android/ # discard non-smoke changes (gitignored anyway)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
description: Unit test conventions — JUnit 5, kotest assertions, mockk, BaseTest, and which Gradle task runs which source set
|
||||
paths:
|
||||
- "app/src/test/**"
|
||||
- "app/src/testFoss/**"
|
||||
- "app/src/testGplay/**"
|
||||
- "app/build.gradle.kts"
|
||||
- "buildSrc/src/main/java/Dependencies.kt"
|
||||
---
|
||||
|
||||
# Testing
|
||||
|
||||
The stack here is not the Android default — check this before reaching for a familiar library.
|
||||
|
||||
## Libraries
|
||||
|
||||
- **JUnit 5** (`org.junit.jupiter.api.Test`). Gradle sets `useJUnitPlatform()`.
|
||||
- **kotest** for assertions: `io.kotest.matchers.shouldBe`, `shouldBeNull`, `shouldBeInstanceOf`,
|
||||
`shouldContainExactly`, `io.kotest.assertions.throwables.shouldThrow`. Use kotest for new
|
||||
assertions — `MediaControlTest` still uses JUnit `Assertions.*` and is a legacy exception.
|
||||
- **mockk** for mocking. Not Mockito.
|
||||
- **Turbine is not a dependency.** `testhelpers.flow.FlowTest` provides a `Flow<T>.test()` helper —
|
||||
use it rather than adding one.
|
||||
|
||||
## Base classes
|
||||
|
||||
Extend `testhelpers.BaseTest`, or the applicable specialized base that already extends it:
|
||||
|
||||
- `BaseBlePodsTest` — BLE advertisement parsing per pod model
|
||||
- `BaseAapSessionTest` — AAP protocol/session tests
|
||||
|
||||
`BaseTest` installs a `JUnitLogger` and calls `unmockkAll()` in `@AfterAll`. Skipping it can leave
|
||||
global mockk and logging state behind for later test classes.
|
||||
|
||||
The only exceptions are the two Robolectric-backed Compose UI tests
|
||||
(`UpgradeScreenFossComposeTest`, `UpgradeScreenComposeTest`), which use JUnit 4 `@RunWith`/`@Rule`
|
||||
via `junit-vintage-engine`. Don't copy that pattern for a plain unit test.
|
||||
|
||||
## Source sets and Gradle tasks
|
||||
|
||||
Each task compiles and runs only its own flavor — running the wrong one silently skips your test.
|
||||
|
||||
| Test location | Task |
|
||||
|---|---|
|
||||
| `app/src/test/` (shared) | either; run both before pushing |
|
||||
| `app/src/testFoss/` | `./gradlew testFossDebugUnitTest` |
|
||||
| `app/src/testGplay/` | `./gradlew testGplayDebugUnitTest` |
|
||||
|
||||
CI runs both. Flavor-specific tests are for code that only exists in that flavor — billing in
|
||||
`gplay`, the sponsor-based upgrade flow in `foss`.
|
||||
@@ -1,17 +1,51 @@
|
||||
---
|
||||
description: Cut a capod release via the "Release prepare" workflow — dispatch inputs, channel mapping, rollback, and auth setup.
|
||||
disable-model-invocation: true
|
||||
argument-hint: "[bump_kind] [version_type|version_override]"
|
||||
---
|
||||
|
||||
# Release Process
|
||||
|
||||
Releases are cut via the **Release prepare** workflow (`.github/workflows/release-prepare.yml`). It bumps `version.properties` and `VERSION`, commits to `main`, tags `v<version>`, pushes atomically, and dispatches `release-tag.yml` which builds, signs, and uploads.
|
||||
|
||||
## Required order
|
||||
|
||||
A real cut pushes a commit and a tag to `main` and is public the moment it lands. Do not skip ahead.
|
||||
|
||||
1. Run the dry run first and read its summary — never dispatch `dry_run=false` blind.
|
||||
2. Report the planned version and `versionCode` back to the user.
|
||||
3. Get explicit confirmation for that specific version before dispatching `dry_run=false`.
|
||||
4. If the user named `bump_kind`/`version_type`/`version_override`, use exactly those. If the request
|
||||
is ambiguous about which field moves, ask rather than assuming `build`.
|
||||
|
||||
## Dispatch
|
||||
|
||||
`gh workflow run` only fires the dispatch — it returns nothing about the result. The summary is
|
||||
written asynchronously, so you have to go fetch it.
|
||||
|
||||
```bash
|
||||
# Plan only — no commit, no tag, no push.
|
||||
# Step 1 — plan only. No commit, no tag, no push. Always run this first.
|
||||
gh workflow run release-prepare.yml -f bump_kind=build -f dry_run=true
|
||||
|
||||
# Real cut.
|
||||
# Step 2 — find the run just dispatched and wait for it.
|
||||
gh run list --workflow=release-prepare.yml --limit 1 # note the run id
|
||||
gh run watch <run-id> --exit-status
|
||||
|
||||
# Step 3 — read the computed plan (version + versionCode) before going further.
|
||||
gh run view <run-id> --log | tail -40
|
||||
```
|
||||
|
||||
Report the planned version and `versionCode`, get explicit confirmation, then:
|
||||
|
||||
```bash
|
||||
# Step 4 — real cut. Repeat the dry run's inputs EXACTLY; change only dry_run.
|
||||
gh workflow run release-prepare.yml -f bump_kind=build -f dry_run=false
|
||||
```
|
||||
|
||||
The `bump_kind=build` above is only an example. If the confirmed plan came from a `patch`/`minor`/
|
||||
`major` bump, a `version_type` switch, or a `version_override`, Step 4 must carry those same flags —
|
||||
otherwise you cut a different version than the one that was approved.
|
||||
|
||||
After `dry_run=false`: Job 1 computes + writes the summary, then Job 2 immediately commits/tags/pushes (no env gate — cancel the run between Job 1 and Job 2 if the summary looks wrong; you have ~seconds). The tag push naturally triggers `release-tag.yml` (the App-token push fires `on: push:` workflows; only `GITHUB_TOKEN`-pushes are suppressed). `release-tag.yml` then runs `validate-tag` and the existing `release-github` (`foss-production` approval) + `release-gplay` (`gplay-production` approval) jobs — those are the two human checkpoints, matching the pre-migration UX.
|
||||
|
||||
## Inputs
|
||||
@@ -39,7 +73,10 @@ bats tools/release/bump.bats
|
||||
| Tag suffix | FOSS APK | GitHub release | Fastlane lane | Play track | Rollout |
|
||||
|---|---|---|---|---|---|
|
||||
| `-beta*` | `assembleFossBeta` | pre-release | `beta` | `beta` | 10% |
|
||||
| `-rc*` (or anything else) | `assembleFossRelease` | full release | `production` | **`beta`** | 10% |
|
||||
| `-rc*` | `assembleFossRelease` | full release | `production` | **`beta`** | 10% |
|
||||
|
||||
`release-tag.yml` accepts only `v<M.m.p>-rcN` or `v<M.m.p>-betaN` — any other suffix fails
|
||||
`validate-tag` before a build starts. There is no third channel.
|
||||
|
||||
`lane :production` in `Fastfile` uploads to Play's **beta** track at 10% — manually promoted to production via Play Console.
|
||||
|
||||
Reference in New Issue
Block a user