Continuous Integration for React Native Apps with Fastlane and GitHub Actions
Continuous Integration for React Native Apps with Fastlane and GitHub Actions
Building a React Native app is fun, but manually testing, building, and deploying every change can slow you down. Continuous integration (CI) automates these tasks, letting you focus on creating awesome features. In this beginner-friendly guide, we’ll set up a CI pipeline for your React Native app using Fastlane, GitHub Actions, and Expo Application Services (EAS) with Expo SDK 51 and React Native 0.75.4. We’ll automate builds for iOS (Xcode 16+, iOS 18 SDK) and Android (SDK 35), run tests, and deploy OTA updates and store releases. No CI experience is needed, and we’ll explain key terms along the way. For prebuilt CI setups, check out templates at Instamobile or Dopebase.
By the end, you’ll have a fully automated pipeline for your app. Let’s dive in!
Prerequisites
Before we start, ensure you have these tools and accounts:
- Node.js (v20.x or later, LTS recommended): Download from nodejs.org.
- npm (v10.x or later, included with Node.js).
- EAS CLI: For Expo project management.
- Android Studio (2024.3.2 or later): With Android SDK 35 and NDK r25+.
- Xcode (16+): For iOS builds, with iOS 18 SDK (macOS only).
- A code editor like VS Code.
- Git: For version control.
- GitHub Account: For hosting your repository and GitHub Actions.
- Apple Developer Account: For iOS builds ($99/year, developer.apple.com).
- Google Play Developer Account: For Android builds ($25 one-time, play.google.com/console).
- Ruby (3.2 or later): For Fastlane, installed via Homebrew (macOS) or system package manager (Linux).
- macOS: Required for iOS builds.
What is CI/CD? Continuous Integration (CI) automatically builds and tests your code when you push changes, catching bugs early. Continuous Deployment (CD) automates releasing those changes to users, like app stores or OTA updates. It’s like having a tireless assistant for your workflow! Learn more in GitHub’s CI/CD guide.
Ready? Let’s set up your project!
Step 1: Create and Configure Your React Native Project
Let’s create a React Native project optimized for CI with EAS.
-
Install EAS CLI globally:
npm install -g eas-cli
-
Create a new project called
MyApp
:npx create-expo-app MyApp --template blank-typescript
This uses Expo SDK 51, supporting React Native 0.75.4, with TypeScript for type safety.
-
Navigate to your project folder:
cd MyApp
-
Configure
app.config.js
for flexibility:export default () => ({
name: "MyApp",
slug: "myapp",
version: "1.0.0",
orientation: "portrait",
icon: ".https://docs.instamobile.io/assets/icon.png",
splash: {
image: ".https://docs.instamobile.io/assets/splash.png",
resizeMode: "contain",
backgroundColor: "#ffffff"
},
ios: {
bundleIdentifier: "com.myapp.app",
buildNumber: "1.0.0"
},
android: {
package: "com.myapp.app",
versionCode: 1
},
updates: {
enabled: true,
url: "https://u.expo.dev/your-project-id",
checkAutomatically: "ON_LOAD",
fallbackToCacheTimeout: 0
}
});- Use unique
bundleIdentifier
(iOS) andpackage
(Android) in reverse-domain format (e.g.,com.yourcompany.myapp
). - Prepare a 1024x1024 PNG icon (iOS) and 512x512 PNG icon (Android) in
assets/
.
- Use unique
-
Test locally:
npx expo start
Press
i
for iOS ora
for Android to run on a simulator/emulator.
Summary: You’ve set up a React Native project with Expo SDK 51 and a dynamic app.config.js
, ready for CI automation.
Mega Bundle Sale is ON! Get ALL of our React Native codebases at 90% OFF discount 🔥
Get the Mega BundleStep 2: Set Up Fastlane
Fastlane automates tasks like building and deploying apps. Let’s configure it for iOS and Android.
What is Fastlane? Fastlane is a tool that automates repetitive tasks, like generating app builds or uploading to app stores. It saves time by scripting complex processes.
-
Install Fastlane:
- macOS (for iOS builds):
brew install fastlane
- Linux (for Android CI on Ubuntu):
sudo gem install fastlane -NV
- macOS (for iOS builds):
-
Initialize Fastlane for iOS:
cd ios && fastlane init
Select manual setup and create
ios/Fastfile
:default_platform(:ios)
platform :ios do
desc "Build and sign iOS app"
lane :build_ios do
setup_ci
app_store_connect_api_key(
key_id: ENV["APP_STORE_CONNECT_KEY_ID"],
issuer_id: ENV["APP_STORE_CONNECT_ISSUER_ID"],
key_content: ENV["APP_STORE_CONNECT_KEY_CONTENT"]
)
match(type: "appstore", readonly: true)
gym(
scheme: "MyApp",
export_method: "app-store",
output_directory: "./build",
output_name: "MyApp.ipa"
)
end
end -
Initialize Fastlane for Android:
cd android && fastlane init
Update
android/Fastfile
:default_platform(:android)
platform :android do
desc "Build and sign Android AAB"
lane :build_android do
gradle(
task: "bundle",
build_type: "Release",
project_dir: "android"
)
end
end -
Set up Fastlane Match for iOS credentials:
fastlane match init
Create a private Git repository for certificates and provisioning profiles, then run:
fastlane match appstore
Summary: You’ve configured Fastlane for iOS and Android, with secure credential management for iOS builds.
Step 3: Configure GitHub Actions for CI
GitHub Actions automates your CI pipeline. Let’s create a workflow to build and test your app.
What are GitHub Actions? GitHub Actions is a platform that runs automated workflows (like building or testing) when you push code to GitHub. It’s like a robot that handles repetitive tasks for you. See GitHub’s guide.
-
Create a GitHub repository and push your project:
git init
git add .
git commit -m "Initial commit"
git remote add origin <your-repo-url>
git push -u origin main -
Create
.github/workflows/ci.yml
:name: React Native CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm install
- name: Run tests
run: npm test
build-ios:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- uses: ruby/setup-ruby@v1
with:
ruby-version: 3.2
- name: Install Fastlane
run: gem install fastlane
- name: Set up Fastlane Match
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
run: |
echo "${{ secrets.MATCH_GIT_PRIVATE_KEY }}" > match_key
chmod 600 match_key
export MATCH_GIT_PRIVATE_KEY_PATH=$(pwd)/match_key
fastlane match appstore --readonly
- name: Build iOS
env:
APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
APP_STORE_CONNECT_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_KEY_CONTENT }}
run: cd ios && fastlane build_ios
- name: Upload iOS artifact
uses: actions/upload-artifact@v4
with:
name: ios-build
path: ios/build/MyApp.ipa
build-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- uses: actions/setup-java@v4
with:
java-version: 17
- name: Install Fastlane
run: sudo gem install fastlane -NV
- name: Install dependencies
run: npm install
- name: Set up Keystore
env:
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: |
echo $KEYSTORE_BASE64 | base64 -d > android/app/my-upload-key.keystore
echo "MYAPP_UPLOAD_STORE_FILE=my-upload-key.keystore" >> android/gradle.properties
echo "MYAPP_UPLOAD_KEY_ALIAS=$KEY_ALIAS" >> android/gradle.properties
echo "MYAPP_UPLOAD_STORE_PASSWORD=$KEYSTORE_PASSWORD" >> android/gradle.properties
echo "MYAPP_UPLOAD_KEY_PASSWORD=$KEY_PASSWORD" >> android/gradle.properties
- name: Build Android
run: cd android && fastlane build_android
- name: Upload Android artifact
uses: actions/upload-artifact@v4
with:
name: android-build
path: android/app/build/outputs/bundle/release/app-release.aab -
Add GitHub secrets (Settings > Secrets and variables > Actions):
MATCH_PASSWORD
: Fastlane Match password.MATCH_GIT_URL
: Match repository Git URL.MATCH_GIT_PRIVATE_KEY
: SSH private key (base64-encoded).APP_STORE_CONNECT_KEY_ID
,APP_STORE_CONNECT_ISSUER_ID
,APP_STORE_CONNECT_KEY_CONTENT
: From App Store Connect API key (generate at appstoreconnect.apple.com).KEYSTORE_BASE64
: Base64-encoded Android keystore (cat my-upload-key.keystore | base64
).KEYSTORE_PASSWORD
,KEY_ALIAS
,KEY_PASSWORD
: From keystore creation.
Summary: You’ve created a GitHub Actions workflow to automate testing and building for iOS and Android, using macos-14
for iOS compatibility.
Step 4: Add Testing to the CI Pipeline
Let’s add automated tests to ensure your code is solid.
-
Install testing dependencies:
npm install --save-dev jest @types/jest ts-jest @testing-library/react-native @testing-library/jest-native
-
Configure Jest in
package.json
:{
"scripts": {
"test": "jest"
},
"jest": {
"preset": "react-native",
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"setupFilesAfterEnv": ["@testing-library/jest-native/extend-expect"]
}
} -
Create a test in
src/__tests__/App.test.tsx
:import { render } from '@testing-library/react-native';
import App from '../App';
test('renders welcome text', () => {
const { getByText } = render(<App />);
expect(getByText('Welcome to MyApp!')).toBeTruthy();
}); -
Update
.github/workflows/ci.yml
to run tests (already included in Step 3). -
Test locally:
npm test
Summary: You’ve added Jest and React Native Testing Library to your CI pipeline, catching bugs early.
Step 5: Automate OTA Updates with EAS Update
Let’s automate over-the-air (OTA) updates for instant deployments using EAS Update.
-
Install
expo-updates
:npm install expo-updates
Check the latest compatible version for Expo SDK 51 in Expo’s package documentation.
-
Configure
app.config.js
(done in Step 1). -
Add update logic to
src/App.tsx
:import { useEffect } from 'react';
import { View, Text } from 'react-native';
import * as Updates from 'expo-updates';
export default function App() {
useEffect(() => {
const checkForUpdates = async () => {
try {
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
await Updates.fetchUpdateAsync();
await Updates.reloadAsync();
}
} catch (e) {
console.error('Update check failed:', e);
}
};
if (!__DEV__) {
checkForUpdates();
}
}, []);
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Welcome to MyApp!</Text>
</View>
);
} -
Configure EAS Update:
eas update:configure
Update
app.config.js
with the generatedproject-id
. -
Update
.github/workflows/ci.yml
to publish OTA updates:name: React Native CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
# ... (same as Step 3)
build-ios:
# ... (same as Step 3)
build-android:
# ... (same as Step 3)
publish-ota:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install EAS CLI
run: npm install -g eas-cli
- name: Install dependencies
run: npm install
- name: Publish OTA update
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
run: eas update --branch production -
Add
EXPO_TOKEN
to GitHub secrets:- Run
eas login
andeas whoami
locally to get your token. - Add it in Settings > Secrets and variables > Actions.
- Run
Summary: You’ve automated OTA updates with EAS Update, enabling instant JavaScript and asset deployments.
Step 6: Deploy to App Stores
Let’s automate deployments to the App Store and Play Store using EAS and Fastlane.
-
Update
ios/Fastfile
for App Store Connect API:platform :ios do
desc "Build and deploy iOS app"
lane :deploy_ios do
setup_ci
app_store_connect_api_key(
key_id: ENV["APP_STORE_CONNECT_KEY_ID"],
issuer_id: ENV["APP_STORE_CONNECT_ISSUER_ID"],
key_content: ENV["APP_STORE_CONNECT_KEY_CONTENT"]
)
match(type: "appstore", readonly: true)
gym(
scheme: "MyApp",
export_method: "app-store",
output_directory: "./build",
output_name: "MyApp.ipa"
)
deliver(
submit_for_review: false,
force: true,
app_identifier: "com.myapp.app"
)
end
end -
Update
android/Fastfile
:platform :android do
desc "Build and deploy Android AAB"
lane :deploy_android do
gradle(
task: "bundle",
build_type: "Release",
project_dir: "android"
)
supply(
package_name: "com.myapp.app",
aab: "android/app/build/outputs/bundle/release/app-release.aab",
track: "production",
json_key_data: ENV["GOOGLE_PLAY_JSON_KEY"]
)
end
end -
Update
.github/workflows/ci.yml
to usedeploy_ios
anddeploy_android
lanes (replacebuild_ios
andbuild_android
in Step 3). -
Add additional GitHub secrets:
GOOGLE_PLAY_JSON_KEY
: Base64-encoded Google Play service account JSON (cat service-account.json | base64
). Generate at play.google.com/console under Setup > API access (see Google’s guide).
Summary: Your CI pipeline now automates App Store and Play Store deployments with secure API key authentication.
Troubleshooting Tips
- Fastlane Match Fails: Verify
MATCH_GIT_URL
andMATCH_GIT_PRIVATE_KEY
. Ensure the Match repository is accessible. - GitHub Actions Errors: Use
macos-14
for iOS,ubuntu-latest
for Android. Check secrets for typos. - Build Fails: Confirm Xcode 16+ (iOS) and Android SDK 35. Clear caches (
rm -rf ~/Library/Developer/Xcode/DerivedData
for iOS,./gradlew clean
for Android). - OTA Issues: Verify
EXPO_TOKEN
andapp.config.js
’supdates.url
. Runeas update:configure
to reset.
Looking for a custom mobile application?
Our team of expert mobile developers can help you build a custom mobile app that meets your specific needs.
Get in TouchConclusion
You’ve built a robust CI pipeline for your React Native app using Fastlane, GitHub Actions, and EAS with Expo SDK 51! Your app now automatically builds, tests, and deploys to the App Store, Play Store, and via OTA updates. For faster setups, explore CI-ready templates at Instamobile or Dopebase. Want more? Dive into Expo’s documentation, or join the React Native community at reactnative.dev.
Additional Resources
- dopebase.com - Mobile app development resources and templates
- instamobile.io - Production-ready React Native templates
- instaflutter.com - Flutter templates for cross-platform development
- reactnative.dev - Official React Native documentation
- docs.expo.dev - Expo SDK and EAS documentation
- docs.github.com/en/actions - GitHub Actions documentation