Continuous Integration and Continuous Deployment (CI/CD) are essential for maintaining code quality and delivering updates efficiently. For React Native specifically, CI/CD is more complex than typical web projects because you are building for two platforms (iOS and Android), each with its own build system, code signing requirements, and store submission process.The payoff is enormous: a well-configured pipeline means every pull request is automatically linted, type-checked, and tested; builds are reproducible across team members; and releasing an update goes from a 2-hour manual process to a single button click. This module covers setting up automated pipelines using GitHub Actions and EAS Build.What You’ll Learn:
GitHub Actions is the most popular CI/CD platform for React Native projects because it offers macOS runners (required for iOS builds), generous free-tier minutes, and tight integration with your repository. The workflow files live in .github/workflows/ and run automatically on push, pull request, or release events.
EAS (Expo Application Services) Build is Expo’s cloud build service. The key advantage: it handles native build complexity (Xcode, Gradle, code signing, provisioning profiles) on remote servers, so your team does not need macOS machines or Android SDK installations to produce production builds. Think of it as “Vercel for mobile apps.”The trade-off is cost (cloud build minutes are not free at scale) and less control over the native build environment. For most teams, especially those using Expo, EAS Build dramatically reduces the operational burden of mobile CI/CD.
Fastlane is a Ruby-based automation tool that wraps the painful parts of iOS and Android deployment: code signing, screenshot generation, metadata management, and store submission. Even if you use EAS Build for the build step, Fastlane is often still useful for the submission step, especially for teams with complex App Store Connect or Google Play Console workflows.
Production apps need at least three environments: development (local testing), staging (QA testing against a staging API), and production (real users). Each environment uses different API URLs, analytics keys, feature flags, and possibly different app icons and names (so testers can have both the staging and production builds installed simultaneously).The challenge in React Native is that environment variables need to flow into both the JavaScript bundle and the native build configuration. The workflow below shows how to create environment-specific .env files and pass them through GitHub Actions environment contexts.
Choosing a CI/CD platform for React Native involves a constraint that web teams do not face: iOS builds require macOS runners. This single requirement eliminates many cheap or self-hosted options and makes cost planning more important.
Teams already on GitHub; flexible workflow authoring
EAS Build
Yes (managed)
30 builds/month (free plan)
10-20 min per platform
Expo projects; teams wanting zero native build config
Bitrise
Yes (dedicated)
90 min/month
12-20 min per platform
Mobile-first teams; extensive mobile step library
CircleCI
Yes (resource class)
30,000 credits/month (~60 min macOS)
15-25 min per platform
Complex workflows; strong caching layer
Codemagic
Yes (managed)
500 min/month
10-18 min per platform
Flutter and React Native; generous free tier
Azure DevOps
Yes (hosted)
1,800 min/month (but limited macOS)
15-25 min per platform
Enterprise teams already on Azure
Decision framework:
Using Expo and want simplicity? EAS Build. It handles code signing, provisioning profiles, and native build toolchains. You trade flexibility for dramatically reduced configuration.
Need full native control? GitHub Actions + Fastlane. You manage everything but can customize every step. Best for bare React Native or heavily customized Expo projects.
Budget-constrained? Codemagic’s free tier is the most generous for mobile. Alternatively, self-host Linux runners for Android (free) and use cloud macOS only for iOS.
Enterprise with compliance requirements? Azure DevOps or self-hosted GitHub runners. You keep build artifacts and secrets within your network.
This is the most consequential CI/CD decision for Expo teams. Here is a detailed comparison:
Aspect
EAS Build
Self-Managed (GitHub Actions + Fastlane)
Setup time
30 minutes
4-8 hours (including code signing)
Code signing
Managed automatically (or use local credentials)
Manual: create certs, export .p12, store as secrets, script keychain setup
iOS builds without Mac
Yes — builds run on EAS servers
No — you must use macOS runners
Build caching
Automatic
Manual: cache node_modules, Pods, Gradle
Cost at scale
$99/month (production plan: 1,000 builds)
GitHub Actions macOS minutes (0.08/min=100/month for 1,250 min)
Debugging build failures
Limited: logs only, no SSH into build machine
Full control: can SSH, add debug steps, inspect artifacts
Custom native code
Supported (config plugins, custom dev client)
Full flexibility
OTA updates
Integrated via eas update
Separate setup required
Vendor lock-in
Medium: EAS-specific config, but can eject
Low: standard tooling
My recommendation: Start with EAS Build. When (and if) you hit its limitations — needing custom native build steps, running builds inside your VPN, or exceeding cost thresholds — migrate to self-managed. Most teams never hit those limits.
This is the single most common CI/CD failure for React Native teams. Code signing involves four pieces that must all align: the signing certificate (.p12), the provisioning profile, the Apple Team ID, and the bundle identifier. If any one is expired, mismatched, or missing, the build fails with a cryptic Xcode error.Common failure scenarios:
Symptom
Cause
Fix
”No signing certificate found”
.p12 not imported into CI keychain
Verify base64 encoding: `base64 -i cert.p12
pbcopy`, re-store in GitHub secrets
”Provisioning profile does not match”
Profile was generated for a different bundle ID or team
Regenerate in Apple Developer Portal matching the exact bundle ID
”Certificate has expired”
Signing cert expired (1-year Apple dev certs)
Generate new cert, update .p12 in CI secrets, regenerate provisioning profile
Build succeeds but IPA rejected by TestFlight
Distribution cert used instead of development, or vice versa
Use “Apple Distribution” cert for App Store/TestFlight, “Apple Development” for ad-hoc
Prevention strategy: Use Fastlane Match, which stores certs and profiles in a private Git repo or cloud storage (S3, GCS). When certs rotate, update once in Match, and all CI pipelines pick up the change automatically.
If you lose your Android upload keystore, you cannot publish updates to your existing Play Store listing. Google introduced Play App Signing to mitigate this (Google holds the actual signing key; you hold an upload key), but many teams set up their app before this existed or opted out.Prevention: Store your keystore file in a secure, versioned location (encrypted S3 bucket, 1Password vault). Never rely solely on a CI/CD secret — if the CI provider has an outage, you need to be able to sign locally.
Stale caches cause the most insidious CI failures: a build that worked yesterday fails today with a “module not found” error, even though no code changed. This happens when a transitive dependency publishes a new version that is incompatible with your lockfile’s resolution, and the cache serves a mix of old and new packages.
# Defensive caching: include lockfile hash in cache key- name: Cache node_modules uses: actions/cache@v4 with: path: node_modules # If package-lock.json changes, cache is busted key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json') }} # Do NOT use restore-keys with partial match -- this causes stale deps # restore-keys: node-modules-${{ runner.os }}- # <-- DON'T do this- name: Cache CocoaPods uses: actions/cache@v4 with: path: ios/Pods key: pods-${{ runner.os }}-${{ hashFiles('ios/Podfile.lock') }}
GitHub charges macOS minutes at a 10x multiplier. A 20-minute iOS build consumes 200 minutes of your 2,000-minute free allocation. Teams building iOS on every PR exhaust their free tier in a week.Mitigation strategies:
Run iOS builds only on pushes to main or develop, not on every PR
Use paths filters to skip builds when only docs or config changed
Use EAS Build for iOS (runs on Expo’s infrastructure, not your GitHub minutes)
Cache aggressively to reduce build time (every minute saved is 10x more valuable on macOS)