mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 10:19:43 +00:00
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:
1 parent
8bf93f372b
commit
2b05e50312
9 files changed
+490
-330
No files matched your search
@@ -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.
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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,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")
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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
@@ -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
|
||||||
Reference in new issue
Block a user