# Pre Release Validation

> Run local CI/CD checks before pushing code. Use before commits, releases, or when validating code quality. Catches issues before they reach remote.

- Skill: `rommapp/pre-release-validation` (Agent Skill)
- Install (CLI): `npx skillmds@latest add rommapp/pre-release-validation`
- Raw SKILL.md: https://api.skillmd.com/api/skills/rommapp/pre-release-validation/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra, Coding & Dev Tools
- Author: rommapp (https://skillmd.com/u/rommapp)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/rommapp/pre-release-validation

---


# 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.

```bash
# Lint check
./gradlew lintDebug 2>&1 | head -50

# Compile check
./gradlew compileDebugKotlin 2>&1
```

### Tier 2: Standard Check (warm ~10 min)
Full validation before commits.

```bash
# Lint
./gradlew lintDebug

# Unit tests
./gradlew testDebugUnitTest

# Compile release
./gradlew compileReleaseKotlin
```

### Tier 3: Full Check (warm 15+ min; much longer cold)
Complete validation before releases.

```bash
# Full lint (both debug and release)
./gradlew lint

# All unit tests
./gradlew testDebugUnitTest testReleaseUnitTest

# Full release build
./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.

```bash
# Sweep every change since the last release tag
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:

```bash
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
```bash
# Quick validation
./gradlew lintDebug compileDebugKotlin testDebugUnitTest
```

### Before Push
```bash
# Standard validation
./gradlew lint testDebugUnitTest compileReleaseKotlin
```

### Before Release
```bash
# Full validation (run these in sequence)
./gradlew lint
./gradlew testDebugUnitTest testReleaseUnitTest
./gradlew assembleRelease
```

---

## Common Issues & Fixes

### Lint Errors

**Unused imports/variables:**
```kotlin
// Remove or use the import/variable
// Android Studio: Optimize Imports (Cmd+Opt+O)
```

**Hardcoded strings:**
```kotlin
// Move to strings.xml or suppress if intentional
@Suppress("HardcodedText")
```

**Missing nullability:**
```kotlin
// Add explicit null handling
val value = nullable ?: default
```

### Test Failures

**Flaky tests:**
```bash
# Run specific test multiple times
./gradlew testDebugUnitTest --tests "TestClass.testMethod" --rerun-tasks
```

**Missing mocks:**
```kotlin
// Ensure all dependencies are mocked in test setup
every { dependency.method() } returns expected
```

### Build Failures

**Dependency conflicts:**
```bash
# Check dependency tree
./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:

```bash
./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:

```markdown
## 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

```bash
# One-liner: Full pre-release validation
./gradlew lint testDebugUnitTest assembleRelease

# Check specific area after changes
./gradlew testDebugUnitTest --tests "*DownloadManager*"
./gradlew testDebugUnitTest --tests "*Platform*"
./gradlew testDebugUnitTest --tests "*Emulator*"

# Just build, skip tests (quick sanity check)
./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:

```markdown
## 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]
```

