| name | rn-harness-build |
| description | Phase 8 โ EAS Build for iOS and Android, plus EAS Update (OTA) configuration. |
| allowed-tools | ["Read","Write","Edit","Bash","Glob","Grep","AskUserQuestion"] |
rn-harness-build โ Phase 8: EAS Build
Build iOS/Android binaries via EAS Build and configure EAS Update for OTA.
EAS ๋ฌด๋ฃ ํ๋์ด ๊ฝ ์ฐผ๊ฑฐ๋(3-ํ๋ก์ ํธ ์ ํ) EAS๋ฅผ ์ฐ์ง ์์ผ๋ ค๋ฉด โ rn-harness-build-local (๋ก์ปฌ Gradle + Xcode/fastlane, ์์ฒด ํธ์คํ
OTA)์ ๋์ ์ฌ์ฉํ์ธ์.
Trigger
Called by the orchestrator as Phase 8.
Input
- Project code (AdMob integration complete)
docs/harness/config.md
Prerequisites
eas-cli installed (npm install -g eas-cli)
- Expo account logged in (
eas login)
Process
Step 1: EAS Init
eas init
Step 2: eas.json Setup
Check if eas.json exists. If not, create with all required config:
{
"cli": {
"version": ">= 15.0.0",
"appVersionSource": "remote"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"channel": "development"
},
"preview": {
"distribution": "internal",
"channel": "preview"
},
"production": {
"autoIncrement": true,
"channel": "production",
"env": {
"GRADLE_OPTS": "-Dorg.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g"
}
}
},
"submit": {
"production": {
"ios": {
"ascAppId": "",
"appleTeamId": ""
},
"android": {
"serviceAccountKeyPath": "",
"track": "internal"
}
}
}
}
CRITICAL: Android GRADLE_OPTS
The production.env.GRADLE_OPTS setting is mandatory. Without it, Android local builds frequently fail with OutOfMemoryError or Metaspace errors during Gradle compilation:
"env": {
"GRADLE_OPTS": "-Dorg.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g"
}
This allocates 4GB heap + 1GB metaspace for the Gradle JVM. Adjust if your machine has less RAM (minimum -Xmx2g).
CRITICAL: Build profile channel binding
Each build profile must declare a channel. A build only receives OTA updates from the channel it was built with โ a build with no channel is subscribed to nothing, so eas update will never reach it no matter how many channels you create later. This binding (in eas.json), not eas channel:create alone, is what actually wires OTA to the app.
Channel strategy:
| Profile | Channel | Purpose |
|---|
development | development | Dev client / internal iteration |
preview | preview | Internal QA / TestFlight / internal track |
production | production | Store releases (live OTA to users) |
Keeping preview and production on separate channels means QA updates never leak to live users, and you can promote a tested update from preview โ production (eas update:republish) instead of rebuilding.
Step 3: EAS Update Setup (OTA)
Configure EAS Update for over-the-air JS bundle updates (no store re-submission needed):
eas update:configure
This adds to app.config.ts:
updates: {
url: "https://u.expo.dev/[PROJECT_ID]",
},
runtimeVersion: {
policy: "appVersion",
},
And adds expo-updates to plugins:
plugins: [
"expo-router",
"expo-updates",
],
Install the dependency:
npx expo install expo-updates
Why OTA matters:
- Fix bugs without store review (JS-only changes)
- A/B test features
- Instant rollback on bad releases
- Store review only needed for native code changes
Step 4: app.config.ts Verification
Verify all required settings:
name, slug, version
ios.bundleIdentifier (from config.md)
android.package (same as iOS)
android.versionCode
- AdMob plugin config
- EAS Update config (
updates.url, runtimeVersion)
Step 4.5: Android Signing Keystore (ASK FIRST โ never auto-generate silently)
By default eas build auto-generates a brand-new Android keystore on the first build. NEVER let this happen silently: if the app is already live on Google Play, a new keystore produces an AAB that Play rejects (signed with the wrong key), and the original key cannot be recovered. Always ask the user first.
AskUserQuestion โ "์๋๋ก์ด๋ ์ฑ ์๋ช
keystore๋ฅผ ์ด๋ป๊ฒ ํ ๊น์?":
| ์ ํ์ง | ์๋ฏธ |
|---|
| ์๋ก ์์ฑ (์ ๊ท ์ฑ) | EAS๊ฐ keystore ์๋ ์์ฑ. ์ฒ์ ์ถ์ํ๋ ์ฑ์ผ ๋๋ง. |
| ๊ธฐ์กด keystore ์ฌ์ฉ | ๋ณด์ ํ .jks๋ฅผ EAS์ ์
๋ก๋. ์ด๋ฏธ Play์ ์ถ์๋ ์ฑ์ด๋ฉด ํ์. |
| ์ ๋ชจ๋ฅด๊ฒ ์ | Play Console์ ์ด ์ฑ์ด ์ด๋ฏธ ์์ผ๋ฉด โ ๊ธฐ์กด ์ฌ์ฉ. ์์ ์ ๊ท๋ฉด โ ์๋ก ์์ฑ. |
"๊ธฐ์กด keystore ์ฌ์ฉ" ์ ํ ์:
- ํ์ผ๊ณผ ์๊ฒฉ์ฆ๋ช
์ ๋ฐ๋๋ค (
.jks๋ฅผ credentials/keystore.jks์ ๋ฐฐ์น):
- keystore ํ์ผ ๊ฒฝ๋ก ยท keystore ๋น๋ฐ๋ฒํธ ยท key alias ยท key ๋น๋ฐ๋ฒํธ
- ํ๋ก์ ํธ ๋ฃจํธ์
credentials.json ์์ฑ:
{
"android": {
"keystore": {
"keystorePath": "credentials/keystore.jks",
"keystorePassword": "...",
"keyAlias": "...",
"keyPassword": "..."
}
}
}
.gitignore์ credentials.json, *.jks, *.keystore๊ฐ ํฌํจ๋๋์ง ํ์ธ (๋น๋ฐํค๋ ์ ๋ ์ปค๋ฐ ๊ธ์ง).
- EAS ์๋ฒ์ ์
๋ก๋:
eas credentials --platform android
(๋๋ credentials.json์ด ์์ผ๋ฉด eas build๊ฐ ๋ก์ปฌ ํ์ผ์ ๊ทธ๋๋ก ์ฌ์ฉ โ ์๋ฒ ์
๋ก๋ ์์ด๋ ๋์.)
"์๋ก ์์ฑ" ์ ํ ์: ๋ณ๋ ์์
์์. ์ฒซ ๋น๋์์ EAS๊ฐ ์์ฑํ๋ค. ์์ฑ ํ ๋ฐ๋์ ๋ฐฑ์
ํ๋๋ก ์๋ด (eas credentials โ download). ์ด ํค๋ฅผ ์์ผ๋ฉด ํฅํ ์
๋ฐ์ดํธ ๋ถ๊ฐ.
HARD GATE: ์ด๋ฏธ Play์ ์ถ์๋ ์ฑ์ ์ keystore๋ฅผ ์ฐ๋ฉด = FAIL (Play๊ฐ ์
๋ก๋ ๊ฑฐ๋ถ). ์ฒซ ๋น๋ ์ ์ฌ์ฉ์ ํ์ธ ํ์.
Step 5: Local Build Test (Recommended)
Local build catches errors faster than cloud build:
eas build --local --platform android --profile production
Common Android local build failures and fixes:
| Error | Fix |
|---|
OutOfMemoryError / Metaspace | Increase GRADLE_OPTS in eas.json |
SDK location not found | Set ANDROID_HOME env var |
NDK not found | Install NDK via Android Studio SDK Manager |
minSdkVersion conflict | Check expo-build-properties plugin |
Duplicate class | Run cd android && ./gradlew clean |
eas build --local --platform ios --profile production
Common iOS local build failures:
| Error | Fix |
|---|
No signing certificate | EAS manages this automatically in cloud build |
Pod install failed | cd ios && pod install --repo-update |
Xcode version mismatch | Update Xcode or set image in eas.json |
If local build fails with signing issues, skip to cloud build (EAS handles provisioning automatically).
Step 6: Cloud Build
eas build --platform ios --profile production --non-interactive
eas build --platform android --profile production --non-interactive
Wait for build completion. Record build URLs.
Step 7: Build Result Check
eas build:list --limit 2
- Success โ proceed
- Failure โ analyze error โ fix โ rebuild
Step 8: EAS Update Channel Setup & Verify
Channels are targeted by builds (via the channel in eas.json); branches hold the actual update commits. A channel points at a branch โ by default a channel maps to a branch of the same name, which is the convention we use here.
eas channel:create preview
eas channel:create production
eas channel:list
eas update --branch production --message "Initial release" --non-interactive
eas build:list --limit 1 --platform ios --json --non-interactive | grep -i channel
If eas build:list shows a build with channel: null, the build was created before the channel field was added to eas.json โ rebuild so the binding takes effect, otherwise OTA will never reach it.
Output
docs/harness/handoff/build-result.md:
# EAS Build Result
## iOS
- Profile: production
- Status: [SUCCESS/FAILED]
- Build URL: https://expo.dev/...
- Binary: .ipa
## Android
- Profile: production
- Status: [SUCCESS/FAILED]
- Build URL: https://expo.dev/...
- Binary: .aab
## EAS Update
- Channels: preview, production
- Build channel binding: [production build โ production / null]
- Runtime Version: [version]
- Status: [CONFIGURED/FAILED]
## Build Config
- bundleIdentifier: com.xxx.xxx
- package: com.xxx.xxx
- version: 1.0.0
- buildNumber/versionCode: 1
- GRADLE_OPTS: -Xmx4g -XX:MaxMetaspaceSize=1g
Git commit:
git add eas.json app.config.ts
git commit -m "chore: configure EAS build + update profiles"
State Update
current_phase: screenshot
next_role: rn-harness-screenshot
HARD GATES
eas.json must exist with all profiles
GRADLE_OPTS must be set in production build env
app.config.ts must have bundleIdentifier/package
- Android keystore choice must be confirmed with the user BEFORE first build (never silently auto-generate for an app already on Play)
- EAS Update must be configured (
expo-updates installed, updates.url set)
- Every
eas.json build profile must declare a channel (buildโOTA binding)
production build must report channel: "production" (eas build:list) โ null blocks OTA delivery
- Build failure blocks next Phase
- Build URL must be recorded on success