Skip to content

React Native Build

React Native allows you to build cross-platform iOS and Android apps using JavaScript and React. This recipe shows how to install dependencies, manage native code, compile your app for both platforms, and sign for distribution.

This is the simplest multi-job pipeline that builds both iOS and Android:

name: React Native Build
platform: react-native
environment:
node: lts
xcode: "16.4"
android_sdk: 34
triggers:
- push
- pull_request
jobs:
build-ios:
name: Build iOS
steps:
- name: Install node modules
run: npm install
- name: Install pods
run: |
cd ios
pod install
- name: Build
run: |
cd ios
xcodebuild -workspace App.xcworkspace -scheme App build
build-android:
name: Build Android
steps:
- name: Install node modules
run: npm install
- name: Build
run: |
cd android
./gradlew assembleRelease

Here’s a more complete example with shared tests and signed release builds:

name: React Native CI/CD
platform: react-native
environment:
node: "20.11" # Explicit version for consistency
xcode: "16.4"
android_sdk: 34
ruby: "3.2" # Optional: for Bundler/CocoaPods
triggers:
- push
- pull_request
jobs:
test:
name: Tests & Lint
steps:
- name: Install node modules
run: npm install
- name: Lint
run: npm run lint
- name: Type check
run: npm run type-check
- name: Run tests
run: npm test
build-ios:
name: Build iOS (Signed)
needs: [test]
steps:
- name: Install node modules
run: npm install
- name: Install pods
run: |
cd ios
pod install
- name: Build for release
run: |
cd ios
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-derivedDataPath build \
build
artifacts:
- ios/build/**/*.app
- ios/build/**/*.dSYM
build-android:
name: Build Android (Signed)
needs: [test]
steps:
- name: Install node modules
run: npm install
- name: Build release bundle
run: |
cd android
./gradlew bundleRelease
artifacts:
- android/app/build/outputs/bundle/release/**/*.aab

Key points:

  • environment.node: "20.11" — Explicit Node version; use lts, latest, or specific semver
  • environment.ruby: "3.2" — Optional; useful if using Ruby-based tools (Bundler, CocoaPods with complex dependencies)
  • needs: [test] — Both iOS and Android builds wait for tests to pass
  • Branch gating — To run signed builds only on main, set triggers: [{ event: push, branches: [main] }] or use step-level if: success() conditions
  • Dependency caching — npm cache is shared across all jobs; subsequent npm install calls are nearly instant
  • Metro cache — Metro bundler cache at node_modules/.cache/metro is automatically cached per platform and branch

For monorepos where the app is in a subdirectory:

name: React Native Monorepo
platform: react-native
environment:
node: lts
xcode: "16.4"
android_sdk: 34
triggers:
- push
jobs:
build-ios:
name: Build iOS
steps:
- name: Install workspace dependencies
run: yarn install
working_directory: .
- name: Install pods
run: pod install
working_directory: apps/mobile/ios
- name: Build
run: xcodebuild -workspace App.xcworkspace -scheme App build
working_directory: apps/mobile/ios
build-android:
name: Build Android
steps:
- name: Install workspace dependencies
run: yarn install
working_directory: .
- name: Build
run: ./gradlew assembleRelease
working_directory: apps/mobile/android

If your React Native app depends on native modules requiring compilation (e.g., react-native-reanimated, react-native-skia):

name: React Native with Native Modules
platform: react-native
environment:
node: lts
xcode: "16.4"
android_sdk: 34
triggers:
- push
jobs:
build-ios:
name: Build iOS
steps:
- name: Install node modules
run: npm install
- name: Install pods (with native modules)
run: |
cd ios
pod install
- name: Build
run: |
cd ios
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
build
artifacts:
- ios/build/**/*.app
build-android:
name: Build Android
steps:
- name: Install node modules
run: npm install
- name: Build
run: |
cd android
./gradlew bundleRelease
artifacts:
- android/app/build/outputs/bundle/release/**/*.aab

CocoaPods and Gradle both support transparent native module compilation. Both dependency caches (pods and gradle) are managed automatically.

For Expo with the bare React Native workflow:

name: Expo Bare Build
platform: react-native
environment:
node: lts
xcode: "16.4"
android_sdk: 34
triggers:
- push
jobs:
build-ios:
name: Build iOS via EAS
steps:
- name: Install dependencies
run: npm install
- name: Build with EAS
run: npx eas build --platform ios --non-interactive
build-android:
name: Build Android via EAS
steps:
- name: Install dependencies
run: npm install
- name: Build with EAS
run: npx eas build --platform android --non-interactive

For Expo projects, use eas build commands directly (no eas-cli setup needed—it’s installed via npm).

Metro (React Native’s bundler) cache is automatically cached between builds at node_modules/.cache/metro. The cache key includes the platform and branch, so:

  • main branch iOS builds share one Metro cache
  • feature/x branch iOS builds share a different cache
  • Android caches separately from iOS

No additional configuration needed—just use npm install as normal, and Metro will use cached artifacts on subsequent builds.

To force a clean Metro build:

- name: Clean Metro cache
run: rm -rf node_modules/.cache/metro
- name: Build
run: npm install && cd ios && xcodebuild -workspace App.xcworkspace -scheme App build
  • npm install: Installs all Node.js dependencies listed in package.json, including React Native itself
  • pod install: Resolves native iOS dependencies in ios/Podfile (required even for React Native)
  • ./gradlew: Builds Android via Gradle; RunnerHub caches gradle wrappers and build outputs
  • working_directory: Use for monorepos to run commands in subdirectories
  • Multi-job pattern: Separate iOS and Android builds allow parallel execution and clear logs
  • Dependency caching: npm cache is shared across all jobs in the pipeline
  • Conditional builds: Use triggers: [{ event: push, branches: [main] }] to gate signed releases to main, or use step-level if: conditions

Pod conflicts: If you encounter pod version mismatches, try:

- name: Update pod repo
run: |
cd ios
pod repo update
pod install --repo-update

Gradle build failures: Clear gradle cache:

- name: Clean Gradle
run: |
cd android
./gradlew clean
./gradlew assembleRelease

Node version issues: If a native module requires a specific Node version, pin it explicitly:

environment:
node: "18.20.3" # Exact version

Metro issues: If Metro bundler seems stale, clear the cache manually:

- name: Clean Metro
run: rm -rf node_modules/.cache/metro && npm install