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
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.
+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,
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
+66 -26
View File
@@ -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:<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,
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:<absolute path>`
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:<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
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.
+1
View File
@@ -11,5 +11,6 @@ expo-env.d.ts
*.keystore
# Fastlane
fastlane-key.json
fastlane/.env
fastlane/report.xml
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__))
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|
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
+3
View File
@@ -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",
+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
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
+326 -294
View File
@@ -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=<alias from EAS>
MYAPP_UPLOAD_STORE_PASSWORD=<password from EAS>
MYAPP_UPLOAD_KEY_PASSWORD=<password from EAS>
```
**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=<alias from EAS>
MYAPP_UPLOAD_STORE_PASSWORD=<password from EAS>
MYAPP_UPLOAD_KEY_PASSWORD=<password from EAS>
```
**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 <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`
---
## 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