Expo React Native CI/CD flow using CircleCI
Expo is a great abstraction layer around React Native which makes it a lot easier to setup React Native apps, from accessing native APIs to establishing CI/CD pipelines.
In this post, I will walk you through how to setup CI/CD flow for Expo React Native using Circle CI.
TL;DR
The diagram above describes the overall flow of the CI/CD flow. The Android builds are not included for simplicity.
Here's the complete config.yml
file for this flow.
version: 2.1
defaults: &defaults
working_directory: ~/app
docker:
# the Docker image with Cypress dependencies
- image: cypress/base:12.14.0
environment:
## this enables colors in the output
TERM: xterm
commands:
restore-cache-and-install-dependencies:
description: "Restore cache and install dependencies"
steps:
- restore_cache:
key: v2-deps
- run:
name: Install Dependencies
command: npm ci
- save_cache:
key: v2-deps-{{ .Branch }}-{{ checksum "package-lock.json" }}
paths:
- node_modules
- ~/.npm
- ~/.cache
publish_to_expo:
description: "Publish JS bundle to Expo"
steps:
- checkout
- restore-cache-and-install-dependencies
- run:
name: Publish to Expo
command: npx expo-cli publish --non-interactive --max-workers 1 --release-channel $EXPO_RELEASE_CHANNEL
build_binaries:
description: "Build native binaries"
steps:
- checkout
- restore-cache-and-install-dependencies
- run:
name: Build Android
command: npx expo build:android -t apk --non-interactive --release-channel $EXPO_RELEASE_CHANNEL --no-wait
- run:
name: Build iOS
command: npx expo build:ios -t archive --non-interactive --release-channel $EXPO_RELEASE_CHANNEL --no-wait
jobs:
unit_test:
<<: *defaults
steps:
- checkout
- restore-cache-and-install-dependencies
- run:
name: Run ESLint
command: npm run lint
- run:
name: Run unit tests
command: npm run test
publish_feature_to_expo:
<<: *defaults
environment:
EXPO_RELEASE_CHANNEL: << pipeline.git.branch >>
resource_class: xlarge
steps:
- publish_to_expo
publish_to_expo_staging:
<<: *defaults
environment:
EXPO_RELEASE_CHANNEL: staging
resource_class: xlarge
steps:
- publish_to_expo
build_binaries_staging:
<<: *defaults
environment:
EXPO_RELEASE_CHANNEL: staging
steps:
- build_binaries
publish_to_expo_production:
<<: *defaults
environment:
EXPO_RELEASE_CHANNEL: production
resource_class: xlarge
steps:
- publish_to_expo
build_binaries_production:
<<: *defaults
environment:
EXPO_RELEASE_CHANNEL: production
steps:
- build_binaries
workflows:
version: 2
build-deploy:
jobs:
- unit_test
- approval:
type: approval
requires:
- unit_test
- publish_feature_to_expo:
context: app-name-staging
requires:
- approval
filters:
branches:
ignore: master
- publish_to_expo_staging:
context: app-name-staging
requires:
- unit_test
filters:
branches:
only: master
- build_binaries_staging:
context: app-name-staging
requires:
- approval
filters:
branches:
only: master
- publish_to_expo_production:
context: app-name-production
requires:
- unit_test
filters:
tags:
only: /^v.*/
- build_binaries_production:
context: app-name-production
requires:
- approval
filters:
tags:
only: /^v.*/
On feature branch
When you push to a feature branch, it will run tests and trigger the Expo publish which will build JS bundle and deploy to Expo CDN. The publish command looks like this.
npx expo-cli publish --non-interactive --max-workers 1 --release-channel $EXPO_RELEASE_CHANNEL
Note that you need to set the EXPO_TOKEN
environment variable in the project settings for authorizing the expo command. You can generate the access token in the Expo dashboard.
Also, it specifies the release channel with the $EXPO_RELEASE_CHANNEL
environment variable.
For feature branches, the release channel corresponds to the branch name. In the job, you can set the environment variable like this.
publish_feature_to_expo:
<<: *defaults
environment:
EXPO_RELEASE_CHANNEL: << pipeline.git.branch >>
resource_class: xlarge
steps:
- publish_to_expo
Once the JS bundle is published to Expo, you can test it with Expo Go app.
On merging to master branch
When a feature branch is merged to the master branch, it will run tests, publish JS bundle to Expo on the staging
channel, and build native binaries for iOS and Android.
Note that you need an Apple Developer account for iOS builds.
Once the native binary has been built, you can download the .ipa
and .apk
file from Expo and upload them to TestFlight and Google Play Console for testing.
I added the approval process as you don't need to build native binaries every time. Since most updates can be done through OTA updates, you need to rebuild native binaries only when you change native metadata like the app's name or icon or upgrade Expo SDK.
If you want to automate uploading native binaries, look at EAS Submit (I haven't tried it yet).
On release tag
When a release tag is created, it will run tests, publish JS bundle to Expo on the production
channel, and build native binaries for iOS and Android.
The only differece from the previous flow is that it publishes to the production
channel.
Environment variables
For most apps, you need to define environment variables for each release channel (e.g. staging, production).
CircleCI contexts allows us to define different environment variables per environment. You can specify which context to use in the workflow like this.
context: app-name-staging
Wrap up
With Expo, it's straightforward to setup CI/CD pipeline for React Native apps.
If you've got any questions or comments, DM me on Twitter @avosalmon.