| name | pre-release-validation |
| description | Run local CI/CD checks before pushing code. Use before commits, releases, or when validating code quality. Catches issues before they reach remote. |
Pre-Release Validation
Run comprehensive local checks before pushing code to catch issues early.
When to Use This Skill
Automatically invoke:
- Before creating a release (stable or beta)
- Before pushing significant changes
- After completing a feature implementation
Manual invocation when:
- User asks to "run checks" or "validate"
- Debugging CI failures locally
- Auditing code quality
Validation Tiers
Timing reality: a cold :app:compileDebugKotlin runs ~12 min with the Compose
stability config (per the comment at app/build.gradle.kts:140; >42 min without
it); a warm compile is ~4.5 min. Tier estimates assume a warm daemon and stretch
accordingly when cold. Build in the background and wait for completion; do not
kill slow builds mid-flight.
Tier 1: Quick Check (warm ~5 min)
Fast validation for small changes.
./gradlew lintDebug 2>&1 | head -50
./gradlew compileDebugKotlin 2>&1
Tier 2: Standard Check (warm ~10 min)
Full validation before commits.
./gradlew lintDebug
./gradlew testDebugUnitTest
./gradlew compileReleaseKotlin
Tier 3: Full Check (warm 15+ min; much longer cold)
Complete validation before releases.
./gradlew lint
./gradlew testDebugUnitTest testReleaseUnitTest
./gradlew assembleRelease
Coupling Sweep (release backstop)
The git-commit hook (scripts/ci/coupling-guard.py, via PreToolUse) gates each
agent commit against Argosy's coupling map. This sweep is the release-time backstop: it
re-runs the same rules over the WHOLE changeset since the last tag, catching coupling gaps
that slipped across many commits.
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
python3 scripts/ci/coupling-guard.py --sweep "${LAST_TAG:+$LAST_TAG..}HEAD"
The sweep is advisory (never blocks the release on its own), but its output is a release
blocker for judgment:
- WALKTHROUGH-FORCED axes (save-sync, RomM) flagged -> confirm the live-data proof
obligations were actually met for this release (
GET /api/saves, negotiate returns no_op
twice). Never ship these on code-reading alone.
- platform / settings / UI flags -> confirm the lockstep set and consumption sites.
Rules live in scripts/ci/coupling-hotspots.json (machine mirror of
coupling-map.md). Update both when the architecture moves. The guard and its rules are
tracked, not maintainer-local: CI runs the same sweep on every PR, so a rule added here
takes effect for every contributor rather than only on this machine.
Validation Checklist
Code Quality
[ ] No lint errors (warnings acceptable)
[ ] No compiler warnings in changed files
[ ] Unit tests pass
[ ] No TODO/FIXME in committed code (unless intentional)
Build Validation
[ ] Debug build succeeds
[ ] Release build succeeds
[ ] APK size reasonable (check for bloat)
Git Hygiene
[ ] No untracked files that should be committed
[ ] No secrets/credentials in staged files
[ ] Commit message follows conventions
Compose Stability Guard
The stability config (app/compose_stability_config.conf) asserts covered packages
are immutable; the compiler does not check it. Before release, verify no mutable
constructor/body properties crept into covered state classes:
grep -rEn --include="*.kt" "^ {4}var [a-zA-Z]+:" \
app/src/main/kotlin/com/nendo/argosy/data/model \
app/src/main/kotlin/com/nendo/argosy/data/local/entity \
app/src/main/kotlin/com/nendo/argosy/ui/screens \
app/src/main/kotlin/com/nendo/argosy/ui/components \
app/src/main/kotlin/com/nendo/argosy/ui/dualscreen
Triage hits: a var property on a data class used as UI state or a composable param
is a BLOCKER (fix to val + copy-on-change, or move the class out of covered
packages). private var internals of ViewModels/delegates are exempt, but confirm
nothing reads them directly inside composition.
[ ] Stability guard grep is clean (or all hits triaged exempt)
Workflow
Before Commit
./gradlew lintDebug compileDebugKotlin testDebugUnitTest
Before Push
./gradlew lint testDebugUnitTest compileReleaseKotlin
Before Release
./gradlew lint
./gradlew testDebugUnitTest testReleaseUnitTest
./gradlew assembleRelease
Common Issues & Fixes
Lint Errors
Unused imports/variables:
Hardcoded strings:
@Suppress("HardcodedText")
Missing nullability:
val value = nullable ?: default
Test Failures
Flaky tests:
./gradlew testDebugUnitTest --tests "TestClass.testMethod" --rerun-tasks
Missing mocks:
every { dependency.method() } returns expected
Build Failures
Dependency conflicts:
./gradlew dependencies --configuration releaseRuntimeClasspath
ProGuard issues:
# Check proguard-rules.pro for missing keep rules
-keep class com.nendo.argosy.** { *; }
Single Invocation, Never Parallel
NEVER run concurrent gradle invocations against this project (no separate
terminals, no &). Concurrent invocations corrupted the KSP caches here in a
prior incident, and recovery meant scoped cache invalidation. Pass multiple
tasks to ONE invocation and let Gradle schedule them:
./gradlew lintDebug testDebugUnitTest
Output Interpretation
Lint Output
> Task :app:lintDebug
Wrote HTML report to file:///path/app/build/reports/lint-results-debug.html
0 errors, 3 warnings # ACCEPTABLE - proceed
5 errors, 3 warnings # BLOCKING - fix errors first
app/lint-baseline.xml snapshots the warnings that existed at 2.6.1, so anything
lint reports now is NEW and worth reading. Regenerate it only as a deliberate act
after clearing real findings (delete the file and re-run lintDebug), never to
silence a warning that just appeared.
What the baseline is holding, so it is not mistaken for a backlog of bugs: the
Recycle entries are false positives (cursors already closed via ?.use,
animators driven by a started AnimatorSet); HardwareIds is the device
identity RomM device-sync and request signing depend on; SdCardPath entries are
the upstream-exact emulator data paths the save resolvers need verbatim;
GetInstance is AES-XTS, which is built from ECB primitives by definition; and
the network config permits cleartext and user certificates because the server is
self-hosted, frequently on a LAN with a self-signed cert. Genuinely worth doing
later: ApplySharedPref outside SessionStateStore (where the synchronous write
is deliberate), and the ObsoleteSdkInt dead branches.
Test Output
> Task :app:testDebugUnitTest
85 tests completed, 0 failed # PASS
85 tests completed, 2 failed # BLOCKING - check failures
Build Output
BUILD SUCCESSFUL in 54s # PASS
BUILD FAILED # BLOCKING - check error
Integration with Release Skill
The release skill should invoke pre-release validation automatically:
## Release Workflow (Updated)
1. Ask release type (stable/beta/rc)
2. **RUN PRE-RELEASE VALIDATION (Tier 3)**
- ./gradlew lint
- ./gradlew testDebugUnitTest testReleaseUnitTest
- ./gradlew assembleRelease
3. **RUN COUPLING SWEEP** over changes since last tag (see Coupling Sweep above)
- Resolve flagged axes; for save-sync/RomM confirm live-data proofs were met
4. If validation or sweep blockers remain, STOP and fix issues
5. If clean, proceed with version bump
6. ... (rest of release process)
CI/CD Parity
CI (.github/workflows/build.yml) runs DEBUG variants only:
| Local Command | CI Job |
|---|
./gradlew assembleDebug -PallAbis | build |
./gradlew testDebugUnitTest -PallAbis | test |
./gradlew lintDebug -PallAbis | lint |
PRs additionally run the rules job (blocking agentic smell check + advisory
coupling sweep; skippable via the maintainer-applied rules-exempt PR label).
CI never builds, tests, or lints the RELEASE variant -- the
release-variant checks in Tier 2/3 are local-only and still required before a
release. Running the debug set locally first prevents CI failures.
Quick Reference Commands
./gradlew lint testDebugUnitTest assembleRelease
./gradlew testDebugUnitTest --tests "*DownloadManager*"
./gradlew testDebugUnitTest --tests "*Platform*"
./gradlew testDebugUnitTest --tests "*Emulator*"
./gradlew assembleRelease -x test -x lint
Do NOT reach for ./gradlew clean when a build misbehaves -- house rule.
Invalidate the specific stale cache (scoped) instead; a clean forces the full
cold-compile cost for no diagnostic gain.
Validation Report Template
After running checks, report:
## Pre-Release Validation Report
### Lint
- Status: PASS/FAIL
- Errors: 0
- Warnings: 3 (acceptable)
### Tests
- Status: PASS/FAIL
- Passed: 85
- Failed: 0
- Skipped: 2
### Build
- Status: PASS/FAIL
- APK Size: 12.3 MB
- Build Time: 54s
### Verdict
Ready for release: YES/NO
Blockers: [list if any]