This repository contains a trimmed-down, production-style example of a mobile CI/CD setup for a React Native application.
It demonstrates:
- Continuous integration with CircleCI
- Automated mobile pipelines using Fastlane
- Beta distribution via Firebase App Distribution
- Production distribution to Google Play and Apple App Store / TestFlight
- A fully wired white-labeling flow for an additional customer application
ℹ️ This repository was prepared as a demo for a conference talk.
Watch the recording here:
YouTube – Mobile CI/CD Examples
- High-Level Architecture
- Repository Structure
- Environments & Flavors
- CI/CD Workflows in CircleCI
- Fastlane Lanes
- Beta Distribution (Firebase)
- Production Distribution (App Store / Google Play)
- White-Labeling Concept
- White-Labeling Implementation Details
- Secrets & Environment Variables
- Local Usage & Example Commands
- Extending with a New White-Label Customer
- Additional Resources
At a high level, this project shows how a single React Native codebase can be built and shipped to:
- Internal testers (Firebase App Distribution)
- Public app stores (Google Play + App Store / TestFlight)
- Multiple brands (white-label customers)
flowchart TD
Developer["Developer pushes to Git"] --> CI["CircleCI workflow"]
CI --> Tests["JS tests"]
Tests --> BuildAndroid["Android build (Fastlane)"]
Tests --> BuildiOS["iOS build (Fastlane)"]
BuildAndroid --> BetaAndroid["Firebase App Distribution (Android)"]
BuildiOS --> BetaIOS["Firebase App Distribution (iOS)"]
BuildAndroid --> PlayStore["Google Play"]
BuildiOS --> AppStore["App Store / TestFlight"]
subgraph WhiteLabelFlow["White-label apps"]
BuildAndroid --> WLPlay["White-label app (Google Play)"]
BuildiOS --> WLAppStore["White-label app (App Store)"]
end
The most relevant directories and files:
| Path | Description |
|---|---|
./android/ |
Android Gradle project (React Native app, flavors, signing). |
./android/app/build.gradle |
App module Gradle file with productFlavors, including white-label. |
./android/keystores/ |
Android keystore storage + notes. |
./ios/ |
iOS project directory (Xcode project/workspace expected here). |
./fastlane/Fastfile |
Main Fastlane configuration for Android & iOS. |
./fastlane/Matchfile |
Configuration for fastlane match (certificate provisioning). |
./fastlane/helpers/CompanyFastlane.rb |
Ruby helper with build profiles (QA/Staging/Production/WhiteLabel). |
./fastlane/helpers/beta_msg.sh |
Script generating release notes for beta builds. |
./fastlane/helpers/apk_report.sh |
Script for APK inspection using apkanalyzer. |
./.circleci/config.yml |
CircleCI pipelines and workflows for beta & production. |
./docs/README.md |
Extra links to documentation. |
./android/keystores/README.md |
Notes on keystore generation and usage. |
The project uses multiple environments and flavors:
Defined in android/app/build.gradle via productFlavors:
productionqastagingWhiteLabelCustomer(white-label example)
Each flavor has its own:
applicationId(e.g.com.company.CompanyProduct,com.WhiteLabelCustomer.Product)resValue "string", "app_name", ...to change app namesigningConfig(release,release_branded, etc.)
On iOS the differentiation is driven by Xcode schemes and bundle IDs, as configured in CompanyFastlane.rb:
Product.qa,Product.staging,Product(production)WhiteLabelCustomerscheme for the white-label build
Each iOS profile sets:
BUNDLEIDXCODESCHEMEGOOGLESERVICEPLIST(per-environment GoogleService-Info.plist)
The CI is defined in .circleci/config.yml.
Key elements:
-
nodejs_setupjob- Restores source cache
- Installs JS dependencies (
yarn) - Runs tests with JUnit reporting
-
Android jobs (build, approve, deploy)
- Use anchors like
*android_flow_beta,*android_flow_deploy - Execute Fastlane via a shared
fastlane_executecommand
- Use anchors like
-
iOS jobs (build, approve, deploy)
- Use anchors like
*ios_flow_beta,*ios_flow_deploy - Also drive Fastlane
- Use anchors like
-
Contexts such as
mobile-ci-cd-qa,mobile-ci-cd-staging,mobile-ci-cd-production- Hold environment-specific secrets (Firebase tokens, keystores, Match password, etc.).
CircleCI workflows (simplified):
flowchart TD
subgraph BetaWorkflow
A["Push to non-main branch"] --> NodeJS["nodejs_setup"]
NodeJS --> QAApprove["approve_beta_qa (manual)"]
NodeJS --> StagingApprove["approve_beta_staging (manual)"]
QAApprove --> AndroidBetaQA["android_beta_qa"]
QAApprove --> IOSBetaQA["ios_beta_qa"]
StagingApprove --> AndroidBetaStaging["android_beta_staging"]
StagingApprove --> IOSBetaStaging["ios_beta_staging"]
end
subgraph ProductionWorkflow
B["Push to integration / master"] --> NodeJSProd["nodejs_setup"]
NodeJSProd --> AndroidApprove["android_approve (manual)"]
NodeJSProd --> IOSApprove["ios_approve (manual)"]
AndroidApprove --> AndroidDeploy["android_deploy"]
AndroidApprove --> AndroidDeployWL["android_deploy_WhiteLabelCustomer"]
IOSApprove --> IOSDeploy["ios_deploy"]
IOSApprove --> IOSDeployWL["ios_deploy_WhiteLabelCustomer"]
end
Each deploy job sets FASTLANE_LANE to the correct Fastlane lane:
android firebase_qa,android firebase_staging,android firebase_productionandroid release,android whitelabel_WhiteLabelCustomerios firebase_qa,ios firebase_staging,ios firebase_productionios release,ios whitelabel_WhiteLabelCustomer
Gemfile pins Fastlane:
gem 'fastlane', '>=2.135.2'fastlane/Pluginfile enables Firebase distribution:
gem 'fastlane-plugin-firebase_app_distribution'fastlane/Matchfile configures certificate storage in a Git repo:
git_url('[email protected]:Company/fastlane_secrets.git')
git_branch('master')
storage_mode('git')
shallow_clone(true)
type('adhoc')
readonly(true)
username('[email protected]')Main Android lanes (names parsed from the file):
firebase_qafirebase_stagingfirebase_productionreleasewhitelabel_WhiteLabelCustomer
Common internal/private lanes:
common_build– orchestrates the build for a given “customer profile”distribution_firebase– uploads to Firebase App Distributiondistribution_playmarket– uploads to Google Play
The before_all hook:
before_all do
ensure_env_vars(env_vars: ['FIREBASE_CLI_TOKEN'])
sh "./helpers/beta_msg.sh > helpers/beta_msg_placeholder.txt"
endMirror the Android lanes:
firebase_qa,firebase_staging,firebase_productionreleasewhitelabel_WhiteLabelCustomerupdate_apple_certsandwhitelabel_WhiteLabelCustomer_update_apple_certs
Private lanes:
distribution_firebase– beta via Firebase App Distributiondistribution_testflight– upload to TestFlightupdate_apple_certs– wrapsmatchfor certificate/provisioning management
Beta distribution is built on top of Fastlane + Firebase App Distribution.
flowchart TD
Dev["Developer pushes feature branch"] --> CI["CircleCI deploy_beta workflow"]
CI --> Node["nodejs_setup & tests"]
Node --> Approve["Manual approval (QA / staging)"]
Approve --> AndroidJob["android_beta_* job"]
Approve --> IOSJob["ios_beta_* job"]
AndroidJob --> FLAndroid["Fastlane android firebase_* lane"]
IOSJob --> FLIOS["Fastlane ios firebase_* lane"]
FLAndroid --> FirebaseA["Firebase App Distribution (Android)"]
FLIOS --> FirebaseI["Firebase App Distribution (iOS)"]
FirebaseA --> Testers["Testers"]
FirebaseI --> Testers
-
CompanyFastlane::BuildsInternal::Qa/Staging/Productiondefine:ANDROIDGRADLETASK(e.g.clean assembleQaRelease)BUNDLEIDGOOGLESERVICEPLISTXCODESCHEMEFIREBASERELEASENOTESFILE(shared across internal builds)- Firebase application IDs via env vars (e.g.
ANDROID_FIREBASE_APP_QA)
-
beta_msg.shgenerates a release notes file with:- Git branch & build number
- Project name
- PR link
- CircleCI build URL
- Latest commit message
This file is used by the Firebase distribution lane for consistent, traceable release notes.
Production distribution uses dedicated release lanes and a separate deploy_production workflow in CircleCI.
flowchart TD
Dev["Developer merges to integration/master"] --> CI["CircleCI deploy_production workflow"]
CI --> Node["nodejs_setup"]
Node --> AndroidApprove["android_approve (manual)"]
Node --> IOSApprove["ios_approve (manual)"]
AndroidApprove --> AndroidRelease["android_deploy job"]
AndroidApprove --> AndroidReleaseWL["android_deploy_WhiteLabelCustomer job"]
IOSApprove --> IOSRelease["ios_deploy job"]
IOSApprove --> IOSReleaseWL["ios_deploy_WhiteLabelCustomer job"]
AndroidRelease --> FLAndroidRelease["Fastlane android release"]
AndroidReleaseWL --> FLAndroidWL["Fastlane android whitelabel_WhiteLabelCustomer"]
IOSRelease --> FLIOSRelease["Fastlane ios release"]
IOSReleaseWL --> FLIOSWL["Fastlane ios whitelabel_WhiteLabelCustomer"]
FLAndroidRelease --> PlayInternal["Google Play internal/production track"]
FLAndroidWL --> PlayWL["Google Play – WhiteLabelCustomer"]
FLIOSRelease --> TestFlight["TestFlight"]
TestFlight --> AppStore["App Store review / release"]
FLIOSWL --> TestFlightWL["WhiteLabelCustomer on TestFlight/App Store"]
From CompanyFastlane:
- Common flags:
PLAYMARKETTRACK = 'internal'(internal track used as an example)PLAYMARKETSKIPUPLOADSCREENSHOTS = truePLAYMARKETSKIPUPLOADMETADATA = false
- For production build profile (e.g.
BuildsInternal::Production):ANDROIDGRADLETASK = 'clean assembleProductionRelease'BUNDLEID = 'com.company.CompanyProduct'
- For white-label build profile (
BuildsWhiteLabel::WhiteLabelCustomer):ANDROIDGRADLETASK = 'clean assembleWhiteLabelCustomerRelease'BUNDLEID = 'com.WhiteLabelCustomer.Product'
- Uses
gymwith export methods configured viaGYMEXPORTMETHOD- Internal builds:
ad-hoc - White-label builds:
app-store
- Internal builds:
- Uses
matchwith:MATCHTYPEset toadhocorappstore- Custom
MATCHGITBRANCH(WhiteLabelCustomerfor the white-label app)
- Lanes like
update_apple_certsandwhitelabel_WhiteLabelCustomer_update_apple_certsmanage provisioning profiles for each profile separately.
White-labeling in this repository means:
Single React Native codebase → multiple branded apps (with different bundle IDs, app names, certificates, and distribution targets), all managed via one CI/CD pipeline.
Goals of the white-label design here:
- Reuse 100% of business logic and UI code
- Isolate branding and distribution logic in:
- Android
productFlavors - iOS schemes & bundle IDs
- Fastlane configuration (
CompanyFastlane, dedicated lanes) - CircleCI jobs and contexts
- Android
- Avoid copying projects or pipelines per customer
Conceptually:
flowchart LR
Codebase["Shared React Native codebase"] --> CoreApp["Core product app"]
Codebase --> WLApp["White-label customer app"]
CoreApp --> CoreBeta["Core - Firebase beta"]
CoreApp --> CoreStores["Core - app stores"]
WLApp --> WLBeta["White-label - Firebase beta"]
WLApp --> WLStores["White-label - app stores"]
subgraph ConfigLayer["Config layer"]
Flavors["Android flavors"]
Schemes["iOS schemes"]
FastlaneProfiles["Fastlane customer profiles"]
CIJobs["CircleCI jobs/workflows"]
end
Flavors --> CoreApp
Schemes --> CoreApp
FastlaneProfiles --> CoreApp
CIJobs --> CoreApp
Flavors --> WLApp
Schemes --> WLApp
FastlaneProfiles --> WLApp
CIJobs --> WLApp
In android/app/build.gradle:
android {
// ...
productFlavors {
production {
dimension "version"
signingConfig signingConfigs.release
resValue "string", "app_name", "Product"
}
qa {
dimension "version"
signingConfig signingConfigs.release
applicationIdSuffix ".qa"
resValue "string", "app_name", "Product.qa"
}
// White-label example
WhiteLabelCustomer {
dimension "version"
signingConfig signingConfigs.release_branded
applicationId "com.WhiteLabelCustomer.Product"
resValue "string", "app_name", "WhiteLabelCustomer XX"
}
}
}What changes per flavor:
applicationId(and optionallyapplicationIdSuffix)app_nameresourcesigningConfig(different keystore for white-label customer)- Potentially other resources (icons, colors, etc. — not included in this minimal example)
Defined indirectly via CompanyFastlane.rb:
class CompanyFastlane
XCODEPROJECT = 'ios/Product.xcodeproj'
XCODEWORKSPACE = 'ios/Product.xcworkspace'
class BuildsInternal < CompanyFastlane
class Qa < BuildsInternal
BUNDLEID = 'com.company.CompanyProduct.qa'
GOOGLESERVICEPLIST = 'GoogleService.qa-Info.plist'
XCODESCHEME = 'Product.qa'
# ...
end
class Production < BuildsInternal
BUNDLEID = 'com.company.CompanyProduct'
GOOGLESERVICEPLIST = 'GoogleService.release-Info.plist'
XCODESCHEME = 'Product'
# ...
end
end
class BuildsWhiteLabel < CompanyFastlane
GYMEXPORTMETHOD = 'app-store'
MATCHTYPE = 'appstore'
SHOWDEVMENU = 'false'
class WhiteLabelCustomer < BuildsWhiteLabel
ANDROIDGRADLETASK = 'clean assembleWhiteLabelCustomerRelease'
APPLEDEVPORTALTEAM = 'JHKJHKJHKJHK'
BACKAPPNAME = 'xxxsdfsdfsdfsd'
BUNDLEID = 'com.WhiteLabelCustomer.Product'
GOOGLESERVICEPLIST = 'GoogleService.WhiteLabelCustomer-Info.plist'
MATCHGITBRANCH = 'WhiteLabelCustomer'
TESTFLIGHTTEAM = 'WhiteLabelCustomer LLC'
XCODESCHEME = 'WhiteLabelCustomer'
# ...
end
end
endKey idea:
Each white-label customer becomes a small Ruby class with all identifiers and build parameters centralized in one place.
The Fastlane lanes accept a “customer profile” and read attributes from the corresponding class (CompanyFastlane::BuildsWhiteLabel::WhiteLabelCustomer).
Examples (conceptual):
lane :whitelabel_WhiteLabelCustomer do
common_build(
customer: CompanyFastlane::BuildsWhiteLabel::WhiteLabelCustomer
)
distribution_playmarket(
customer: CompanyFastlane::BuildsWhiteLabel::WhiteLabelCustomer
)
end
lane :whitelabel_WhiteLabelCustomer_update_apple_certs do
update_apple_certs(
customer: CompanyFastlane::BuildsWhiteLabel::WhiteLabelCustomer
)
endThis keeps the pipeline generic (the same lanes), while customer-specific parameters live inside the Ruby helper classes.
.circleci/config.yml wires dedicated jobs for the white-label customer:
android_deploy_WhiteLabelCustomer:
<<: *android_flow_deploy
environment:
- FASTLANE_LANE: "android whitelabel_WhiteLabelCustomer"
ios_deploy_WhiteLabelCustomer:
<<: *ios_flow_deploy
environment:
- FASTLANE_LANE: "ios whitelabel_WhiteLabelCustomer"These jobs are then placed into production workflows with their own approvals:
- ios_approve_WhiteLabelCustomer:
type: approval
<<: *workflow_deploy_filters
- android_approve_WhiteLabelCustomer:
type: approval
<<: *workflow_deploy_filtersSecrets are injected via CircleCI contexts and Fastlane environment variables.
From CompanyFastlane.rb:
FIREBASE_CLI_TOKEN– used byfastlane-plugin-firebase_app_distributionANDROID_FIREBASE_APP_QAANDROID_FIREBASE_APP_STAGINGANDROID_FIREBASE_APP_PRODUCTION
From CompanyFastlane.rb:
sigh_com.company.CompanyProduct.qa_adhoc_profile-pathsigh_com.company.CompanyProduct.staging_adhoc_profile-pathsigh_com.company.CompanyProduct_adhoc_profile-pathsigh_com.company.CompanyProduct_appstore_profile-pathsigh_com.WhiteLabelCustomer.Product_appstore_profile-path
These variables are generated by fastlane match and accessed in Ruby helpers.
From .circleci/config.yml:
ANDROID_RELEASE_KEYSTORE(base64-encoded keystore content)- Decoded into
android/keystores/release.jksduring the build.
- Decoded into
See android/keystores/README.md for keystore generation examples and notes.
⚠️ The repo is a partial example, not a fully runnable production project.
Paths, bundle IDs, teams and package names are placeholders.
- Node.js + Yarn
- Android SDK / Java JDK
- Xcode (for iOS)
- Ruby + Bundler
- Firebase CLI
# Ruby gems
bundle install
# JavaScript dependencies
yarn installAndroid QA beta:
bundle exec fastlane android firebase_qaAndroid production release:
bundle exec fastlane android releaseAndroid white-label release:
bundle exec fastlane android whitelabel_WhiteLabelCustomeriOS QA beta:
bundle exec fastlane ios firebase_qaiOS production release:
bundle exec fastlane ios releaseiOS white-label release:
bundle exec fastlane ios whitelabel_WhiteLabelCustomerUpdate Apple certificates for the white-label customer:
bundle exec fastlane ios whitelabel_WhiteLabelCustomer_update_apple_certsInspect Android APK outputs:
./fastlane/helpers/apk_report.shTo add another white-label customer:
-
Android
- Add a new flavor in
android/app/build.gradle:- Unique
applicationId - App name via
resValue - Dedicated
signingConfigwith its own keystore
- Unique
- Configure any brand-specific resources (icons, colors, etc.).
- Add a new flavor in
-
iOS
- Add a new Xcode scheme.
- Configure a new bundle ID in the project.
- Add a dedicated
GoogleService.<Customer>-Info.plistif using Firebase.
-
Fastlane
- In
CompanyFastlane.rb, create a new class underBuildsWhiteLabel:- Set
BUNDLEID,XCODESCHEME,GOOGLESERVICEPLIST,MATCHGITBRANCH, etc.
- Set
- Optionally add new Fastlane lanes mirroring
whitelabel_WhiteLabelCustomer.
- In
-
CircleCI
- Add new jobs that point to the new Fastlane lanes (
FASTLANE_LANEenv). - Wire these jobs into the production workflow with appropriate approvals.
- Add a new CircleCI context for the customer if you need separate secrets.
- Add new jobs that point to the new Fastlane lanes (
This approach keeps the pipeline template stable and lets you plug in new customers with minimal duplication.
- Talk Recording:
YouTube – Mobile CI/CD Examples - Firebase Documentation (see also
docs/README.md)
General: https://firebase.google.com/docs - Fastlane: https://fastlane.tools
- CircleCI Config Reference: https://circleci.com/docs/configuration-reference/
This README is intentionally detailed so it can be used as a reference during or after a CI/CD talk, and as a starting point for implementing similar pipelines in real projects.