Zain Sajjad Head of Product Experience at Peekaboo Guru. In love with Mobile Machine Learning, React, React Native and User Interface Designing.

React Native CI/CD using GitHub Actions

6 min read 1849

React Native CI/CD Using GitHub Actions

Time is the most valuable asset we humans have. As developers, we spend far too much of that time doing two things: staring at our code wondering why the hell it’s not working, and waiting for our apps to build. Nobody likes to wait for the build to complete and then distribute it before going home. Why not automate it? CI/CD? Yes! And for React Native apps, too.

Today, we have great tools available for CI/CD of React Native apps. GitHub Actions, GitLab CI/CD, CircleCI, Travis CI, and many others are helping developers save loads of time and resources. Today, we will use Github Actions for the CI/CD of our React Native application.

This post is a continuation of our previous post that discusses testing React Native apps with React Native Testing Library. Although it isn’t exactly prerequisite reading, we discussed a few pre-commit tasks to maintain the quality of code. Building on the same app, we will complete the CI/CD pipeline here.

A bit about CI/CD with GitHub Actions

GitHub Actions allows us to define workflows that will run based on their associated conditions. Every repository can contain multiple workflows that trigger different jobs based on different events. GitHub on every trigger picks all YAML files in .github/workflows/ and executes the required workflows. If a workflow ends abruptly, then the action is marked as a failure.

To fulfill some basic tasks like checking our project or caching files, GitHub has a wide range of officially maintained actions. We can also leverage several open-source actions maintained by the community.

Our targets

So, our set of requirements looks like this:

  • On every pull request on the develop branch, execute all tests [CI]
  • On every push on the develop branch, distribute our Android application via Firebase app distribution [CD]
  • On every push on the alpha branch, publish an alpha release of our application via Google Play Console [CD]
  • On every push on the main branch, publish a beta release of our application via Google Play Console [CD]

As indicated, our first requirement belongs to the CI (continuous integration) phase. That allows us to establish an automated way to keep development moving fast without breaking our application. CD (continuous delivery) picks up where CI ends. CD automates the delivery of our applications to the selected environments.

Adding a CI workflow for our React Native app

To recap, we have two different kinds of workflows: CI and CD. Any workflow can be executed on different triggers; in our case, the CI workflow will be executed on every pull request on protected branches, whereas CD workflows are executed on every push on protected branches.

To set up the CI workflow for our repo, we will add a new file ci.yml in .github/workflows/.

Defining our CI workflow

Our CI workflow trigger is a pull request on the protected branch. Here is how our workflow configurations should look:

We made a custom demo for .
No really. Click here to check it out.

name: Continuous Integration

      - develop
      - alpha 
      - main

As seen, this will execute on every pull request on defined branches.

Project checkout

Every workflow that we define via GitHub Actions is executed in a separate virtual machine. To execute anything on our codebase, we need to check out the repo on the assigned instance. As discussed earlier, GitHub has a wide variety of utility actions available to handle such tasks. Here we will use the Checkout action:

    - name: Checkout
      uses: actions/[email protected]

Installing dependencies

Once our checkout is complete, we will set up our environment and project. To run any Node.js project in a GitHub VM, we can use the official Node setup action.

Since we are executing our tests using React Native Testing Library, we have to install some npm dependencies. In this step, we will set up a Node environment on runner and install our npm dependencies.

    - uses: actions/[email protected]
    - uses: c-hive/[email protected]

    - name: Install node modules
      run: |
        yarn install

To make our subsequent actions, we can cache the npm dependencies for our project. We have leveraged C-Hive’s one-liner Yarn module cache action.

Running our test

Now that the Node environment and dependencies are available, let’s start executing our test suites. This is as simple as running our test using the local CLI.

    - name: Run test
      run: |
        yarn test-ci

As discussed, this job will execute on every pull request; GitHub will display the result of the pipeline execution with every PR. Here’s a screenshot of how it looks:

Successful Execution of the Pipeline in GitHub Actions

This wraps up our CI workflow. This workflow will make our application more scalable in terms of code quality as any code that is merged in our repo will be passing through our quality checks.

Adding our build job

Since we have a decisive CI phase intact, any code that is merged into our protected branches passes all tests. Thus, once any PR is merged, it is safe to build our application. In our CD pipelines, we will begin with building our apps and publishing them as per our requirements.

For Android apps, the steps for our build job are as follows:

Setting up Gradle cache

Just like we did with out npm dependencies, we will cache our Gradle dependencies and wrapper to keep our builds faster. We’ll use GitHub’s Cache action here:

    - name: Cache Gradle Wrapper
      uses: actions/[email protected]
        path: ~/.gradle/wrapper
        key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('gradle/wrapper/') }}

    - name: Cache Gradle Dependencies
      uses: actions/[email protected]
        path: ~/.gradle/caches
        key: ${{ runner.os }}-gradle-caches-${{ hashFiles('gradle/wrapper/') }}
        restore-keys: |
          ${{ runner.os }}-gradle-caches-

We are caching our Gradle dependencies and wrapper separately to ensure they are available for subsequent builds and to save us some valuable CI/CD minutes.

Generating the Android release build

Now we will start the build process for our Android app:

    - name: Make Gradlew Executable
      run: cd android && chmod +x ./gradlew

    - name: Build Android App Bundle
      run: |
        cd android && ./gradlew bundleRelease --no-daemon

In some Linux environments, Gradle is not executable by default. So, to be on the safe side, we make sure gradlew is executable before we start our heavy build process.

Since all actions are executed as separate instances, having the Gradle daemon running doesn’t make sense; it will create an extra burden on memory. Therefore, we have added a --no-daemon flag to our build step.

Signing the Android release

For the security of the app, it’s recommended that we keep signing properties outside of the codebase. Once our build is complete, we will have an unsigned app bundle available. To sign this bundle, we will use an open-source signing action, r0adkll/sign-android-release.

Here is our step to sign a release:

    - name: Sign App Bundle
      id: sign_app
      uses: r0adkll/[email protected]
        releaseDirectory: android/app/build/outputs/bundle/release
        signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }}
        alias: ${{ secrets.ANDROID_SIGNING_ALIAS }}
        keyStorePassword: ${{ secrets.ANDROID_SIGNING_STORE_PASSWORD }}
        keyPassword: ${{ secrets.ANDROID_SIGNING_KEY_PASSWORD }}

This task uses secrets from our project. We can generate signingKeyBase64 using this command:

openssl base64 < some_signing_key.jks | tr -d '\n' | tee some_signing_key.jks.base64.txt

The content of the output .txt file will be a Base64 signing key.

Uploading an artifact

GitHub Actions allows us to save the output of any job. These files can be downloaded later for testing or backup purposes. We will upload our signed app as an artifact of this job.

    - name: Upload Artifact
      uses: actions/[email protected]
        name: Signed App Bundle
        path: ${{steps.sign_app.outputs.signedReleaseFile}}

One crucial piece here is the path of our signed APK; this was generated via our previous step, sign_app, which output the path for the signed APK. Here, we will use the same path.

This concludes the build phase of the job! The signed bundle or APK is now available for us.

Distributing our app via Firebase

As mentioned in our requirements above, on every push to the develop branch, we have to distribute our application using Firebase App Distribution. To use Firebase CLI, we will install firebase-tools globally on the instance and make its PATH available.

    - name: Distribute app via Firebase App Distribution
          firebaseToken: ${{ secrets.FIREBASE_TOKEN }}
          firebaseGroups: ${{ secrets.FIREBASE_GROUPS }}
          firebaseAppId: ${{ secrets.FIREBASE_APP_ID }}
          notes: ${{ github.event.head_commit.message }}
      run: |
        yarn global add firebase-tools
        export PATH="$(yarn global bin):$PATH"
        firebase \
          appdistribution:distribute android/app/build/outputs/apk/release/app-release.apk \
          --app $firebaseAppId \
          --release-notes "$notes" \
          --groups "$firebaseGroups" \
          --token "$firebaseToken"

Here we are using firebaseToken, firebaseGroups, and firebaseAppId, all coming from our secrets. We have set these values from the secret in our env and then used env variables in our CLI command. This is another way of accessing secrets and other variables.

Also, we have accessed the message of the head commit from the GitHub event. Broad information about the job, commit, etc. is available in the workflow context.

Distributing via Play Console

Let’s go for a home run. In this step, we will distribute our signed app via Google Play Console. This process requires setting up the Google Play Developer API and attaching it with Play Console. On completion of this setup, we’ll get a JSON file with all the information about the service account. We’ll place that in our project secrets.

For this step, we will use the open-source r0adkll/upload-google-play, which does all the heavy lifting related to the Play API.

    - name: Deploy to Play Store (BETA)
      uses: r0adkll/[email protected]
        serviceAccountJsonPlainText: ${{ secrets.ANDROID_SERVICE_ACCOUNT }}
        packageName: com.testedapp
        releaseFile: a${{steps.sign_app.outputs.signedReleaseFile}}
        track: beta
        inAppUpdatePriority: 3
        userFraction: 0.5
        whatsNewDirectory: android/release-notes/

Here comes the euphoria — our tested app is now delivered to the end user without any manual intervention!

What’s next?

As discussed in the beginning, the main goal of CI/CD is to save developer time and keep the quality of the codebase and resulting application intact. Leveraging GitHub Actions can save loads of time that we would have spent on manually building and distributing our applications.

CI will also allow developers to pull only code that passes all our quality standards and tests, meaning fewer bugs and faster iterations. What else could make you happier — aside from free pizza?

One vital feature that I missed while working with GitHub Actions, however, is the ability to share steps among different jobs. In our case, we had to clone, install dependencies, and run tests in all of our jobs.

Currently, steps are defined in all of the scripts; this redundancy can be avoided if we share steps between different jobs. Let’s keep track of this discussion and hope this will arrive in GitHub Actions soon.

You can dive in further and see how multiple jobs can be executed in parallel and interdependently. Also, you can create workflow templates to share across different projects. Share the cool stuff you’re doing with GitHub Actions and React Native in the comments below!

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

Zain Sajjad Head of Product Experience at Peekaboo Guru. In love with Mobile Machine Learning, React, React Native and User Interface Designing.

Leave a Reply