From 2b05e5031268e7d9eb46975bd5d99aec650dde18 Mon Sep 17 00:00:00 2001 From: jjbrunton Date: Mon, 31 Aug 2026 11:32:32 +0100 Subject: [PATCH] feat(release): add fastlane ship lanes and end-to-end release pipeline docs Co-Authored-By: Claude Fable 5 --- .claude/skills/release-mobile/SKILL.md | 2 +- .codex/skills/generate-release/SKILL.md | 12 +- .codex/skills/release-mobile/SKILL.md | 92 +++- apps/mobile/.gitignore | 1 + apps/mobile/fastlane/Appfile | 1 + apps/mobile/fastlane/Fastfile | 82 ++++ apps/mobile/package.json | 3 + docs/mobile-ota.md | 7 +- docs/releasing.md | 620 +++++++++++++----------- 9 files changed, 490 insertions(+), 330 deletions(-) diff --git a/.claude/skills/release-mobile/SKILL.md b/.claude/skills/release-mobile/SKILL.md index 1b7ab74..07339ef 100644 --- a/.claude/skills/release-mobile/SKILL.md +++ b/.claude/skills/release-mobile/SKILL.md @@ -1,6 +1,6 @@ --- name: release-mobile -description: Prepare and verify Sauci iOS or Android releases and store uploads. +description: Prepare, verify, and ship Sauci iOS or Android releases end to end, including store uploads, review submission, and rollout. --- Read `.codex/skills/release-mobile/SKILL.md` completely and follow it. diff --git a/.codex/skills/generate-release/SKILL.md b/.codex/skills/generate-release/SKILL.md index 25a6477..1215f43 100644 --- a/.codex/skills/generate-release/SKILL.md +++ b/.codex/skills/generate-release/SKILL.md @@ -14,12 +14,12 @@ before accepting a build. A request to generate or create a release authorizes the scoped version edits, release notes, local builds, release commits, and one local annotated tag. When -the request also specifies a mobile release, upload, or store-submission -workflow, it authorizes the requested store upload within its stated platform -and destination scope after the exact artifact has been verified. It does not -authorize pushing commits or tags, changing signing credentials, regenerating -native projects, a public rollout, App Review submission, or additional -TestFlight distribution unless the request explicitly includes that action. +the request also asks to ship, upload, submit, or release to the stores, it +authorizes the complete store pipeline for the stated platforms after the exact +artifact has been verified: upload, App Review / Google Play review submission, +and rollout, including full production rollout (see the `release-mobile` skill). +It does not authorize pushing commits or tags, changing signing credentials, or +regenerating native projects unless the request explicitly includes that action. ## Defaults diff --git a/.codex/skills/release-mobile/SKILL.md b/.codex/skills/release-mobile/SKILL.md index d1ff6be..8906de0 100644 --- a/.codex/skills/release-mobile/SKILL.md +++ b/.codex/skills/release-mobile/SKILL.md @@ -1,6 +1,6 @@ --- name: release-mobile -description: Prepare and verify Sauci iOS or Android releases. Use for versioning, EAS/native builds, Fastlane upload, release notes, or store submission preparation. +description: Prepare, verify, and ship Sauci iOS or Android releases end to end. Use for versioning, EAS/native builds, store uploads, App Review / Play review submission, and rollout. --- # Mobile release @@ -9,6 +9,41 @@ Read `docs/releasing.md`, `apps/mobile/RELEASING.md` when present, and the mobil scoped instructions. Confirm target platform and release profile. Run repository verification plus a native acceptance pass before starting a release build. +## OTA update vs full store release + +Choose the delivery mechanism before doing anything else; they are different +pipelines with different blast radii. `docs/mobile-ota.md` is the authority. + +- **OTA update** (`npm run ota:publish:` in `apps/mobile`): ships only the + JavaScript bundle and assets to builds already installed on devices. No store + review, live within minutes, and rolled back by repointing the XPREM channel + at a previous update. Valid only when the change touches no native code, no + native dependency, no `ios/`/`android/` project files, and no config that + alters the native binary. The OTA runtime version equals the public app + version, so an OTA update can only reach installs of the current + `expo.version`. +- **Full store release** (this skill): a new signed binary through App Store + Connect and Google Play, including review. Required whenever native code, + native dependencies, the checked-in native projects, Expo SDK, permissions, + entitlements, or `expo.version` change. Rollback is coarser: Play staged + rollouts can be halted, iOS phased release can be paused, and a bad release is + otherwise superseded by shipping the next version. + +When a change qualifies for OTA, prefer OTA unless the user asks for a store +release. Never present an OTA publish as a store release or vice versa; report +which mechanism was used. + +## Authorization + +An explicit request for a mobile release authorizes the complete pipeline for +the stated platforms: version bump, build, verification, store upload, review +submission (App Review / Google Play review), and rollout — including full +production rollout. Rollout is recoverable by design (staged-rollout halt, +phased-release pause, OTA supersession, or the next store version), so do not +pause to re-confirm these steps; execute them and report. A request that +explicitly narrows scope (for example "upload only", "TestFlight only", or a +named track) still bounds what you do. + Before a build from the current upstream branch, fetch, require a clean worktree, and prove `HEAD` matches its upstream. The `generate-release` skill may instead build a clean, scoped release-preparation commit that descends directly from that @@ -17,37 +52,41 @@ verified EAS version metadata. Sauci tracks its native iOS and Android projects, so routine release builds must package those checked-in sources. Do not run `expo prebuild` first; native regeneration is a separate reviewed change. -Use the package scripts in `apps/mobile/package.json`; they create artifacts only. -An explicit request for a mobile release, upload, or store-submission workflow -authorizes the requested store upload within its stated platform and destination -scope. Building an artifact alone does not authorize an upload. +Use the package scripts in `apps/mobile/package.json` to build artifacts and the +Fastlane lanes in `apps/mobile/fastlane/Fastfile` to upload, submit, and roll +out. -## Upload execution +## Upload and submission execution -Before an external upload, independently verify and report: +Verification before upload is unchanged and mandatory. Independently verify and +report: - the platform, exact artifact path, bundle/package identifier, public version, build number, signature identity, and SHA-256 digest; -- the exact destination and resulting state: Google Play internal-track draft or - App Store Connect/TestFlight processing; -- that the upload does not authorize a public rollout or App Review submission. +- the exact destination and resulting state: which Play track and rollout + status, or App Store Connect version state and whether App Review submission + and automatic release are included. -For iOS, inspect the current App Store Connect automatic-distribution behavior -before uploading. If the build would be distributed automatically, report the -exact TestFlight groups or testers. If the effect cannot be established, stop; -changing an automatic-distribution setting is a separately authorized action. +Once these details match the request, proceed through upload, review +submission, and rollout without requesting further confirmation. -Once these details match the user's stated scope, upload without requesting a -second confirmation. Run `eas submit` with the explicit -`--platform`, `--profile production`, `--path`, and `--wait` arguments; never -select `--latest`. If the result is ambiguous, inspect EAS and the store before -doing anything else; do not retry unless the user explicitly authorizes another -upload. +- **Android**: `bundle exec fastlane android release aab:` + commits the release on the production track (pass `track:` to target another + track, `rollout:0.1` for a staged rollout). Play review runs automatically; + rollout proceeds on approval. Use the `upload` lane only when the request is + explicitly upload-only. +- **iOS**: `bundle exec fastlane ios release ipa:` uploads, + creates the App Store version, submits for App Review, and releases + automatically on approval (pass `phased_release:true` for a 7-day phased + release, `release_notes:"..."` for What's New). Use the `upload` lane for + TestFlight-only requests. `eas submit` remains valid for upload-only flows; + when used, pass explicit `--platform`, `--profile production`, `--path`, and + `--wait` — never `--latest`. -Apply this policy to every upload method, including EAS, Fastlane, Xcode -Organizer, and the store consoles. A public rollout, App Review submission, or -additional TestFlight distribution requires separate explicit authorization -unless it was expressly included in the original request. +If an upload or submission command returns an ambiguous result, inspect EAS and +the store before doing anything else; do not retry blind. After submission, read +back the store state (Play Console track status / App Store Connect version +state) and report it. Preserve platform signing and local EAS versioning. After a build, verify that tracked changes are limited to the expected version metadata, then independently @@ -55,5 +94,6 @@ read the artifact version and signature. For an iOS IPA, inspect every packaged app extension as well as the containing app and require matching `CFBundleShortVersionString` and `CFBundleVersion` values. Treat Xcode archive warnings as diagnostic evidence, not the final artifact state: decide whether to -reject or rebuild only after reading the exported IPA itself. Report the artifact, -version/build number, upload result when authorized, and store read-back state. +reject or rebuild only after reading the exported IPA itself. Report the +artifact, version/build number, upload and submission results, rollout state, +and store read-back state. diff --git a/apps/mobile/.gitignore b/apps/mobile/.gitignore index 8f5435e..e45d01f 100755 --- a/apps/mobile/.gitignore +++ b/apps/mobile/.gitignore @@ -11,5 +11,6 @@ expo-env.d.ts *.keystore # Fastlane fastlane-key.json +fastlane/.env fastlane/report.xml fastlane/README.md diff --git a/apps/mobile/fastlane/Appfile b/apps/mobile/fastlane/Appfile index 5084007..453582b 100644 --- a/apps/mobile/fastlane/Appfile +++ b/apps/mobile/fastlane/Appfile @@ -1,2 +1,3 @@ json_key_file(ENV["GOOGLE_PLAY_KEY_FILE"] || File.expand_path("../fastlane-key.json", __dir__)) package_name("com.sauci.app") +app_identifier("com.sauci.app") diff --git a/apps/mobile/fastlane/Fastfile b/apps/mobile/fastlane/Fastfile index 441406f..3b63e6c 100644 --- a/apps/mobile/fastlane/Fastfile +++ b/apps/mobile/fastlane/Fastfile @@ -45,4 +45,86 @@ platform :android do lane :upload_beta do |options| upload(options.merge(track: "beta")) end + + desc "Upload AAB and release it: commits the release on the given track (default production), triggering Play review and rollout on approval" + lane :release do |options| + track = options[:track] || "production" + + project_root = File.expand_path("..", __dir__) + aab_path = options[:aab] + UI.user_error!("Pass the exact verified artifact with aab: — never release an implicit build") unless aab_path + aab_path = File.expand_path(aab_path, project_root) unless aab_path.start_with?("/") + UI.user_error!("AAB not found at #{aab_path}") unless File.exist?(aab_path) + + rollout = options[:rollout] # e.g. "0.1" for 10% staged rollout; omit for full rollout + + UI.message("Releasing #{aab_path} to #{track} track#{rollout ? " at #{(rollout.to_f * 100).round}% rollout" : " at full rollout"}...") + + upload_to_play_store( + track: track, + aab: aab_path, + skip_upload_metadata: true, + skip_upload_images: true, + skip_upload_screenshots: true, + skip_upload_changelogs: false, + release_status: rollout ? "inProgress" : "completed", + rollout: rollout, + ) + + UI.success("Release committed to #{track}. Google Play review runs automatically; rollout proceeds on approval.") + end +end + +platform :ios do + # App Store Connect API key, supplied via environment (never committed): + # ASC_KEY_ID, ASC_ISSUER_ID, ASC_KEY_PATH (path to the .p8 file) + private_lane :asc_api_key do + UI.user_error!("Set ASC_KEY_ID, ASC_ISSUER_ID, and ASC_KEY_PATH") unless ENV["ASC_KEY_ID"] && ENV["ASC_ISSUER_ID"] && ENV["ASC_KEY_PATH"] + app_store_connect_api_key( + key_id: ENV["ASC_KEY_ID"], + issuer_id: ENV["ASC_ISSUER_ID"], + key_filepath: ENV["ASC_KEY_PATH"], + ) + end + + desc "Upload IPA to App Store Connect / TestFlight only (no App Review submission)" + lane :upload do |options| + ipa_path = options[:ipa] + UI.user_error!("Pass the exact verified artifact with ipa:") unless ipa_path && File.exist?(File.expand_path(ipa_path)) + + upload_to_testflight( + api_key: asc_api_key, + ipa: File.expand_path(ipa_path), + skip_waiting_for_build_processing: false, + ) + UI.success("Uploaded to App Store Connect; build is processing for TestFlight.") + end + + desc "Upload IPA, create the App Store version, and submit it for App Review with automatic release on approval" + lane :release do |options| + ipa_path = options[:ipa] + UI.user_error!("Pass the exact verified artifact with ipa:") unless ipa_path && File.exist?(File.expand_path(ipa_path)) + + api_key = asc_api_key + + upload_to_app_store( + api_key: api_key, + ipa: File.expand_path(ipa_path), + submit_for_review: true, + automatic_release: options.fetch(:automatic_release, true), + phased_release: options.fetch(:phased_release, false), + precheck_include_in_app_purchases: false, + # Metadata lives in App Store Connect; only release notes change per version. + skip_metadata: options[:release_notes].nil?, + skip_screenshots: true, + release_notes: options[:release_notes] ? { "en-GB" => options[:release_notes], "default" => options[:release_notes] } : nil, + submission_information: { + add_id_info_uses_idfa: false, + export_compliance_uses_encryption: false, + }, + force: true, # skip interactive HTML preview; agents run non-interactively + ) + + UI.success("Submitted for App Review. The version releases #{options.fetch(:automatic_release, true) ? "automatically on approval" : "manually after approval"}.") + end end diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 8e5e88c..21bc71e 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -19,6 +19,9 @@ "release:android": "dotenv -e .env -- eas build --platform android --profile production --local", "release:android:preview": "dotenv -e .env -- eas build --platform android --profile preview --local", "release:android:upload": "bundle exec fastlane android upload", + "release:android:ship": "bundle exec fastlane android release", + "release:ios:upload": "bundle exec fastlane ios upload", + "release:ios:ship": "bundle exec fastlane ios release", "ota:preflight:staging": "node scripts/ota-publish.mjs staging --preflight", "ota:preflight:production": "node scripts/ota-publish.mjs production --preflight", "ota:publish:staging": "node scripts/ota-publish.mjs staging", diff --git a/docs/mobile-ota.md b/docs/mobile-ota.md index 76ce7c4..ab052ab 100644 --- a/docs/mobile-ota.md +++ b/docs/mobile-ota.md @@ -6,9 +6,10 @@ against a self-hosted XPREM update server (formerly `expo-open-ota`) at protocol, so the client is stock `expo-updates`; only the endpoint, the request headers, and the code-signing certificate are Sauci specific. -Store releases still follow [Mobile release process](releasing.md). OTA never -replaces a store build; it only refreshes the JavaScript bundle and assets of an -already installed build. +Store releases still follow [Mobile release process](releasing.md), which opens +with the OTA-vs-store-release decision rule. OTA never replaces a store build; +it only refreshes the JavaScript bundle and assets of an already installed +build. ## Status diff --git a/docs/releasing.md b/docs/releasing.md index cec20a2..484a0b8 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,28 +1,45 @@ -# Releasing Sauci Mobile App - -This guide covers local release builds for Android and iOS. - -JavaScript-only changes can also ship over the air between store releases. See -[Mobile over-the-air updates](mobile-ota.md). Anything that changes native code -still requires a store build with a bumped `expo.version`, because the OTA -explicit runtime version is synchronized with the app version. - -## Prerequisites - -- **Java JDK 17** - Required for Android Gradle builds -- **Android Studio** - For SDK and build tools (including Android SDK/NDK) -- **Xcode** - For iOS builds (macOS only) -- **EAS CLI** - `npm install -g eas-cli` -- **Expo account** - Run `eas login` to authenticate - ---- - -## EAS Build Local (Recommended) - -The `--local` flag runs the EAS build process on your machine instead of Expo's cloud servers. This automatically handles signing credentials and produces the same output as cloud builds. - -### Quick Start - +# Releasing Sauci Mobile App + +This guide covers local release builds for Android and iOS. + +## OTA update or full store release? + +Decide the delivery mechanism first — they are different pipelines: + +- **OTA update** ([Mobile over-the-air updates](mobile-ota.md)) ships only the + JavaScript bundle and assets to already-installed builds. No store review, + live in minutes, rolled back by repointing the XPREM channel. Valid only for + changes that leave the native binary untouched (no native code, native + dependencies, `ios/`/`android/` project changes, permissions, entitlements, + Expo SDK, or `expo.version` bump). OTA reaches only installs of the current + `expo.version`, because the runtime version is synchronized with the app + version. +- **Full store release** (this document) builds a new signed binary and ships + it through App Store Connect and Google Play, including store review. It is + required for anything OTA cannot carry. Rollback is coarser: halt a Play + staged rollout, pause an iOS phased release, or supersede with the next + version (an OTA update can often patch a bad JS-level regression in a live + store build immediately). + +Prefer OTA when the change qualifies, and always report which mechanism was +used. + +## Prerequisites + +- **Java JDK 17** - Required for Android Gradle builds +- **Android Studio** - For SDK and build tools (including Android SDK/NDK) +- **Xcode** - For iOS builds (macOS only) +- **EAS CLI** - `npm install -g eas-cli` +- **Expo account** - Run `eas login` to authenticate + +--- + +## EAS Build Local (Recommended) + +The `--local` flag runs the EAS build process on your machine instead of Expo's cloud servers. This automatically handles signing credentials and produces the same output as cloud builds. + +### Quick Start + ```bash cd apps/mobile @@ -35,49 +52,62 @@ npm run release:ios Output files are placed in the current directory by default. -These scripts create signed artifacts only. They do not upload or submit them. -An explicit request for a mobile release, upload, or store-submission workflow -authorizes the requested store upload within its stated platform and destination -scope. Building an artifact alone does not authorize an upload. +These scripts create signed artifacts only. An explicit request for a mobile +release authorizes the complete pipeline for the stated platforms: build, +verification, store upload, review submission, and rollout — including full +production rollout. Rollout is recoverable (staged-rollout halt, phased-release +pause, OTA supersession, or the next version), so agents should execute the +whole requested pipeline and report, rather than pausing for re-confirmation. A +request that explicitly narrows scope ("upload only", "TestFlight only", a named +track) still bounds the work. -## Submit a local artifact with EAS +## Ship a verified artifact -The production submission credentials are managed by EAS. Never add App Store -Connect `.p8` files, Google service-account JSON files, or local credential paths -to the repository or `eas.json`. +Credentials are never committed: production submission credentials are managed +by EAS, and the Fastlane iOS lanes read an App Store Connect API key from the +environment (`ASC_KEY_ID`, `ASC_ISSUER_ID`, `ASC_KEY_PATH`). Never add `.p8` +files, Google service-account JSON files, or local credential paths to the +repository or `eas.json`. -Submit only an independently verified artifact by its exact absolute path: +Ship only an independently verified artifact by its exact absolute path. Before +upload, verify and report the platform, exact path, bundle/package identifier, +version, build number, signing identity, SHA-256 digest, and destination — then +proceed through upload, review submission, and rollout without asking for a +second confirmation. + +### Full release (upload + review submission + rollout) ```bash cd apps/mobile -# Uploads an Android internal-track draft; it does not roll out the release. +# Android: commits the release on the production track. Play review runs +# automatically and rollout proceeds on approval. +bundle exec fastlane android release aab:/absolute/path/to/sauci.aab +# Optional: track:beta, rollout:0.1 (10% staged rollout) + +# iOS: uploads, creates the App Store version, submits for App Review, and +# releases automatically on approval. +bundle exec fastlane ios release ipa:/absolute/path/to/sauci.ipa release_notes:"What's new..." +# Optional: phased_release:true (7-day phased release), automatic_release:false +``` + +### Upload only (when the request stops short of review) + +```bash +cd apps/mobile + +# Android internal-track draft; does not roll out the release. eas submit --platform android --profile production --path /absolute/path/to/sauci.aab --wait -# Uploads to App Store Connect/TestFlight; it does not submit for App Review. +# App Store Connect/TestFlight; does not submit for App Review. eas submit --platform ios --profile production --path /absolute/path/to/sauci.ipa --wait ``` -Agents must not upload an unverified artifact, whether they use EAS, Fastlane, -Xcode Organizer, or a store console. Before upload, they must verify and report -the platform, exact path, bundle/package identifier, version, build number, -signing identity, SHA-256 digest, and destination. Once those details match the -user's stated scope, proceed without asking for a second confirmation. - -Before an iOS upload, inspect App Store Connect for automatic TestFlight -distribution. If upload will add the build to existing groups or testers, report -them before proceeding. If the effect cannot be established, stop. Changing -automatic-distribution settings is a separate action and requires its own -authorization. - -Do not use `--latest` for a local release. Before upload, verify the artifact's -package or bundle identifier, public version, build number, signature, and -SHA-256 digest. After upload, read back the same identity and version from Google -Play Console or App Store Connect. Public rollout, App Review submission, and -additional TestFlight group distribution remain separate, explicitly authorized -actions unless the original request expressly includes them. If an upload command -returns an ambiguous result, inspect EAS and the store first; do not retry unless -the user explicitly authorizes another upload. +Do not use `--latest`; always pass the exact verified path. After upload or +submission, read back the resulting state from Google Play Console or App Store +Connect (track/rollout status, App Store version state) and report it. If a +command returns an ambiguous result, inspect EAS and the store first; do not +retry blind. ### Source integrity @@ -100,229 +130,231 @@ Android release manifests must not contain `READ_MEDIA_IMAGES` or broad read access. After any Expo, media-library, image-picker, or native Android change, run `:app:processReleaseMainManifest` and inspect the merged release manifest before building an artifact for Google Play. - -### Building an APK (for Testing) - -The production profile creates an AAB (Android App Bundle), which can't be directly installed on devices. To build an APK for testing: - -```bash -# Use the preview profile (configured for APK output) -eas build --platform android --profile preview --local -``` - -Or add a custom profile to `eas.json`: - -```json -{ - "build": { - "local-apk": { - "android": { - "buildType": "apk" - } - } - } -} -``` - -Then run: -```bash -eas build --platform android --profile local-apk --local -``` - -### Installing the APK - -```bash -# Via ADB (device connected via USB) -adb install ./build-*.apk - -# Or drag-and-drop onto Android Emulator -``` - -### Build Profiles - -| Profile | Output | Use Case | -|---------|--------|----------| -| `production` | .aab | Play Store / App Store submission | -| `preview` | .apk | Internal testing, device installation | -| `development` | .apk | Dev client with hot reload | - -### Environment Variables - -Useful for debugging build issues: - -| Variable | Purpose | -|----------|---------| -| `EAS_LOCAL_BUILD_SKIP_CLEANUP=1` | Keep build directory after completion | -| `EAS_LOCAL_BUILD_WORKINGDIR=/path` | Custom build directory (default: /tmp) | -| `EAS_LOCAL_BUILD_ARTIFACTS_DIR=/path` | Output directory for build artifacts | - -Example with debugging enabled: -```bash -EAS_LOCAL_BUILD_SKIP_CLEANUP=1 \ -EAS_LOCAL_BUILD_WORKINGDIR=~/eas-builds \ -eas build --platform android --profile production --local -``` - -### Limitations of Local Builds - -- **Single platform only** - Can't use `--platform all` -- **Software versions ignored** - `node`, `yarn`, `ndk`, `image` fields in eas.json are ignored -- **No caching** - Each build starts fresh -- **Secret env vars not available** - Set them in your local environment instead -- **macOS/Linux only** - Windows requires WSL (unsupported) - -### Troubleshooting Local Builds - -**Java version mismatch:** -```bash -# Check Java version (should be 17) -java -version - -# On macOS, switch Java version -export JAVA_HOME=$(/usr/libexec/java_home -v 17) -``` - -**Gradle daemon issues:** -```bash -cd android && ./gradlew --stop && cd .. -``` - + +### Building an APK (for Testing) + +The production profile creates an AAB (Android App Bundle), which can't be directly installed on devices. To build an APK for testing: + +```bash +# Use the preview profile (configured for APK output) +eas build --platform android --profile preview --local +``` + +Or add a custom profile to `eas.json`: + +```json +{ + "build": { + "local-apk": { + "android": { + "buildType": "apk" + } + } + } +} +``` + +Then run: +```bash +eas build --platform android --profile local-apk --local +``` + +### Installing the APK + +```bash +# Via ADB (device connected via USB) +adb install ./build-*.apk + +# Or drag-and-drop onto Android Emulator +``` + +### Build Profiles + +| Profile | Output | Use Case | +|---------|--------|----------| +| `production` | .aab | Play Store / App Store submission | +| `preview` | .apk | Internal testing, device installation | +| `development` | .apk | Dev client with hot reload | + +### Environment Variables + +Useful for debugging build issues: + +| Variable | Purpose | +|----------|---------| +| `EAS_LOCAL_BUILD_SKIP_CLEANUP=1` | Keep build directory after completion | +| `EAS_LOCAL_BUILD_WORKINGDIR=/path` | Custom build directory (default: /tmp) | +| `EAS_LOCAL_BUILD_ARTIFACTS_DIR=/path` | Output directory for build artifacts | + +Example with debugging enabled: +```bash +EAS_LOCAL_BUILD_SKIP_CLEANUP=1 \ +EAS_LOCAL_BUILD_WORKINGDIR=~/eas-builds \ +eas build --platform android --profile production --local +``` + +### Limitations of Local Builds + +- **Single platform only** - Can't use `--platform all` +- **Software versions ignored** - `node`, `yarn`, `ndk`, `image` fields in eas.json are ignored +- **No caching** - Each build starts fresh +- **Secret env vars not available** - Set them in your local environment instead +- **macOS/Linux only** - Windows requires WSL (unsupported) + +### Troubleshooting Local Builds + +**Java version mismatch:** +```bash +# Check Java version (should be 17) +java -version + +# On macOS, switch Java version +export JAVA_HOME=$(/usr/libexec/java_home -v 17) +``` + +**Gradle daemon issues:** +```bash +cd android && ./gradlew --stop && cd .. +``` + **Native regeneration after dependency changes:** Treat `expo prebuild` as a source change, not a release step. Run it separately only when native dependencies intentionally changed, review the full native diff, verify it, and commit it before starting the release from a clean worktree. - -**Out of memory:** -```bash -# Increase Gradle memory -echo "org.gradle.jvmargs=-Xmx4g" >> android/gradle.properties -``` - ---- - -## Manual Android Release Build - -### Step 1: Get the Production Keystore - -The production keystore is stored in EAS. Download it: - -```bash -cd apps/mobile -eas credentials --platform android -``` - -Select: -1. `production` -2. `Keystore` -3. `Download Keystore` - -Save the keystore file and note the **password** and **alias** shown. - -### Step 2: Configure Signing - -Ensure `android/gradle.properties` has the keystore configuration: - -```properties -MYAPP_UPLOAD_STORE_FILE=sauci-release.keystore -MYAPP_UPLOAD_KEY_ALIAS= -MYAPP_UPLOAD_STORE_PASSWORD= -MYAPP_UPLOAD_KEY_PASSWORD= -``` - -**Important:** Never commit these credentials to git. - + +**Out of memory:** +```bash +# Increase Gradle memory +echo "org.gradle.jvmargs=-Xmx4g" >> android/gradle.properties +``` + +--- + +## Manual Android Release Build + +### Step 1: Get the Production Keystore + +The production keystore is stored in EAS. Download it: + +```bash +cd apps/mobile +eas credentials --platform android +``` + +Select: +1. `production` +2. `Keystore` +3. `Download Keystore` + +Save the keystore file and note the **password** and **alias** shown. + +### Step 2: Configure Signing + +Ensure `android/gradle.properties` has the keystore configuration: + +```properties +MYAPP_UPLOAD_STORE_FILE=sauci-release.keystore +MYAPP_UPLOAD_KEY_ALIAS= +MYAPP_UPLOAD_STORE_PASSWORD= +MYAPP_UPLOAD_KEY_PASSWORD= +``` + +**Important:** Never commit these credentials to git. + ### Step 3: Confirm tracked native sources The committed `android/` project is the release source. If native dependencies changed, regenerate and review that project as a separate change before release. - -### Step 4: Build the Release Bundle - -```bash -cd android -./gradlew bundleRelease -``` - -The AAB file will be at: -``` -android/app/build/outputs/bundle/release/app-release.aab -``` - + +### Step 4: Build the Release Bundle + +```bash +cd android +./gradlew bundleRelease +``` + +The AAB file will be at: +``` +android/app/build/outputs/bundle/release/app-release.aab +``` + ### Step 5: Upload to Play Store -Apply the upload execution policy above before opening or changing the release. -Uploading the AAB and starting any rollout are separate actions unless the -original request expressly includes the rollout. +Apply the artifact-verification policy above before opening or changing the +release. Prefer the Fastlane `android release` lane; when using the console: 1. Go to [Google Play Console](https://play.google.com/console) 2. Select **Sauci** app -3. Go to **Internal testing**, unless the user authorized a different exact track +3. Go to the track named in the request (**Production** for a release request) 4. **Create new release** 5. Upload the `.aab` file -6. Add release notes, save the release as a draft, and read it back +6. Add release notes and roll out the release — Play review runs automatically + and the rollout proceeds on approval +7. Read back the track and rollout state + +A release request includes review submission and rollout; a bad rollout is +halted from the same release page or superseded by the next version. + +### Verifying Keystore Fingerprint + +To verify you're using the correct keystore: + +```bash +keytool -list -v -keystore android/sauci-release.keystore -storepass +``` -Do not send the draft for review or start a rollout unless the original request -expressly includes that action. - -### Verifying Keystore Fingerprint - -To verify you're using the correct keystore: - -```bash -keytool -list -v -keystore android/sauci-release.keystore -storepass -``` - Expected EAS production upload-key SHA1: `F0:60:26:C7:54:B0:46:F2:80:FE:AC:43:8A:69:10:BD:9A:62:B4:59` - ---- - -## iOS Release Build - + +--- + +## iOS Release Build + ### Step 1: Confirm tracked native sources The committed `ios/` project is the release source. If native dependencies changed, regenerate and review that project as a separate change before release. - -### Step 2: Install Pods - -```bash -cd ios -pod install -cd .. -``` - -### Step 3: Build Archive in Xcode - -1. Open `ios/Sauci.xcworkspace` in Xcode -2. Select **Any iOS Device (arm64)** as the build target -3. Go to **Product** → **Archive** -4. Wait for the build to complete - + +### Step 2: Install Pods + +```bash +cd ios +pod install +cd .. +``` + +### Step 3: Build Archive in Xcode + +1. Open `ios/Sauci.xcworkspace` in Xcode +2. Select **Any iOS Device (arm64)** as the build target +3. Go to **Product** → **Archive** +4. Wait for the build to complete + ### Step 4: Upload to App Store -Apply the upload execution policy above before using Xcode Organizer. Report any -verified automatic TestFlight distribution before uploading. +Apply the artifact-verification policy above before using Xcode Organizer. 1. In Xcode Organizer (Window → Organizer), select the archive -2. Click **Distribute App** -3. Select **App Store Connect** → **Upload** -4. Follow the prompts to upload - +2. Click **Distribute App** +3. Select **App Store Connect** → **Upload** +4. Follow the prompts to upload + ### Step 5: Submit in App Store Connect -Submitting for App Review is not authorized by an upload request unless the -original request expressly includes App Review submission. +A release request includes App Review submission and release on approval. Prefer +the Fastlane `ios release` lane, which does all of this in one step; manually: 1. Go to [App Store Connect](https://appstoreconnect.apple.com) -2. Select **Sauci** -3. Create a new version or select the uploaded build -4. Fill in release notes and submit for review - ---- - -## Version Management - +2. Select **Sauci** +3. Create a new version or select the uploaded build +4. Fill in release notes and submit for review, with automatic release on + approval unless the request says otherwise +5. Read back the version state and report it + +--- + +## Version Management + Version is managed locally by EAS with `appVersionSource: "local"` in `apps/mobile/eas.json`. Production builds auto-increment the platform build number and synchronize the tracked native version file. For a public-version @@ -334,48 +366,48 @@ checked-in iOS and Android public/runtime values together. node .codex/skills/generate-release/scripts/set_version.mjs --version 1.0.7 node .codex/skills/generate-release/scripts/set_version.mjs --check ``` - + To inspect or deliberately set versions: - -```bash -# Check current version -eas build:version:get - -# Set version manually -eas build:version:set --platform android --version 1.0.1 -eas build:version:set --platform ios --version 1.0.1 -``` - + +```bash +# Check current version +eas build:version:get + +# Set version manually +eas build:version:set --platform android --version 1.0.1 +eas build:version:set --platform ios --version 1.0.1 +``` + The user-facing version remains in `app.json`: - -```json -{ - "expo": { - "version": "1.0.1" - } -} -``` - -And for Android, update `android/app/build.gradle`: - -```gradle -versionCode 2 -versionName "1.0.1" -``` - ---- - -## Troubleshooting - -### Wrong Keystore Error - -If Play Store rejects with "signed with wrong key": - -1. Run `eas credentials --platform android` -2. Download the correct keystore -3. Verify fingerprint matches expected SHA1 -4. Replace your local keystore file - + +```json +{ + "expo": { + "version": "1.0.1" + } +} +``` + +And for Android, update `android/app/build.gradle`: + +```gradle +versionCode 2 +versionName "1.0.1" +``` + +--- + +## Troubleshooting + +### Wrong Keystore Error + +If Play Store rejects with "signed with wrong key": + +1. Run `eas credentials --platform android` +2. Download the correct keystore +3. Verify fingerprint matches expected SHA1 +4. Replace your local keystore file + ### Build Failures After Dependency Changes ```bash @@ -386,8 +418,8 @@ cd android && ./gradlew bundleRelease If native regeneration is genuinely required, do it as a separate reviewed and committed source change before running the release build. - -### iOS Signing Issues - -Ensure your Apple Developer certificates and provisioning profiles are up to date in Xcode: -- **Xcode** → **Settings** → **Accounts** → Download Manual Profiles + +### iOS Signing Issues + +Ensure your Apple Developer certificates and provisioning profiles are up to date in Xcode: +- **Xcode** → **Settings** → **Accounts** → Download Manual Profiles