Automating React Native Deployments with Fastlane and GitHub Actions

2024-05-02

Before explaining what's the problem, it's important to understand the complexity of my deployment process.

In a nutshell I have two platforms: iOS 🍏, Android 🤖 and every platform compiles two applications: Beta testing app also known as Canary 🐤 and Production 🚀 one.

Basically every platform goes through a lane sequentially that looks like this 👇

  1. Code sign setup ✍️
  2. Version management 🔖
  3. Native builds 📦
  4. Beta testing distribution 🐤
  5. Stores distribution 🚀
  6. Sourcemaps 🗺
  7. Communication 🗣

Now let's see in depth every step of the deployment process to understand what I do.

Code sign setup ✍️

Signing native applications is scary 😱, specially when you come from the JavaScript ecosystem. Certificates, provisioning profiles, keys... You have to be utterly organized when using them in a development team.

I adopted the codesigning.guide concept through Fastlane. Basically this idea comes up with having a specific git repository to store and distribute certificates across a development team. I store both iOS and Android code signing files on an encrypted private git repository that lives on GitHub.

Then, my CI workflows on every deploy clone the repository and install the decrypted certificates. On iOS the workflow creates an OS X Keychain where the certificates are installed.

Version management 🔖

Native builds and stores require code version bumps.

Every platform has its own way to manage versions and build numbers. The difference between those two is that the version should be used as the public store number that identifies a new release, and the build number is an incremental identifier that bumps on every build.

Android 🤖

  • Public version number: versionName
  • Build numbers: VERSION_CODE

iOS 🍏

  • Public version number: CFBundleShortVersionString
  • Build numbers: CFBundleVersion and CURRENT_PROJECT_VERSION

These attributes are stored on .plist, .pbxproj, .properties and .gradle files. To automate and do version management I use the package.json version number as the source of truth for public version numbers 💯. This allows me to use npm version cli command to manage bumps.

Native builds 📦

I need to provision two environments to build and compile native applications.

For iOS I need a macOS runner with Xcode, because it's the only way to compile and sign the application. On Android I use an Ubuntu runner, with all the Android SDK packages and tools needed.

These environments are created by GitHub Actions, which means every build runs on a new fresh and clean environment 💻.

Beta testing distribution 🐤

To distribute the application to beta testers I use TestFlight on iOS and HockeyApp for Android. I tried Google Play Beta but it was too slow on the app roll out compared to HockeyApp.

Stores distribution 🚀

To distribute the application to the stores I upload the production build to TestFlight on iOS and Google Play Store for Android. The release is done manually by a human being.

Sourcemaps 🗺

To get human readable information about crashes and errors, I use a service called Bugsnag. Every time I deploy a new build, I need to upload debug symbols .dSYM and sourcemaps to Bugsnag.

Communication 🗣

Finally, when the apps are deployed, I need to inform beta testers, release manager and developers, that there's a new version. I use Slack with a bot that sends alerts to specific channels.

The problem

Every time I wanted to do a release, I had to manually fire 🔥 the Fastlane deployment lanes. That means that human factor was needed. This was a time consuming process that often failed due to code sign, biased environments, software updates, native platform dependencies...

Machines should work, people should think.

Definitely I decided to end with those problems by automating all the things!

The solution

The solution is to implement this automated process into a system that continuously delivers master branch pushes up to the stores magically 🎉, giving freedom to the manager to decide when a new release comes up. Finally, I could forget about everything and be happy! ❤️

Now I'm going to show how I integrated GitHub Actions and Fastlane to automate the deployment of my apps 👏.

GitHub Actions Configuration

I use GitHub Actions workflows to run my deployment process in three steps, sequentially. This allows me to deploy apps only on the master branch when tests have passed ✅.

Here's my workflow configuration:

name: React Native CI/CD
 
on:
  push:
    branches: [master]
 
jobs:
  test:
    name: Test and lint ✅
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: "8.5.0"
      - run: yarn install
      - run: npm run test:lint && npm run test:unit
 
  deploy-android:
    name: Deploy Android 🤖
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-java@v2
        with:
          java-version: "8"
          distribution: "adopt"
      - uses: actions/setup-node@v2
        with:
          node-version: "8.5.0"
      - name: Install dependencies
        run: |
          yarn install
          gem install bundler
          bundle install
      - name: Setup Git config
        run: ./internals/scripts/github/gitconfig.sh
      - name: Deploy Android
        run: npm run deployment:android
 
  deploy-ios:
    name: Deploy iOS 🍏
    needs: test
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: "8.5.0"
      - name: Install dependencies
        run: |
          yarn install
          gem install bundler
          bundle install
      - name: Setup Git config
        run: ./internals/scripts/github/gitconfig.sh
      - name: Deploy iOS
        run: npm run deployment:ios

The workflow consists of three jobs:

  1. Test job ✅ - Runs linters and test suites to ensure everything is working as expected. If something fails here, we automatically stop the deploy.

  2. Android job 🤖 - Uses an Ubuntu runner with JDK and Android SDK installed. Builds both Canary 🐤 and Production 🚀 applications. Takes around 15 minutes ⏰ to ship Android apps.

  3. iOS job 🍏 - Uses a macOS runner with Xcode installed. Builds both Canary 🐤 and Production 🚀 apps. Takes around 20 minutes ⏰ to ship iOS apps.

Lessons learned

  • Avoid human factor as much as you can, by automating all the things!
  • Native ecosystem is tough, sometimes kind of frustrating and you should accept that. It's not my expertise since I'm a JS dev, but there's a lot of people and documentation that helps out.
  • Make processes repeatable and automated.