---
name: android-releaser
description: Use when releasing or managing Mission Geo on Google Play Console — building/uploading an AAB, deploying to internal/beta/production, promoting between tracks, bumping versionCode, updating store listing/screenshots/changelogs, managing staged rollouts, or regenerating Play Store screenshots via the apply-patch capture pipeline. Triggers on "release", "publier", "déployer sur Android", "Play Store", "Play Console", "fastlane", "supply", "track internal/beta/prod", "promouvoir", "changelog", "screenshot Play", "mettre à jour les screenshots", "regénérer les screens", "capture pipeline". Use BEFORE any release-related action.
---

# Android Releaser — Mission Geo Play Console

## Overview

**Iron Law:** every Play Console action that fastlane can handle goes through `android/fastlane/Fastfile` lanes — never through the Play Console UI for things fastlane manages (AAB upload, track promotion, listing texts, changelogs, screenshots, staged rollouts). The Play Console UI is only for what fastlane can't touch (content rating, data safety, etc. — see "What fastlane CAN'T do" below).

The pipeline is set up — service account, signing key, lanes are all live. Mission Geo is currently on the **internal** track only. The first production release is the immediate target.

**Sources of truth:**
- `android/fastlane/Fastfile` — every lane
- `android/fastlane/Appfile` — package_name + JSON key path
- `android/fastlane/play-console-key.json` — service account credentials (**gitignored — never commit, never paste anywhere**)
- `android/fastlane/metadata/android/<locale>/` — listing texts, changelogs, images
- `pubspec.yaml` line `version: X.Y.Z+N` — `+N` is the Play versionCode
- Memory: `[[project-fastlane-play-pipeline]]`, `[[feedback-worktree-signing-symlinks]]`

## When to Apply

**Apply BEFORE any of:**
- Releasing a new build (any track)
- Promoting a release between tracks
- Editing Play Store listing (title, description, screenshots, changelogs)
- **Regenerating Play Store screenshots** (see "Updating screenshots" below — has its own apply-patch workflow)
- Configuring or troubleshooting staged rollouts
- Bumping versionCode for a release
- User says "release", "publier", "déployer", "push to Play", "promouvoir prod", "release notes", "mettre à jour les screenshots", "regénérer les screens"

**Skip for:**
- Reading existing Fastfile/Appfile to understand the setup
- Pure code work that doesn't touch release artifacts
- Crashlytics/Analytics investigations (use `mission-geo-analytics` instead)
- Local-only debug/profile builds (no Play involvement)

## Pipeline anatomy

```
android/
├── key.properties                      # Symlinked from main worktree (signing — gitignored)
├── firebase_debug_token.properties     # Symlinked from main worktree (gitignored)
├── app/build.gradle.kts                # Reads pubspec versionCode/Name via flutter.*
└── fastlane/
    ├── Appfile                         # package_name + json_key_file
    ├── Fastfile                        # 6 lanes (see below)
    ├── play-console-key.json           # Service account credentials (GITIGNORED)
    └── metadata/android/<locale>/
        ├── title.txt                   # ≤30 chars
        ├── short_description.txt       # ≤80 chars
        ├── full_description.txt        # ≤4000 chars
        ├── video.txt                   # YouTube URL, optional, ≤100 chars
        ├── changelogs/<N>.txt          # MANDATORY for each shipped versionCode N, ≤500 chars
        └── images/                     # featureGraphic.png + icon.png (flat) + phoneScreenshots/ sevenInchScreenshots/ tenInchScreenshots/ (subfolders)
```

`pubspec.yaml` controls versioning:
```yaml
version: 1.0.0+2   # ↑ versionName (1.0.0)  ↑ versionCode (2) — must increment for every release
```

## Quick Reference — Lanes

All lanes run from `android/` with bundler (Gemfile at repo root).

| Lane | Cmd (prefix `bundle exec --gemfile=../Gemfile fastlane`) | What | When |
|---|---|---|---|
| `ping` | `fastlane ping` | Smoke test — fastlane version + package_name + pubspec version | Sanity-check setup |
| `bump` | `fastlane bump` | Increments `+N` in pubspec.yaml. **Modifies a tracked file — commit it after.** | Before every release (Play rejects duplicate versionCode) |
| `build` | `fastlane build` | `flutter build appbundle --release` → `build/app/outputs/bundle/release/app-release.aab` (requires `android/key.properties` for signing) | Local AAB build, no upload |
| `deploy_internal` | `fastlane deploy_internal` | Uploads existing AAB to track `internal`, `release_status: completed`. **Skips metadata/images/screenshots** — never auto-overwrites listing. Uploads changelog (needs `changelogs/<N>.txt`). | After `bump` + `build` |
| `release_internal` | `fastlane release_internal` | One-shot: `bump` + `build` + `deploy_internal` | Standard ship-to-internal command |
| `promote_internal_to_production` | `fastlane promote_internal_to_production` | Promotes the latest internal release to production (no AAB re-upload, no listing/changelog touch) | Graduation step (after internal validation) |

**For extra control** (validate-only, custom track, staged rollout) — invoke `supply` directly:
```bash
bundle exec --gemfile=../Gemfile fastlane run supply \
  track:production validate_only:true \
  skip_upload_aab:true skip_upload_apk:true
```

## Mandatory pre-flight before any deploy

1. **Symlinks present** (in any worktree other than main) — see `[[feedback-worktree-signing-symlinks]]`:
   ```bash
   ln -s /home/mrjack/git/mission-geo/android/key.properties android/key.properties
   ln -s /home/mrjack/git/mission-geo/android/firebase_debug_token.properties android/firebase_debug_token.properties
   ```
2. **Changelog file exists** for the versionCode about to ship — in **every locale**:
   ```bash
   N=$(grep '^version:' pubspec.yaml | sed 's/.*+//')
   for loc in fr-FR en-US de-DE sr; do
     [ -f android/fastlane/metadata/android/$loc/changelogs/$N.txt ] || echo "MISSING $loc/changelogs/$N.txt"
   done
   ```
3. **`supply validate_only`** dry-run — catches char-limit violations, missing changelogs, auth issues:
   ```bash
   bundle exec --gemfile=../Gemfile fastlane run supply \
     track:internal validate_only:true \
     skip_upload_aab:true skip_upload_apk:true \
     skip_upload_metadata:false skip_upload_changelogs:true \
     skip_upload_images:true skip_upload_screenshots:true
   ```

## Track strategy

```
internal  →  beta  →  production
   │           │           ▲
   └───────────┴───── promotion (no AAB re-upload) ─────┘
```

- **internal** (≤100 testers, no Google review): every dev build. Default for `release_internal`.
- **beta** (open or closed, light review): pre-prod hardening.
- **production** (full review, public): cannot un-publish — only release a new versionCode.

**Promote vs re-upload:**
- Same versionCode moves between tracks → `promote_internal_to_production` (or custom supply call with `track_promote_to:`).
- New versionCode → `release_internal` first, validate, then promote.

**Staged rollout (production only):**
```bash
bundle exec --gemfile=../Gemfile fastlane run supply \
  track:production track_promote_to:production \
  release_status:inProgress rollout:0.05    # 5% → bump to 0.20, 0.50, 1.0 iteratively
```
- Halt: `release_status:halted` (stops new installs, existing keep)
- Resume: re-run with `release_status:inProgress` and new `rollout`
- Finalize: `release_status:completed` (drops `userFraction`, ships 100%)

## Metadata management

### Locale mapping

Mission Geo in-app locales = `fr`, `en`, `de`, `sr-Latn`. Play Console uses different codes:

| In-app | Play Console folder | Notes |
|---|---|---|
| `fr` | `fr-FR` | Default |
| `en` | `en-US` | Use `en-US` not `en-GB` |
| `de` | `de-DE` | |
| `sr-Latn` | `sr` | **Play does NOT support `sr-Latn`** — use `sr`, content must be in Latin script |

Every text file must exist in ALL 4 locales when shipping prod.

### Character limits (Play API enforced)

| Field | Max | File |
|---|---|---|
| Title | 30 | `title.txt` |
| Short description | 80 | `short_description.txt` |
| Full description | 4000 | `full_description.txt` |
| Changelog (per version, per locale) | 500 | `changelogs/<N>.txt` |
| Video URL | 100 | `video.txt` |

`supply validate_only` rejects over-limit content before upload.

### Default Fastfile skip behavior

`deploy_internal` and `promote_internal_to_production` ship with:
```
skip_upload_metadata: true       # listing text NEVER auto-overwritten
skip_upload_images: true         # featureGraphic/icon NEVER auto-overwritten
skip_upload_screenshots: true    # screenshots NEVER auto-overwritten
skip_upload_changelogs: false    # changelogs DO upload (per-versionCode)
```

**Why:** listing/screenshots evolve at a different cadence than the AAB. A typo fix on listing shouldn't require a new build. A new AAB shouldn't risk wiping curated screenshots.

### Intentionally updating listing / screenshots

When you DO want to push listing or screenshots, run `supply` directly with the flags off and an explicit `version_code` (so it knows which changelog applies):
```bash
bundle exec --gemfile=../Gemfile fastlane run supply \
  track:internal version_code:<N> \
  skip_upload_aab:true skip_upload_apk:true \
  skip_upload_metadata:false skip_upload_images:false skip_upload_screenshots:false \
  skip_upload_changelogs:false
```

## Listing & screenshots — what to ship

### Screenshot specs (Play Store)

| Slot | Folder | Count | Aspect | Min/Max | Mission Geo |
|---|---|---|---|---|---|
| Phone | `phoneScreenshots/` | 2–8 | Portrait 9:16 (app is portrait-locked) | 320 short / 3840 long | **7 × 4 locales = 28** |
| 7" tablet | `sevenInchScreenshots/` | 1–8 | Portrait | same | **5 × 4 locales = 20** (subset of phone) |
| 10" tablet | `tenInchScreenshots/` | 1–8 | Portrait | same | **5 × 4 locales = 20** (subset of phone) |
| TV | `tvScreenshots/` | — | — | — | N/A |
| Wear | `wearScreenshots/` | — | — | — | N/A |

**Naming**: prefix with `01_`, `02_`, … to lock order on Play Store (files uploaded in lexicographic order).

### Mission Geo shot list (phone — 7 shots)

| # | File | Screen | State |
|---|---|---|---|
| 1 | `01_landmark_taj_mahal.png` | Landmark typing | Taj Mahal photo + question |
| 2 | `02_drawing_south_africa.png` | Drawing game | 🇿🇦 ~50% coloured (fallback 🇧🇷) |
| 3 | `03_searching_chile.png` | Searching/map | "Trouve le Chili", zoomed on South America |
| 4 | `04_duel_local_3_players.png` | Local duel | 3 players, no pseudonyms |
| 5 | `05_adventure_regions.png` | Adventure regions list | Mix medals + locked tiles |
| 6 | `06_defi_classement_population.png` | Classement Défi | Population: NG/BR/MX placed (3/5), JP+DE in pool |
| 7 | `07_ranking_swiss_vs_brazil.png` | Ranking ELO | 🇨🇭 vs 🇧🇷 |

### Tablet subset (10" + 7", same 5 shots)

Use **#1, #2, #3, #5, #6** — main-view shots with content density. Skip #4 (Duel) and #7 (Ranking) on tablet (format-dependent / low content for large screens).

### Graphics specs

| Slot | File path | Spec |
|---|---|---|
| Feature graphic | `images/featureGraphic.png` | **Exactly 1024×500 px**, JPEG or 24-bit PNG **without alpha**, ≤8 MB |
| Icon | `images/icon.png` | 512×512, 32-bit PNG **with alpha** |
| TV banner | `images/tvBanner.png` | 1280×720 — N/A (no TV build) |
| Promo graphic | `images/promoGraphic.png` | 180×120 — legacy, optional |

These are **flat single files inside `images/`** — not subfolders. `supply` discovers them at `metadata/android/<locale>/images/<type>.{png,jpg,jpeg}` (`supply/lib/supply.rb:21`). A subfolder layout silently uploads nothing (the run completes in <1 s with no `⬆️ Uploading image` lines, just `Uploaded all items` — useless).

Mission Geo's feature graphic is produced externally via the ChatGPT prompt (see [[project-fastlane-play-pipeline]]) and dropped at `images/featureGraphic.png`.

## Updating screenshots — apply-patch capture pipeline

**Trigger phrases:** *"mettre à jour les screenshots", "regénérer les screens / screenshots", "refresh les captures", "redo the Play Store screenshots", "update screenshots".*

### The architecture (read this once)

The 7 Play Store scenarios each need a **deterministic in-app state** that the user would normally have to grind hours of gameplay to reach (full Adventure progression, mid-game duel, forced ranking pair, half-coloured South African flag, etc.). The pipeline injects that state directly via compile-time hooks gated on a `kScreenshotMode` flag.

**Why a patch and not committed code:** these hooks touch ~12 production files. Even though they const-fold to no-ops without `--dart-define=SCREENSHOT_MODE=true`, they're a permanent regression-surface on the UI hot path. We bundle them into a single patch that lives at `.claude/skills/android-releaser/screenshot-mode.patch` and is applied only during a capture session.

**The orchestrator refuses to run if the patch is not applied** (it checks for `lib/core/dev/screenshot_mode.dart`).

### Prerequisites

1. **Emulator via the shared pool** — claim one of the 3 pool ports (5554/5556/5558) atomically so the capture never collides with a parallel agent (no more dedicated `mission_geo_capture` AVD — that's gone; read-only multi-instance means there's nothing to "keep separate"). The capture script auto-detects the device type from the **booted AVD name** (`mission_geo_phone` / `mission_geo_tablet7` / `mission_geo_tablet10`), so just boot the AVD you want to shoot. **Never `emu kill` an emulator you didn't start** — release only your own port. See [[feedback-emulator-must-be-free]].

   ```bash
   source "$(git rev-parse --show-toplevel)/.claude/skills/shared/emulator-pool.sh"
   mg_claim_port
   mg_boot_avd mission_geo_phone          # phone screenshots first
   # then capture the tablet tiers by swapping on the SAME port:
   #   mg_swap_avd mission_geo_tablet7
   #   mg_swap_avd mission_geo_tablet10
   # when fully done:  mg_release_port
   ```

2. **Worktree symlinks** present (signing + App Check debug token) — see [[feedback-worktree-signing-symlinks]]. The orchestrator builds a DEBUG APK; without the App Check debug token, Firebase calls would hang on the splash.

3. **Branch + worktree** for this work — never on `main` directly. The patch + screenshots produce a real diff that needs review before merge.

### The 4-step workflow

```bash
# ── Step 1 — Apply the patch
git apply .claude/skills/android-releaser/screenshot-mode.patch
# ↳ Adds lib/core/dev/screenshot_mode.dart + edits to 11 files.
# ↳ flutter analyze is still clean after this (everything compile-time-gated).

# ── Step 2 — Run the orchestrator
.claude/skills/android-releaser/scripts/capture_play_screenshots.sh emulator-5554
# ↳ Builds debug APK with --dart-define=SCREENSHOT_MODE=true.
# ↳ Installs + adb-root + walks onboarding once (UMP refuse + pseudo "Sam" + Asia region).
# ↳ Seeds the DB (2450 miles, 5 hints, 6 unlocked regions with mixed progress).
# ↳ Loops 7 scenarios × 4 locales = 28 captures (~12 min total).
# ↳ Writes to android/fastlane/metadata/android/<locale>/images/phoneScreenshots/.

# ── Step 3 — Visually verify EVERY capture (NOT optional)
for loc in fr-FR en-US de-DE sr; do
  mkdir -p /tmp/mg-previews/$loc
  for f in android/fastlane/metadata/android/$loc/images/phoneScreenshots/*.png; do
    convert "$f" -resize 400x "/tmp/mg-previews/$loc/$(basename $f)"
  done
done
# Then Read each /tmp/mg-previews/<loc>/<scenario>.png — the originals are
# 1080×2400 which exceeds the 2000-line Read tool budget. Check:
#   - target screen reached (not stuck on splash)
#   - locale strings rendering (German/Serbian Latin etc.)
#   - state correctly injected (Chile green, Switzerland vs Brazil, etc.)
#   - 2450 miles + 5 hints visible (not the placeholder 99999)

# ── Step 4 — Revert the patch
git apply -R .claude/skills/android-releaser/screenshot-mode.patch
flutter analyze  # confirm clean — must be unchanged from baseline
```

After Step 4, the **only** diff vs. main is the new PNG files in `metadata/.../phoneScreenshots/`. That's what gets reviewed and committed.

### Filtering (faster iteration on a single scenario)

When tweaking one scenario or one locale, skip the full 12-minute loop:

```bash
# Single scenario, all 4 locales
.claude/skills/android-releaser/scripts/capture_play_screenshots.sh emulator-5554 landmarkTaj

# Single scenario, single locale (fastest — ~45s)
.claude/skills/android-releaser/scripts/capture_play_screenshots.sh emulator-5554 landmarkTaj fr-FR

# Multiple scenarios + multiple locales
.claude/skills/android-releaser/scripts/capture_play_screenshots.sh emulator-5554 drawingSouthAfrica,searchingChile fr-FR,en-US
```

Scenario names (must match the `ScreenshotScenario` enum):
`landmarkTaj`, `drawingSouthAfrica`, `searchingChile`, `duelLocal3Players`, `adventureRegions`, `defiClassementPopulation`, `rankingSwissBrazil`.

### What the patch wires up

| File | Role |
|---|---|
| `lib/core/dev/screenshot_mode.dart` (new) | `kScreenshotMode` const, scenario enum, MethodChannel reader, per-scenario state helpers (`screenshotDrawingPrefillRatio`, `screenshotClassementSpec`, `screenshotRankingPair`, `screenshotDuelSpec`), typed-route push for routes with required constructor args |
| `MainActivity.kt` | `mission_geo/screenshot` MethodChannel piping `screenshot_scenario` + `screenshot_locale` intent extras to Dart |
| `main.dart` | `await ScreenshotMode.init()` before `runApp`; hides debug banner when active |
| `splash_screen.dart` | Bypasses Firebase auth + initial-sync (emulator Play Services are too old), pushes the scenario's route |
| `swipe_flags.dart` (Ranking) | Override `_pickFlags` with forced left/right pair |
| `drawing_game_provider.dart` | Pre-fill biggest zones (50%) with correct colour |
| `color_palette.dart` | Convert StatelessWidget → StatefulWidget, scroll palette 60 px so a clipped colour signals "more available" |
| `searching_game_provider.dart` | Set `showCorrectAnswer=true, isCorrect=true` so target paints green |
| `map_canvas.dart` | Continental zoom (scale 9) on target; suppress the zoom-on-correct animation |
| `classement_provider.dart` | Build deterministic round: Population stat, NG/BR/MX/JP/DE, first 3 pre-placed |
| `duel_setup_page.dart` | Pre-select 3 players (lobby shot, no auto-start) |
| `duel_game_page.dart` | Dead in this build (kept in case we restore the trophy phase) |

The orchestrator (`.claude/skills/android-releaser/scripts/capture_play_screenshots.sh`) and DB seed (`.claude/skills/android-releaser/scripts/seed_screenshot_db.py`) stay in the repo permanently — they're dev tools that don't affect any production build.

### Adding or changing a scenario

When the shot list evolves (new feature you want to showcase, replace an old scenario, change the deterministic state of one):

1. **Apply the patch** so the scaffolding is in place.
2. **Edit `lib/core/dev/screenshot_mode.dart`**:
   - Add a value to `ScreenshotScenario` enum.
   - Add a deep route in `_routeFor` (path-only routes) OR add a `case` in `pushScenarioRoute` (typed routes with constructor args, like `ClassementGameRoute(region: …)`).
   - If the scenario needs state injection, add a `screenshotXxxSpec()` helper that returns `null` outside the scenario.
3. **Hook the helper** in the target page/notifier. Gate every override on `ScreenshotMode.scenario == ScreenshotScenario.xxx`.
4. **Add the scenario** to `SCENARIOS` array in `.claude/skills/android-releaser/scripts/capture_play_screenshots.sh` with a filename stem (e.g. `08_my_new_scenario`).
5. **Iterate**: filter to your scenario only, screenshot, tweak, re-screenshot until clean.
6. **Regenerate the patch** — replaces the existing one with the updated diff:
   ```bash
   MERGE_BASE=$(git merge-base main HEAD)
   git diff "$MERGE_BASE"..HEAD -- \
     lib/core/dev/screenshot_mode.dart \
     android/app/src/main/kotlin/app/missiongeo/MainActivity.kt \
     lib/main.dart lib/pages/splash/splash_screen.dart \
     lib/pages/games/drawing/state/drawing_game_provider.dart \
     lib/pages/games/drawing/widgets/color_palette.dart \
     lib/pages/games/searching/state/searching_game_provider.dart \
     lib/pages/games/searching/widgets/map_canvas.dart \
     lib/pages/modes/defi/classement/state/classement_provider.dart \
     lib/pages/modes/duel/duel_game_page.dart \
     lib/pages/modes/duel/duel_setup_page.dart \
     lib/pages/modes/favorite_flags/swipe_flags.dart \
     > .claude/skills/android-releaser/screenshot-mode.patch
   ```
   Note: if you've added/removed files from this 12-file list, update both the `git diff` call AND the file list in this skill.
7. **Revert the patch** and verify `git apply --check .claude/skills/android-releaser/screenshot-mode.patch` still applies cleanly to the reverted state.

### Common pitfalls

| Symptom | Cause | Fix |
|---|---|---|
| Orchestrator exits with "Screenshot-mode hooks are not applied" | Step 1 skipped | `git apply .claude/skills/android-releaser/screenshot-mode.patch` |
| Capture shows the splash screen ("Chargement…") | Splash bypass took longer than 30 s (cold APK install + Gradle daemon cold) | Re-run the single scenario — second pass uses warm caches. If persistent, increase the `sleep 30` in the orchestrator |
| Capture shows Home instead of target scenario | Typed route not registered in `pushScenarioRoute` (path-only `pushPath` can't pass constructor args like `ChallengeRegion`) | Add a `case` in `pushScenarioRoute` for the scenario |
| State override doesn't apply (random country, wrong stat, etc.) | Notifier race: `_initialize()`/`_pickRandomCountry` overwrites state set by `setTargetCountry` | Apply the override at the EARLIEST notifier write that touches `state.copyWith(...)` for that field, not just in the user-facing setter |
| Wrong locale in capture | Orchestrator launched without `--es screenshot_locale "<tag>"` OR the intent extra didn't reach `ScreenshotMode.init()` | Check Kotlin MethodChannel's `getLocale` handler returns the right value via `adb shell dumpsys activity | grep -A2 screenshot_locale` |
| Soft keyboard typing doesn't work via `adb shell input text` | Flutter's IME bridge swallows raw text events | Use per-key `input tap` against UIAutomator-dumped bounds — see `landmarkTaj` case in the orchestrator |
| `flutter analyze` fails after revert | A reverted file was edited between apply and revert | `git checkout HEAD -- <file>` to restore, then re-revert |
| Two agents fight over the same port | Both tried to self-allocate a port without the pool | Always `source .claude/skills/shared/emulator-pool.sh` + `mg_claim_port` — the `flock` makes the claim atomic. **NE TUE RIEN** d'un autre agent ; libère seulement ton port (`mg_release_port`). |

### After capture: ship the screenshots

After the patch is reverted and you've reviewed the PNG diff, the upload itself goes through `supply` (see "Intentionally updating listing / screenshots" above):

```bash
N=$(grep '^version:' pubspec.yaml | sed 's/.*+//')
bundle exec --gemfile=../Gemfile fastlane run supply \
  track:internal version_code:$N \
  skip_upload_aab:true skip_upload_apk:true \
  skip_upload_metadata:true skip_upload_images:true \
  skip_upload_screenshots:false skip_upload_changelogs:true
```

`skip_upload_screenshots:false` is the only thing you really need; everything else stays untouched.

## What fastlane CAN'T do (Play Console UI only)

No fastlane equivalent — manual Play Console work, typically once during initial production setup:

| Field | Location |
|---|---|
| **IARC content rating questionnaire** | Production → App content → Content ratings |
| **Data safety form** | Production → App content → Data safety |
| **Target audience and content** | Production → App content → Target audience |
| **Ads declaration** | Production → App content → Ads |
| **Government / news app status** | Production → App content |
| **Pricing & distribution** | Production → Distribution (country availability matrix) |
| **Pre-registration** | Growth → Pre-registration |
| **Store listing experiments (A/B)** | Growth → Store listing experiments |
| **User review replies** | Quality → Ratings and reviews |
| **In-app products / subscriptions** | Monetisation → Products |
| **App signing key rotation** | Setup → App signing |

These don't re-occur per release. If a fresh prod release is blocked with "App content declarations incomplete", first fix the missing forms in Play Console UI, then re-run the supply call — fastlane retries automatically when `rescue_changes_not_sent_for_review: true` (default).

## Validate before shipping production

```bash
# 1. Dry-run validate
bundle exec --gemfile=../Gemfile fastlane run supply \
  track:production validate_only:true skip_upload_aab:true skip_upload_apk:true \
  skip_upload_metadata:false skip_upload_images:false skip_upload_screenshots:false \
  skip_upload_changelogs:true

# 2. Check all 4 locales have content
for loc in fr-FR en-US de-DE sr; do
  for f in title short_description full_description; do
    [ -s android/fastlane/metadata/android/$loc/$f.txt ] || echo "MISSING $loc/$f.txt"
  done
done

# 3. Check changelog exists for current versionCode (in all locales)
N=$(grep '^version:' pubspec.yaml | sed 's/.*+//')
for loc in fr-FR en-US de-DE sr; do
  [ -f android/fastlane/metadata/android/$loc/changelogs/$N.txt ] || echo "MISSING $loc/changelogs/$N.txt"
done

# 4. Check screenshot counts (every locale, every device type)
for loc in fr-FR en-US de-DE sr; do
  for dev in phoneScreenshots sevenInchScreenshots tenInchScreenshots; do
    count=$(ls android/fastlane/metadata/android/$loc/images/$dev 2>/dev/null | wc -l)
    echo "$loc/$dev: $count"
  done
done
```

Only after all 4 return clean → run `release_internal` then `promote_internal_to_production` (or `supply` with staged rollout).

## Red Flags — STOP

| Symptom | Cause | Action |
|---|---|---|
| `Cannot find changelog because no version code given` | Metadata-only run without `version_code:N` | Add `version_code:N` to the supply call |
| `Execution failed for task ':app:signReleaseBundle' > NullPointerException` | Missing `key.properties` symlinks in worktree | See [[feedback-worktree-signing-symlinks]] |
| `Changes cannot be sent for review automatically` | First time on a track / pending Play Console declarations | Default `rescue_changes_not_sent_for_review:true` retries with the flag; if it persists, fix the declaration in Play Console UI manually |
| `Invalid release - cannot have status completed with rollout fraction` | Mixed `release_status:completed` with `rollout:` | Use `release_status:inProgress` for staged rollouts |
| AAB size > 200 MB warning | Bundle is heavy (Mission Geo ~220 MB) | Set `ack_bundle_installation_warning:true` if Play rejects (currently auto-acked, may break in future) |
| About to push to **production** | Highest-risk action | MUST validate dry-run + show diff + get explicit user OK before |
| Updating screenshots / listing | Overwrites carefully-curated content | Always show user what's about to change first, get explicit OK |
| Service account key visible in `git status` | Catastrophic credential leak | `git restore --staged android/fastlane/play-console-key.json` immediately; if pushed, **rotate the key in GCP** then `bfg` / `git filter-repo` history |
| User says "merger" or "ça marche" | Validation, not push authorization | Merge is OK, but `git push origin main` requires SEPARATE explicit user approval — see `[[feedback-merge-does-not-imply-push]]` |
| About to commit `lib/core/dev/screenshot_mode.dart` or any other patched file | Capture session not reverted — these belong in the patch, NOT the codebase | `git apply -R .claude/skills/android-releaser/screenshot-mode.patch` first. Only the new PNGs in `metadata/.../phoneScreenshots/` should appear in the final diff. |

## Recovery

### "Production rollout going badly"
```bash
# Halt — existing installs OK, no new ones
bundle exec --gemfile=../Gemfile fastlane run supply \
  track:production track_promote_to:production \
  release_status:halted version_code:<N>
```
Fix forward in next versionCode. Play Store doesn't support true rollback — only ship a fixed new versionCode.

### "Service account key compromised"
1. Revoke in GCP Console → IAM → Service accounts → `play-console-release@…` → Keys → delete
2. Generate new JSON key from the same service account
3. Drop into `android/fastlane/play-console-key.json` (same path, gitignored — no code change)
4. Verify with `bundle exec fastlane ping`

### "I bumped versionCode but the build failed"
```bash
git restore -- pubspec.yaml
```
Bump reverted. Re-run `release_internal` after fixing the build issue.

### "I committed the JSON key by mistake"
**Immediate:**
1. **Revoke the key in GCP NOW** (the file IS the credential — assume leak the moment it's in any commit)
2. Remove from git history (`git filter-repo` or BFG, not just `git rm`)
3. Generate new key, place at same path
4. Confirm `.gitignore` covers `android/fastlane/play-console-key.json`
5. Force-push only after explicit user approval

## Sub-skills required

- **`git-workflow-branch-worktree`** — REQUIRED before any change to fastlane configs or metadata. Release work is still code work, never on `main` directly.
- **`visual-validation-android`** — REQUIRED when capturing screenshots. Drives the Android emulator + captures PNG via `adb`.
- **`mission-geo-design-system`** — when producing the feature graphic or icon: colors must come from `AppColors`, typography from DynaPuff (display) / Quicksand (body).

## References

- [Fastlane `supply` reference](https://docs.fastlane.tools/actions/supply/)
- [Play Console — Track structure](https://support.google.com/googleplay/android-developer/answer/9845334)
- [Play Console — Preview assets specs](https://support.google.com/googleplay/android-developer/answer/9866151)
- [Google Play Developer API — Edits model](https://developers.google.com/android-publisher/edits)
- Memory: `[[project-fastlane-play-pipeline]]` — service account, lanes, locale mapping
- Memory: `[[feedback-worktree-signing-symlinks]]` — required symlinks for release builds in worktrees
- Memory: `[[feedback-merge-does-not-imply-push]]` — never push without explicit user approval
