The best CI/CD for React Native with E2E support

January 11, 2021 - 6 min read

Continuous Integration (CI) is the process of regularly integrating software components, usually from multiple developers (team) into the main codebase. So with Continuous Integration, you mostly avoid code conflicts by merging all code from all developers more to a central branch many times a day. Historically, this process seem to have happened once a day/night (or less frequently.

Continuous Deployment (CD) is the process of regularly deploying your software into a production environment. In addition, this happens in a fresh and isolated environment, to have reproduciblity instead of “it works on my machine…”. CD usually involves checking out the code, installing dependencies, running checks like code formatting/linting, build and full test execution.

As a developer, you want “instant” feedback when the code you’ve written broke the system. Optimally, you run all necessary checks like code formatting/linting and tests locally, before you push changes to the remote codebase. To speed things up, you may run these checks only on the files you’ve actually changed.

React Native is a way to write fully-native mobile apps for iOS and Android in one shared JavaScript/TypeScript codebase. The underlying build system is a normal iOS and Android project, with the addition of needing a JavaScript execution environment and libraries to render your bundled JavaScript code into actual native components. In addition to that, you may need native modules, depending on your use case. These modules themselves can include native code, that is included into your project(s) as a Pod/Library or Gradle dependency.


The fact of having multiple factors that can easily break your build improves the significance of having a well working CI and CD pipeline. A complete development and deployment process should at least involve the following steps:

  • When committing changes to your git repository, a pre-commit hook runs linting and unit/integration tests on your staged files. When successful, the changes can be pushed
  • When pushed to the git repository, the CI/CD environment checks out the code
  • Then the dependencies get installed (node modules and iOS Pods)
  • Then the linter checks if the code is properly formatted, etc
  • If you use Flow for static type checking, you may run it
  • If you use TypeScript, you may run the TypeScript compiler
  • Then you test your JavaScript code with unit/integration tests, usually via Jest or Mocha
  • Then you run the native builds for iOS and Android
  • If you have End-to-End (E2E) tests, you may run them now in an Android Emulator and iOS simulator
  • Finally, you could sign & upload the apps to the Play Store and Appstore

In this article I will show how I’ve setup Github Actions Workflows to run E2E tests for both iOS and Android. I’ve made good experience with integrating this into my personal app on Github.

Github actions must be setup in a folder inside .github/workflows as .yml files. Each of these workflows may run in parallel, so I set up one for iOS and Android specifically.

Decide for a run environment

Github actions provides a variety of different run environments, ranging from Windows, Ubuntu and Mac. You can find a full list of installed software here. You can also find very detailed Github Actions instructions on their page.

How Github Action look like

In order for iOS to build, you need a Mac environment, including Xcode set up. Github Actions provides a MacOS environment with multiple different Xcode already set up. So we will usemacos-latest for both iOS and Android.

name: iOS
on: [push, pull_request]
runs-on: macos-latest
- name: Checkout project
uses: actions/checkout@v1
- name: Specify node version
uses: actions/setup-node@v1
node-version: '10.x'
view raw iOS-1.yml hosted with ❤ by GitHub

The yml format is pretty much self-explanatory. Here, a workflow will be triggered on each push to a branch or creation of a pull request on Github. You could further limit the workflows to only be triggered on specific tags, for example.

I will only use one job in every workflow, but you could also use multiple, for example for install, build and test. Each step in a job begins with a “-” and needs either a “uses” or “run”. If you need multiple lines in a “run”, you have to add a “|”, which you will see. The first job does a git checkout of the project.

Use a specific Node Version

In some projects it is necessary to change the Node version, in order for some tools to work properly. This can be done with the actions/setup-node action. We will use Node version 10.

Setup Detox & Trigger a Build with specific Xcode Version

Since Detox will be used as the E2E test framework, it needs to be setup. Then, we want to install the node modules (including Cocoapods via postinstall script). --frozen-lockfile may be useful here, in order to make sure to use exactly the dependencies which are pinned in the yarn.lock file. We also specify the exact Xcode version to be used.

- name: Detox setup
run: |
brew tap wix/brew
brew install applesimutils
- name: Install node_modules & deploy with specific XCode version
DEVELOPER_DIR: /Applications/
run: |
yarn install --frozen-lockfile
yarn build-detox-ios
view raw iOS-2.yml hosted with ❤ by GitHub

Run iOS E2E tests

As a last step for iOS, we may start the Detox E2E / instrumentation tests. This should automatically start the iOS Simulator with the specific device defined in your Detox configuration.

- name: iOS test
run: |
npx detox clean-framework-cache && npx detox build-framework-cache
yarn test-detox-ios
view raw iOS-3.yml hosted with ❤ by GitHub

Specific Java Version for installing the Android Emulator

Specifically for the Android avdmanager to work, we have to make a change in the Java installation. For that, we can use the joschi/setup-jdk action. After that, the specific Android Emulator image can be downloaded and the virtual device be set up.

- name: Use specific Java version for sdkmanager to work
uses: joschi/setup-jdk@v1
java-version: 'openjdk8'
architecture: 'x64'
- name: Download Android Emulator Image
run: |
echo y | sudo $ANDROID_HOME/tools/bin/sdkmanager --verbose "system-images;android-27;google_apis;x86"
$ANDROID_HOME/tools/bin/avdmanager create avd -n emu -k "system-images;android-27;google_apis;x86" -b "x86" -c 1G -d 7 -f
view raw Android-1.yml hosted with ❤ by GitHub

Starting the Android Emulator and E2E Tests

Starting the Android Emulator may take a while, depending on the resources your machine has available. Waiting for an Android Emulator to be available can be via the adb wait-for-device command. In order to wait for the boot to be finished, you can just continuously poll for the specific system message to happen. After the boot has been finished, you need to unlock the Emulator via adb shell wm dismiss-keyguard.

It may be possible that your tests hang up for some reason (especially during CI/CD setup). In such a case you want the job to time out, otherwise you may end up with a queue of pending workflows.

- name: Android test
timeout-minutes: 10
continue-on-error: true
run: |
mkdir -p artifacts
export PATH=$PATH:$ANDROID_HOME/platform-tools
$ANDROID_HOME/emulator/emulator @emu &
adb wait-for-device; adb shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;'; adb shell wm dismiss-keyguard
yarn test-detox-android
view raw Android-2.yml hosted with ❤ by GitHub

When an E2E test fails

Detox provides the possibility to record videos (or logs) for each running test. This can be further limited to only happen if a test is failing. Using the Github Action actions/upload-artifact we can automatically upload files to a workflow and download them later in order to directly see what went wrong.

- name: Provide videos of failed E2E tests
uses: actions/upload-artifact@master
name: android-failing-e2e-videos
path: artifacts/
view raw Android-3.yml hosted with ❤ by GitHub

Very important: adding a badge

You can add a badge to your README file. Suppose you have a workflow file in .github/workflows/iOS.yml. Then, code to put in your markup file should look like the following: ![example](

iOS badge example

view raw hosted with ❤ by GitHub

You can find the full source code here.

Originally published at on January 11, 2021.