feat(release): add fastlane ship lanes and end-to-end release pipeline docs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jjbruntonandClaude Fable 5 committed 2026-08-31 11:32:32 +01:00
1 parent 8bf93f372b
commit 2b05e50312
9 files changed
+490 -330

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
name: release-mobile 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. Read `.codex/skills/release-mobile/SKILL.md` completely and follow it.
+6 -6
View File
@@ -14,12 +14,12 @@ before accepting a build.
A request to generate or create a release authorizes the scoped version edits, 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 release notes, local builds, release commits, and one local annotated tag. When
the request also specifies a mobile release, upload, or store-submission the request also asks to ship, upload, submit, or release to the stores, it
workflow, it authorizes the requested store upload within its stated platform authorizes the complete store pipeline for the stated platforms after the exact
and destination scope after the exact artifact has been verified. It does not artifact has been verified: upload, App Review / Google Play review submission,
authorize pushing commits or tags, changing signing credentials, regenerating and rollout, including full production rollout (see the `release-mobile` skill).
native projects, a public rollout, App Review submission, or additional It does not authorize pushing commits or tags, changing signing credentials, or
TestFlight distribution unless the request explicitly includes that action. regenerating native projects unless the request explicitly includes that action.
## Defaults ## Defaults
+66 -26
View File
@@ -1,6 +1,6 @@
--- ---
name: release-mobile 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 # 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 scoped instructions. Confirm target platform and release profile. Run repository
verification plus a native acceptance pass before starting a release build. 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:<env>` 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, 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 and prove `HEAD` matches its upstream. The `generate-release` skill may instead
build a clean, scoped release-preparation commit that descends directly from that 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 so routine release builds must package those checked-in sources. Do not run
`expo prebuild` first; native regeneration is a separate reviewed change. `expo prebuild` first; native regeneration is a separate reviewed change.
Use the package scripts in `apps/mobile/package.json`; they create artifacts only. Use the package scripts in `apps/mobile/package.json` to build artifacts and the
An explicit request for a mobile release, upload, or store-submission workflow Fastlane lanes in `apps/mobile/fastlane/Fastfile` to upload, submit, and roll
authorizes the requested store upload within its stated platform and destination out.
scope. Building an artifact alone does not authorize an upload.
## 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, - the platform, exact artifact path, bundle/package identifier, public version,
build number, signature identity, and SHA-256 digest; build number, signature identity, and SHA-256 digest;
- the exact destination and resulting state: Google Play internal-track draft or - the exact destination and resulting state: which Play track and rollout
App Store Connect/TestFlight processing; status, or App Store Connect version state and whether App Review submission
- that the upload does not authorize a public rollout or App Review submission. and automatic release are included.
For iOS, inspect the current App Store Connect automatic-distribution behavior Once these details match the request, proceed through upload, review
before uploading. If the build would be distributed automatically, report the submission, and rollout without requesting further confirmation.
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 user's stated scope, upload without requesting a - **Android**: `bundle exec fastlane android release aab:<absolute path>`
second confirmation. Run `eas submit` with the explicit commits the release on the production track (pass `track:` to target another
`--platform`, `--profile production`, `--path`, and `--wait` arguments; never track, `rollout:0.1` for a staged rollout). Play review runs automatically;
select `--latest`. If the result is ambiguous, inspect EAS and the store before rollout proceeds on approval. Use the `upload` lane only when the request is
doing anything else; do not retry unless the user explicitly authorizes another explicitly upload-only.
upload. - **iOS**: `bundle exec fastlane ios release ipa:<absolute path>` 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 If an upload or submission command returns an ambiguous result, inspect EAS and
Organizer, and the store consoles. A public rollout, App Review submission, or the store before doing anything else; do not retry blind. After submission, read
additional TestFlight distribution requires separate explicit authorization back the store state (Play Console track status / App Store Connect version
unless it was expressly included in the original request. state) and report it.
Preserve platform signing and local EAS versioning. After a build, verify that Preserve platform signing and local EAS versioning. After a build, verify that
tracked changes are limited to the expected version metadata, then independently 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 app extension as well as the containing app and require matching
`CFBundleShortVersionString` and `CFBundleVersion` values. Treat Xcode archive `CFBundleShortVersionString` and `CFBundleVersion` values. Treat Xcode archive
warnings as diagnostic evidence, not the final artifact state: decide whether to 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, reject or rebuild only after reading the exported IPA itself. Report the
version/build number, upload result when authorized, and store read-back state. artifact, version/build number, upload and submission results, rollout state,
and store read-back state.
+1
View File
@@ -11,5 +11,6 @@ expo-env.d.ts
*.keystore *.keystore
# Fastlane # Fastlane
fastlane-key.json fastlane-key.json
fastlane/.env
fastlane/report.xml fastlane/report.xml
fastlane/README.md fastlane/README.md
+1
View File
@@ -1,2 +1,3 @@
json_key_file(ENV["GOOGLE_PLAY_KEY_FILE"] || File.expand_path("../fastlane-key.json", __dir__)) json_key_file(ENV["GOOGLE_PLAY_KEY_FILE"] || File.expand_path("../fastlane-key.json", __dir__))
package_name("com.sauci.app") package_name("com.sauci.app")
app_identifier("com.sauci.app")
+82
View File
@@ -45,4 +45,86 @@ platform :android do
lane :upload_beta do |options| lane :upload_beta do |options|
upload(options.merge(track: "beta")) upload(options.merge(track: "beta"))
end 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 end
+3
View File
@@ -19,6 +19,9 @@
"release:android": "dotenv -e .env -- eas build --platform android --profile production --local", "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:preview": "dotenv -e .env -- eas build --platform android --profile preview --local",
"release:android:upload": "bundle exec fastlane android upload", "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:staging": "node scripts/ota-publish.mjs staging --preflight",
"ota:preflight:production": "node scripts/ota-publish.mjs production --preflight", "ota:preflight:production": "node scripts/ota-publish.mjs production --preflight",
"ota:publish:staging": "node scripts/ota-publish.mjs staging", "ota:publish:staging": "node scripts/ota-publish.mjs staging",
+4 -3
View File
@@ -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 protocol, so the client is stock `expo-updates`; only the endpoint, the request
headers, and the code-signing certificate are Sauci specific. headers, and the code-signing certificate are Sauci specific.
Store releases still follow [Mobile release process](releasing.md). OTA never Store releases still follow [Mobile release process](releasing.md), which opens
replaces a store build; it only refreshes the JavaScript bundle and assets of an with the OTA-vs-store-release decision rule. OTA never replaces a store build;
already installed build. it only refreshes the JavaScript bundle and assets of an already installed
build.
## Status ## Status
+326 -294
View File
@@ -1,28 +1,45 @@
# Releasing Sauci Mobile App # Releasing Sauci Mobile App
This guide covers local release builds for Android and iOS. This guide covers local release builds for Android and iOS.
JavaScript-only changes can also ship over the air between store releases. See ## OTA update or full store release?
[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 Decide the delivery mechanism first — they are different pipelines:
explicit runtime version is synchronized with the app version.
- **OTA update** ([Mobile over-the-air updates](mobile-ota.md)) ships only the
## Prerequisites JavaScript bundle and assets to already-installed builds. No store review,
live in minutes, rolled back by repointing the XPREM channel. Valid only for
- **Java JDK 17** - Required for Android Gradle builds changes that leave the native binary untouched (no native code, native
- **Android Studio** - For SDK and build tools (including Android SDK/NDK) dependencies, `ios/`/`android/` project changes, permissions, entitlements,
- **Xcode** - For iOS builds (macOS only) Expo SDK, or `expo.version` bump). OTA reaches only installs of the current
- **EAS CLI** - `npm install -g eas-cli` `expo.version`, because the runtime version is synchronized with the app
- **Expo account** - Run `eas login` to authenticate 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
## EAS Build Local (Recommended) 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
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. store build immediately).
### Quick Start 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 ```bash
cd apps/mobile cd apps/mobile
@@ -35,49 +52,62 @@ npm run release:ios
Output files are placed in the current directory by default. Output files are placed in the current directory by default.
These scripts create signed artifacts only. They do not upload or submit them. These scripts create signed artifacts only. An explicit request for a mobile
An explicit request for a mobile release, upload, or store-submission workflow release authorizes the complete pipeline for the stated platforms: build,
authorizes the requested store upload within its stated platform and destination verification, store upload, review submission, and rollout — including full
scope. Building an artifact alone does not authorize an upload. 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 Credentials are never committed: production submission credentials are managed
Connect `.p8` files, Google service-account JSON files, or local credential paths by EAS, and the Fastlane iOS lanes read an App Store Connect API key from the
to the repository or `eas.json`. 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 ```bash
cd apps/mobile 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 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 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, Do not use `--latest`; always pass the exact verified path. After upload or
Xcode Organizer, or a store console. Before upload, they must verify and report submission, read back the resulting state from Google Play Console or App Store
the platform, exact path, bundle/package identifier, version, build number, Connect (track/rollout status, App Store version state) and report it. If a
signing identity, SHA-256 digest, and destination. Once those details match the command returns an ambiguous result, inspect EAS and the store first; do not
user's stated scope, proceed without asking for a second confirmation. retry blind.
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.
### Source integrity ### 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 broad read access. After any Expo, media-library, image-picker, or native Android
change, run `:app:processReleaseMainManifest` and inspect the merged release change, run `:app:processReleaseMainManifest` and inspect the merged release
manifest before building an artifact for Google Play. manifest before building an artifact for Google Play.
### Building an APK (for Testing) ### 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: The production profile creates an AAB (Android App Bundle), which can't be directly installed on devices. To build an APK for testing:
```bash ```bash
# Use the preview profile (configured for APK output) # Use the preview profile (configured for APK output)
eas build --platform android --profile preview --local eas build --platform android --profile preview --local
``` ```
Or add a custom profile to `eas.json`: Or add a custom profile to `eas.json`:
```json ```json
{ {
"build": { "build": {
"local-apk": { "local-apk": {
"android": { "android": {
"buildType": "apk" "buildType": "apk"
} }
} }
} }
} }
``` ```
Then run: Then run:
```bash ```bash
eas build --platform android --profile local-apk --local eas build --platform android --profile local-apk --local
``` ```
### Installing the APK ### Installing the APK
```bash ```bash
# Via ADB (device connected via USB) # Via ADB (device connected via USB)
adb install ./build-*.apk adb install ./build-*.apk
# Or drag-and-drop onto Android Emulator # Or drag-and-drop onto Android Emulator
``` ```
### Build Profiles ### Build Profiles
| Profile | Output | Use Case | | Profile | Output | Use Case |
|---------|--------|----------| |---------|--------|----------|
| `production` | .aab | Play Store / App Store submission | | `production` | .aab | Play Store / App Store submission |
| `preview` | .apk | Internal testing, device installation | | `preview` | .apk | Internal testing, device installation |
| `development` | .apk | Dev client with hot reload | | `development` | .apk | Dev client with hot reload |
### Environment Variables ### Environment Variables
Useful for debugging build issues: Useful for debugging build issues:
| Variable | Purpose | | Variable | Purpose |
|----------|---------| |----------|---------|
| `EAS_LOCAL_BUILD_SKIP_CLEANUP=1` | Keep build directory after completion | | `EAS_LOCAL_BUILD_SKIP_CLEANUP=1` | Keep build directory after completion |
| `EAS_LOCAL_BUILD_WORKINGDIR=/path` | Custom build directory (default: /tmp) | | `EAS_LOCAL_BUILD_WORKINGDIR=/path` | Custom build directory (default: /tmp) |
| `EAS_LOCAL_BUILD_ARTIFACTS_DIR=/path` | Output directory for build artifacts | | `EAS_LOCAL_BUILD_ARTIFACTS_DIR=/path` | Output directory for build artifacts |
Example with debugging enabled: Example with debugging enabled:
```bash ```bash
EAS_LOCAL_BUILD_SKIP_CLEANUP=1 \ EAS_LOCAL_BUILD_SKIP_CLEANUP=1 \
EAS_LOCAL_BUILD_WORKINGDIR=~/eas-builds \ EAS_LOCAL_BUILD_WORKINGDIR=~/eas-builds \
eas build --platform android --profile production --local eas build --platform android --profile production --local
``` ```
### Limitations of Local Builds ### Limitations of Local Builds
- **Single platform only** - Can't use `--platform all` - **Single platform only** - Can't use `--platform all`
- **Software versions ignored** - `node`, `yarn`, `ndk`, `image` fields in eas.json are ignored - **Software versions ignored** - `node`, `yarn`, `ndk`, `image` fields in eas.json are ignored
- **No caching** - Each build starts fresh - **No caching** - Each build starts fresh
- **Secret env vars not available** - Set them in your local environment instead - **Secret env vars not available** - Set them in your local environment instead
- **macOS/Linux only** - Windows requires WSL (unsupported) - **macOS/Linux only** - Windows requires WSL (unsupported)
### Troubleshooting Local Builds ### Troubleshooting Local Builds
**Java version mismatch:** **Java version mismatch:**
```bash ```bash
# Check Java version (should be 17) # Check Java version (should be 17)
java -version java -version
# On macOS, switch Java version # On macOS, switch Java version
export JAVA_HOME=$(/usr/libexec/java_home -v 17) export JAVA_HOME=$(/usr/libexec/java_home -v 17)
``` ```
**Gradle daemon issues:** **Gradle daemon issues:**
```bash ```bash
cd android && ./gradlew --stop && cd .. cd android && ./gradlew --stop && cd ..
``` ```
**Native regeneration after dependency changes:** **Native regeneration after dependency changes:**
Treat `expo prebuild` as a source change, not a release step. Run it separately 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, only when native dependencies intentionally changed, review the full native diff,
verify it, and commit it before starting the release from a clean worktree. verify it, and commit it before starting the release from a clean worktree.
**Out of memory:** **Out of memory:**
```bash ```bash
# Increase Gradle memory # Increase Gradle memory
echo "org.gradle.jvmargs=-Xmx4g" >> android/gradle.properties echo "org.gradle.jvmargs=-Xmx4g" >> android/gradle.properties
``` ```
--- ---
## Manual Android Release Build ## Manual Android Release Build
### Step 1: Get the Production Keystore ### Step 1: Get the Production Keystore
The production keystore is stored in EAS. Download it: The production keystore is stored in EAS. Download it:
```bash ```bash
cd apps/mobile cd apps/mobile
eas credentials --platform android eas credentials --platform android
``` ```
Select: Select:
1. `production` 1. `production`
2. `Keystore` 2. `Keystore`
3. `Download Keystore` 3. `Download Keystore`
Save the keystore file and note the **password** and **alias** shown. Save the keystore file and note the **password** and **alias** shown.
### Step 2: Configure Signing ### Step 2: Configure Signing
Ensure `android/gradle.properties` has the keystore configuration: Ensure `android/gradle.properties` has the keystore configuration:
```properties ```properties
MYAPP_UPLOAD_STORE_FILE=sauci-release.keystore MYAPP_UPLOAD_STORE_FILE=sauci-release.keystore
MYAPP_UPLOAD_KEY_ALIAS=<alias from EAS> MYAPP_UPLOAD_KEY_ALIAS=<alias from EAS>
MYAPP_UPLOAD_STORE_PASSWORD=<password from EAS> MYAPP_UPLOAD_STORE_PASSWORD=<password from EAS>
MYAPP_UPLOAD_KEY_PASSWORD=<password from EAS> MYAPP_UPLOAD_KEY_PASSWORD=<password from EAS>
``` ```
**Important:** Never commit these credentials to git. **Important:** Never commit these credentials to git.
### Step 3: Confirm tracked native sources ### Step 3: Confirm tracked native sources
The committed `android/` project is the release source. If native dependencies The committed `android/` project is the release source. If native dependencies
changed, regenerate and review that project as a separate change before release. changed, regenerate and review that project as a separate change before release.
### Step 4: Build the Release Bundle ### Step 4: Build the Release Bundle
```bash ```bash
cd android cd android
./gradlew bundleRelease ./gradlew bundleRelease
``` ```
The AAB file will be at: The AAB file will be at:
``` ```
android/app/build/outputs/bundle/release/app-release.aab android/app/build/outputs/bundle/release/app-release.aab
``` ```
### Step 5: Upload to Play Store ### Step 5: Upload to Play Store
Apply the upload execution policy above before opening or changing the release. Apply the artifact-verification policy above before opening or changing the
Uploading the AAB and starting any rollout are separate actions unless the release. Prefer the Fastlane `android release` lane; when using the console:
original request expressly includes the rollout.
1. Go to [Google Play Console](https://play.google.com/console) 1. Go to [Google Play Console](https://play.google.com/console)
2. Select **Sauci** app 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** 4. **Create new release**
5. Upload the `.aab` file 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 <password>
```
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 <password>
```
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` 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 ### Step 1: Confirm tracked native sources
The committed `ios/` project is the release source. If native dependencies The committed `ios/` project is the release source. If native dependencies
changed, regenerate and review that project as a separate change before release. changed, regenerate and review that project as a separate change before release.
### Step 2: Install Pods ### Step 2: Install Pods
```bash ```bash
cd ios cd ios
pod install pod install
cd .. cd ..
``` ```
### Step 3: Build Archive in Xcode ### Step 3: Build Archive in Xcode
1. Open `ios/Sauci.xcworkspace` in Xcode 1. Open `ios/Sauci.xcworkspace` in Xcode
2. Select **Any iOS Device (arm64)** as the build target 2. Select **Any iOS Device (arm64)** as the build target
3. Go to **Product** → **Archive** 3. Go to **Product** → **Archive**
4. Wait for the build to complete 4. Wait for the build to complete
### Step 4: Upload to App Store ### Step 4: Upload to App Store
Apply the upload execution policy above before using Xcode Organizer. Report any Apply the artifact-verification policy above before using Xcode Organizer.
verified automatic TestFlight distribution before uploading.
1. In Xcode Organizer (Window → Organizer), select the archive 1. In Xcode Organizer (Window → Organizer), select the archive
2. Click **Distribute App** 2. Click **Distribute App**
3. Select **App Store Connect** → **Upload** 3. Select **App Store Connect** → **Upload**
4. Follow the prompts to upload 4. Follow the prompts to upload
### Step 5: Submit in App Store Connect ### Step 5: Submit in App Store Connect
Submitting for App Review is not authorized by an upload request unless the A release request includes App Review submission and release on approval. Prefer
original request expressly includes App Review submission. the Fastlane `ios release` lane, which does all of this in one step; manually:
1. Go to [App Store Connect](https://appstoreconnect.apple.com) 1. Go to [App Store Connect](https://appstoreconnect.apple.com)
2. Select **Sauci** 2. Select **Sauci**
3. Create a new version or select the uploaded build 3. Create a new version or select the uploaded build
4. Fill in release notes and submit for review 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 Management
Version is managed locally by EAS with `appVersionSource: "local"` in Version is managed locally by EAS with `appVersionSource: "local"` in
`apps/mobile/eas.json`. Production builds auto-increment the platform build `apps/mobile/eas.json`. Production builds auto-increment the platform build
number and synchronize the tracked native version file. For a public-version 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 --version 1.0.7
node .codex/skills/generate-release/scripts/set_version.mjs --check node .codex/skills/generate-release/scripts/set_version.mjs --check
``` ```
To inspect or deliberately set versions: To inspect or deliberately set versions:
```bash ```bash
# Check current version # Check current version
eas build:version:get eas build:version:get
# Set version manually # Set version manually
eas build:version:set --platform android --version 1.0.1 eas build:version:set --platform android --version 1.0.1
eas build:version:set --platform ios --version 1.0.1 eas build:version:set --platform ios --version 1.0.1
``` ```
The user-facing version remains in `app.json`: The user-facing version remains in `app.json`:
```json ```json
{ {
"expo": { "expo": {
"version": "1.0.1" "version": "1.0.1"
} }
} }
``` ```
And for Android, update `android/app/build.gradle`: And for Android, update `android/app/build.gradle`:
```gradle ```gradle
versionCode 2 versionCode 2
versionName "1.0.1" versionName "1.0.1"
``` ```
--- ---
## Troubleshooting ## Troubleshooting
### Wrong Keystore Error ### Wrong Keystore Error
If Play Store rejects with "signed with wrong key": If Play Store rejects with "signed with wrong key":
1. Run `eas credentials --platform android` 1. Run `eas credentials --platform android`
2. Download the correct keystore 2. Download the correct keystore
3. Verify fingerprint matches expected SHA1 3. Verify fingerprint matches expected SHA1
4. Replace your local keystore file 4. Replace your local keystore file
### Build Failures After Dependency Changes ### Build Failures After Dependency Changes
```bash ```bash
@@ -386,8 +418,8 @@ cd android && ./gradlew bundleRelease
If native regeneration is genuinely required, do it as a separate reviewed and If native regeneration is genuinely required, do it as a separate reviewed and
committed source change before running the release build. committed source change before running the release build.
### iOS Signing Issues ### iOS Signing Issues
Ensure your Apple Developer certificates and provisioning profiles are up to date in Xcode: Ensure your Apple Developer certificates and provisioning profiles are up to date in Xcode:
- **Xcode** → **Settings** → **Accounts** → Download Manual Profiles - **Xcode** → **Settings** → **Accounts** → Download Manual Profiles