---
title: Automate React Native App deployments
teaser: 'Use Fastlane and continuous integration to automate building React Native
  apps.

  '
tags: deployment,javascript,mobile,react native,ios,android
author: Rakesh Arunachalam
published_on: 2022-09-30
---

## Introduction

App deployment is a fundamental part of the native development workflow, and
automating these deployments can make the process more efficient and
controllable. This post aims to educate on the various deployment options
available for a React Native app, the appropriate strategy, and the ways to
automate them. Combining these strategies has enabled us to deliver App updates
as fast as back-end updates.

Before diving into deployment, it is helpful to understand that there are two
core parts of any React Native App:

- The Native App container (different for iOS and Android)
- The JavaScript codebase that talks to the native container using a bridge

The [React Native documentation]  explains this well.

[React Native documentation]: <https://reactnative.dev/docs/communication-ios>

## Manual steps to deploy a React Native app

For iOS, these are the steps to archive and upload a new build to TestFlight via
the App Store Connect:

1. Open XCode and select your project in the Project Navigator.
2. Update the Version or Build number.
3. On the Top Bar, click on Product > Archive (after the app is archived, the
   Archives Manager will open).
4. Select the new Archive and click on the Distribute App button.
5. Select method of distribution: App Store Connect and choose Destination:
   Upload
6. On App Store Connect distribution options, ensure all boxes are checked.
7. Select the Automatic profile, review the content and click the Upload button.
8. Once the upload is finished go to App Store Connect > My Apps, select your
   app, and the build should be available on the dashboard.
9. After the build is processed, it is ready to be submitted to internal testers
   that are part of your Apple Development team or external users invited
   directly via a public link.

The process for [Android deployment] involves a series of steps such as
generating an upload key using `keytool`, executing commands to generate release
builds, and uploading the App to the Play store.

Based on this, We have several steps for both platforms, and some of the steps are prone to developer errors.

[Android deployment]: <https://reactnative.dev/docs/signed-apk-android>

## Metrics for deployment strategy

For a React Native app, here are a few valuable metrics that can be used to
decide if a deployment strategy will be successful:

1. **Time to deploy** - The time taken from the start of the deployment process
   till an App update is available on the stores. This also includes the time
   spent on the unpredictable App review process.
2. **Time to upgrade** - We have not deployed to 100% of the users if they don't
   upgrade immediately. Not all users upgrade their apps when there is an App
   update, and it might take a few weeks to even a month to ensure a large
   percentage of the users have upgraded. Sometimes it's worth measuring if a
   higher percentage of active users have upgraded.
3. **Frequency of deployments** - The number of App updates shipped in a given
   time. If this number is small, the above two factors might not matter. But if
   our team ships many updates and the above factors start impacting delivery,
   it might be worth changing the strategy or adding a new approach to
   supplement the existing one.
4. **Automatic/Manual** - While deploying mobile app updates, developers do not
   have an "Undo" button, and updates are permanent once shipped. Imagine
   shipping an App update to thousands of users and realising that a developer
   wrongly used the staging environment instead of production during the
   deployment process. This gets worse when a fix addressing this takes two days
   to be available due to it being stuck on App review. Any deployment strategy
   must be automatic to an extent that it at least eliminates developer errors.
5. **Implementation complexity** - It's great to have a one-click deployment in
   place, but it's not optimal if it takes a few weeks to implement. We
   optimistically expect developers to set up the App, CI tools (TypeScript,
   Linter, Test-suite) and App deployments (Android and iOS) in a week. It might
   be okay if the process is not fully automatic as long as the rest of the
   metrics are addressed by our strategy.

## Deployment of Expo vs React Native CLI app

It is relatively easy to deploy an App using Expo compared to the React Native
CLI.

The difference between [Expo] and [RN CLI] is that Expo provides great defaults
and aids the development of the different aspects (App permissions, Push
notifications, Dark mode, etc. ) of a mobile application such that developers
can focus on building good user experiences instead of handling third-party
library conflicts, integration problems, upgrades and maintenance. With the CLI,
developers have greater flexibility in deciding the correct third-party
libraries that solve the problem and sometimes this could be a good thing as
Expo can limit developers with what can be achieved in a Mobile app. But this
choice comes with a cost in the form of developer hours spent on integrating the
third-party library with the native platforms. Hence the Native container of an
Expo App is constrained for a reason yet stable for each release.

The JavaScript codebase can change dynamically based on the App we want to
build. Additionally, Expo uses [Expo Application Services], which makes it a
breeze to update the JavaScript bundle over the air. Running one command on your
terminal or the CI can deliver instant updates to all the App users without
submitting an App update to the iOS App Store or the Google Playstore, which is
fantastic from a continuous deployment perspective.

But there are a few cons to using Expo, such as

- [Native Modules], which is available only on React Native CLI, enables the
  React Native JavaScript code to talk with Native platform code.
- Latest Platform (iOS/Android) updates are not available immediately, and we
  need to wait for the Expo team to implement them on a future release.

Apps built using RN CLI have near-unlimited capabilities. Hence, We will only
focus on the deployment strategies of RN CLI-based apps in this post.

[Expo]: https://expo.dev/
[RN CLI]: https://reactnative.dev/docs/environment-setup#installing-dependencies
[Expo Application Services]: https://docs.expo.dev/eas/
[Native Modules]: https://reactnative.dev/docs/native-modules-intro

## FastLane

When starting a new project, there is a higher possibility of adding third-party
libraries and associated dependencies to the project, which means the Native app
container and the JavaScript codebase change continuously. This means teams must
also frequently submit App updates to the iOS/Android app stores. Automating app
builds keeps the team productive and focused on developing the app instead of
deploying it.

[Fastlane] is a commonly used tool for managing App deployments. It is built
using Ruby, has been around for some time, and is nicely documented. It also
makes deploying apps to multiple environments (dev, staging, production) easier
as we can create separate [lanes] for each environment and platform.

`Fastfile` contains all such lane commands, and the `README.md` is
auto-generated so that every team member is empowered to deploy the Apps across
environments and platforms. There's also a [plugin] ecosystem with a lot of
third-party integration for services such as Microsoft AppCenter, Version number
increment, Upload symbols to Sentry, Bugsnag, etc.

[Fastlane]: https://fastlane.tools/
[lanes]: https://docs.fastlane.tools/advanced/lanes
[plugin]: https://docs.fastlane.tools/plugins/available-plugins/

## App deployment on local machine

The Fastlane docs contain all the information required to set up an individual
deployment workflow. This section shows how a deployment workflow looks like
highlighting the ones which have proven to be successful for our projects.

### iOS

For iOS, these are the typical steps of the app deployment workflow that
Fastlane handles for us:

- Ensure the git repository is clean, as we don't want to build and submit apps
  with uncommitted code.
- Auto-increment the iOS build numbers, as every iOS app submitted to TestFlight
  needs a higher build number.
- Commit these changes, create a git tag and push the changes to the repository.
  These [git tags] serve as checkpoints to quickly identify the commit that
  caused a particular App release.
- Build the app using the correct scheme based on the selected lane.
  **Example**: For lane `development`, build the Development version of the app.
- Submit the generated App build to iOS TestFlight.

```ruby

lane :development do
  ensure_git_status_clean
  increment_build_number(xcodeproj: 'ios/MyApp.xcodeproj')
  commit_version_bump(xcodeproj: 'ios/MyApp.xcodeproj')
  add_git_tag
  push_to_git_remote
  scheme = options.fetch(:scheme, 'Development')
  gym(
    scheme: scheme,
    include_bitcode: false,
    export_xcargs: '-allowProvisioningUpdates',
  )
  testflight(skip_waiting_for_build_processing: false)
end
```

[git tags]: https://docs.github.com/en/rest/git/tags

### Android

For Android, these are the steps managed by Fastlane:

- Ensure the git repository is clean.
- Build the app for the correct environment and Google play track.
- Supply (Submit) the generated App build to Google Playstore.

```ruby
lane :build do |options|
  type = options.fetch(:type, 'Release')
  env = options.fetch(:env, 'development')
  track = options.fetch(:track, 'beta')
  ENV['ENVFILE'] = '.env.#{env}'
  Dir.chdir('..') do
    sh 'bin/bundle-android'
  end
  gradle(task: 'clean')
  gradle(
    task: 'assemble',
    flavor: track,
    build_type: type,
  )
end

lane :development do
  ensure_git_status_clean
  build(type: 'Release', env: 'development', track: 'development')
  supply(
    track: 'internal',
    apk: 'development',
    package_name: 'com.myapp.development'
  )
end
```

We do not have automated version number increments because, on Android, we use
[product flavours] which FastLane does not support to be incremented
automatically. Unfortunately, this must be done manually before deploying an
Android app.

[product flavours]: https://developer.android.com/studio/build/build-variants#product-flavors

## Continuous Deployment workflow on the CI

The entire deployment process is difficult to automate on the CI fully, so we
perform semi-automatic deployments. The build process is fully automated on
Circle CI, whereas the deployment to the Stores is manually done on the local
machine. This ensures that there are no developer errors with respect to the
environment variables and other environment-specific Configurations (Firebase,
Play store config, etc.) during App deployments as these data are stored as
Circle CI secrets.

A Circle CI workflow is shown below, which helps developers build both iOS and
Android apps on the CI in three steps:

- **build** - Basic code quality checks (lint, types, tests, etc.)
- **hold_build** - An approval process to conserve CI resources and not build
  apps for every commit.
- **build_ios** or **build_android** - Actual build process which generates the
  App builds as artefacts.

![Circle CI](<https://images.thoughtbot.com/blog-vellum-image-uploads/ecV9gKMwQyHjDS85FUml_Screenshot%202022-06-17%20at%2015.32.40.png>)

### iOS build process

The Apple AppStore connect session sometimes requires a one-time password, which
can be entered on the local machine but not on the CI. The workarounds available
were unreliable, which made us split the workflow into three like this:

1. On the local machine, run `bundle exec fastlane ios adhoc_pre_build`.
   Irrespective of "Development", "Staging", or "Production", this must be
   executed for each deployment to Testflight as it increments the build number
   of the iOS App.
2. `bundle exec fastlane ios adhoc_build` will be executed in the CI, based on
   the selected environment, eventually generating an App build (IPA file).
3. `bundle exec fastlane ios adhoc_deploy scheme:{SCHEME_NAME}` must be executed
   by the developer after downloading the artefact (IPA) produced by the CI.
   SCHEME_NAME must be provided depending on the chosen environment in the build
   process above.

```ruby
lane :adhoc_pre_build do
  increment_build_number(xcodeproj: 'ios/MyApp.xcodeproj')
  commit_version_bump(xcodeproj: 'ios/MyApp.xcodeproj')
  add_git_tag
  push_to_git_remote
end

lane :adhoc_build do |options|
  scheme = options.fetch(:scheme, "Development")
  match(type: "appstore", readonly: true)
  gym(
    scheme: scheme,
    include_bitcode: false,
    export_xcargs: "-allowProvisioningUpdates",
  )
end

lane :adhoc_deploy do |options|
  scheme = options.fetch(:scheme, "Development")
  match(type: "appstore", readonly: true)
  testflight(
    ipa: "MyApp.ipa",
    skip_waiting_for_build_processing: false
  )
end
```

### Android build process

Android product flavours do not allow automating the version numbers increment
by flavour. To get around that and also keep a three-step process similar to
iOS, the Android workflow is as follows:

1. Ensure that the build number of the Android App is incremented in
   `build.gradle` file for the corresponding `productFlavors` - "development",
   "staging" or "production".
2. `bundle exec fastlane build` will be executed in the CI, generating an App
   build (APK file).
3. `bundle exec fastlane android adhoc_deploy env:{ENV_NAME}` must be executed
   by the developer after downloading the artefact (APK) produced above.
   ENV_NAME must be specified based on the downloaded artefact.

```ruby
lane :build
# Same as described previously
end

lane :adhoc_deploy do |options|
  env = options.fetch(:env, "development")
  package_name = "com.myapp.development"
  apk = "myapp-development-release.apk"

  case env
    when "development"
      package_name = "com.myapp.development"
      apk = "myapp-development-release.apk"
    when "staging"
      package_name = "com.myapp.staging"
      apk = "myapp-staging-release.apk"
    when "production"
      package_name = "com.myapp"
      apk = "myapp-production-release.apk"
  end

  supply(
    track: "internal",
    apk: apk,
    package_name: package_name
  )
end
```

## Conclusion

We covered App deployment strategies of React Native CLI based mobile
apps in this post. Remember how there are two core parts of a React Native app.
The process described in this post mainly applies to automating the deployment
of the Native App container in which the JavaScript code gets bundled during the
build process. [React Native Code Push] is a module that enables the JavaScript
code to be separately updated over the air, similar to Expo Application
Services, and automating deployments using this technology can hopefully be a
separate blog post.

[React Native Code Push]: https://github.com/microsoft/react-native-code-push
