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

+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