Effortless React Native Deployment: Leveraging Github Actions for Automation

Effortless React Native Deployment: Leveraging Github Actions for Automation

Overview

Are you tired of the time-consuming manual process of building and deploying React Native applications? In this article, I'll introduce an automation workflow using Github Actions, a game-changer for future mobile app development projects. I'm documenting this process here, so I don't forget it in the future.

Setting Up the Android Workflow

To get started with automating the Android deployment process, create a directory from your project root .github/workflows/ and create a new workflow called deploy-android.yml with the following content:

name: Deploy android app
on:
   push: # explanation1
      branches:
          - main
jobs:
   deploy-android:
       runs-on: ubuntu-latest
       container: reactnativecommunity/react-native-android
       steps:
          - name: Checkout # explanation2
            uses: actions/checkout@v2

          - name: Set up JDK environment # explanation3
            uses: actions/setup-java@v1.4.3
            with:
              java-version: 1.8

          - name: Set up Android SDK # explanation4
            uses: android-actions/setup-android@v2

          - name: Set up Android Build-tools # explanation5
            uses: maxim-lobanov/setup-android-tools@v1
            with:
                cache: true
                packages: |
                   platforms;android-29
                   build-tools;29.0.3

          - name: Extract branch name # explanation6
            shell: bash
            run: echo "::set-output name=branch::${GITHUB_REF#refs/heads/}"

          - name: Get yarn cache directory path # explanation7
            id: yarn-cache-dir-path
            run: echo "::set-output name=dir::$(yarn cache dir)"

          - name: Restore node_modules from cache # explanation8
            uses: actions/cache@v2
            id: yarn-cache
            with:
                path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
                key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
                restore-keys: |
                   ${{ runner.os }}-yarn-

          - name: Install dependencies # explanation9
            uses: actions/cache@v2
            with:
               path: ~/.gradle/caches
               key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
               restore-keys: |
                 ${{ runner.os }}-gradle-caches-

          - name: Bundle asset # explanation10
            run: |
              npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/

          - name: Remove duplicate resource # explanation11
            run: |
               git config --global --add safe.directory /__w/mcportal-app/mcportal-app && git ls-files --others --exclude-standard | grep drawable | xargs rm

          - name: Build Artifact # explanation12
            run: |
              cd android && ./gradlew assembleDevelopment --no-daemon

          - name: Sign APK # explanation13
            id: sign_app
            uses: r0adkll/sign-android-release@v1
            with:
              releaseDirectory: android/app/build/outputs/apk/Development/release
              signingKeyBase64: ${{ secrets.ANDROID_SIGINING_KEY }}
              alias: ${{ secrets.ANDROID_ALIAS }}
              keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}
              keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}

         - name: Upload artifact to deploy gate # explanation14
           working-directory: ./android
           run: |
             curl \
               -H "Authorization: token ${{secrets.DEPLOY_GATE_API_KEY}}" \
               -F "file=@app/build/outputs/apk/Development/release/app-Development-release.apk" \
               -F "message=$(git rev-parse --short HEAD)" \
               -F "distribution_name=${{ steps.extract_branch.outputs.branch }}" \
               -v "https://deploygate.com/api/users/${{secrets.DEPLOY_GATE_USER_NAME}}/apps"        

I've assigned an explanation number to each step, which I'll clarify here.

Explanation 1: Configuring Triggers in Github Actions

GitHub Actions offers a wide range of triggers to schedule workflows. This command instructs the Continuous Integration (CI) system on when to execute the workflow. For more details, you can explore further in the provided resource.

Explanation 2: Setting Up the Workflow

Each job in GitHub Actions represents a clean daemon process. To ensure that the workflow runs on the correct remote-tracking branch, we must follow it up with the 'git checkout' process.

Explanation 3: JDK Environment Setup

For building the Android application, it's essential to establish the Java Development Kit (JDK) environment.

Explanation 4: Android SDK Configuration

However, JDK alone isn't sufficient. We also need to configure the Android SDK to enable the installation of build tools.

Explanation 5: Build Tools Installation

To build the Android app successfully, we must install build tools. I've chosen version 29.0.3, as it aligns with the APK signing process. You can opt for different versions, but remember to adjust it when signing the APK in the subsequent step.

Explanation 6: Extracting the Branch Name

This step involves extracting the branch name, which is necessary for referencing when uploading to DeployGate.

Explanation 7: Fetching Yarn Cache Directory

Fetching the Yarn cache directory is crucial as it can significantly reduce the build time in most cases.

Explanation 8: Restoring node_modules from Cache

Similarly to the previous step, restoring 'node_modules' from the cache can save substantial build time in many scenarios.

Explanation 9: Installing Dependencies

This step installs the dependencies listed in 'package.json' while utilizing '--frozen-lockfile' to ensure consistent package version installation.

Explanation 10: Asset Bundling

Asset bundling is a crucial step in the process, just as it would be when performed manually.

Explanation 11: Removing Extraneous Resources

During asset bundling, certain additional resources can be copied, potentially leading to build errors. To prevent this, we delete these resources before initiating the build process, leveraging Git commands on the server.

Explanation 12: Building the APK

To build the APK file, we employ the 'assembleDevelopment' method in this case, as I have a 'Development' flavor specified in my 'build.gradle.' If you don't have additional flavors, the typical scenario would be 'assembleRelease.'

Explanation 13: APK Signing

This step involves signing the generated APK file in preparation for deployment.

Explanation 14: Uploading to DeployGate To finalize the process, we upload the APK file to DeployGate using the DeployGate API.


Within the aforementioned workflow configuration, six secrets must be added to the repository. Let's delve into each one individually for a comprehensive understanding.

ANDROID_SIGNING_KEY: This secret corresponds to the .keystore file used in manual processes, generated with the following command:

$ keytool -genkeypair -v -storetype PKCS12 -keystore my-upload-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000        

Creating the Key File If you don't have this key file, it's necessary to create one. Every trigger in GitHub Actions functions as a cron process, and once it completes, it's terminated, whether it ends in success or failure. In this context, we cannot directly place the .keystore file on the server because GitHub Actions secrets only support string values. So, how do we handle this situation? The solution is to convert the .keystore file into a base64-encoded string. You can achieve this by using the command below:

$ openssl base64 < my-upload-key.keystore  | tr -d '\n' | tee my-upload-key.keystore.base64.txt        

Saving Secrets in GitHub Actions

  1. Copy the 'stdout' from the previous step and store it in the GitHub Actions secrets section of your repository.

Secrets to Configure:

  • ANDROID_ALIAS: This should match the alias you assigned when creating your keystore.
  • ANDROID_KEY_STORE_PASSWORD: Use the password associated with your keystore during its creation.
  • ANDROID_KEY_PASSWORD: Input the password related to your keystore's key.
  • DEPLOY_GATE_API_KEY: To obtain your API key, log in to your DeployGate account, navigate to Settings, scroll down, and you'll find the API Key section. For more details, refer to the documentation.


Setting Up the iOS Workflow

Just like the Android setup, create a file named 'deploy-ios.yml' within the '.github/workflows' directory and insert the following contents:

name: Deploy iOS app
on:
   push:
     branches:
        - main

jobs:
  deploy-ios:
    runs-on: macOS-latest
    defaults:
       run:
         working-directory: ios
   steps:
     - name: Checkout
       uses: actions/checkout@v2

     - name: Extract branch name
       shell: bash
       run: echo "::set-output name=branch::${GITHUB_REF#refs/heads/}"
       id: extract_branch

     - name: Get yarn cache directory path
       id: yarn-cache-dir-path
       run: echo "::set-output name=dir::$(yarn cache dir)"

     - name: Restore node_modules from cache
       uses: actions/cache@v2
       id: yarn-cache
       with:
         path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
         key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
         restore-keys: |
             ${{ runner.os }}-yarn-

     - name: Install dependencies
       run: yarn install --frozen-lockfile --network-timeout 300000

     - name: Setup Ruby # explanation1
       uses: ruby/setup-ruby@v1
       with:
         ruby-version: 2.7.4
         bundler-cache: true

     - name: Restore Pods Cache # explanation2
       uses: actions/cache@v2
       with:
         path: |
           ios/Pods
           ~/Library/Caches/CocoaPods
           ~/.cocoapods
         key: ${{ runner.os }}-pods-${{ hashFiles('ios/Podfile.lock') }}
         restore-keys: |
           ${{ runner.os }}-pods-

     - name: Install Pods # explanation3
       run: pod install --repo-update && cd ..

     - name: Build iOS App # explanation4
       uses: yukiarrr/ios-build-action@v1.4.0
       with:
         project-path: ios/MyApp.xcodeproj
         p12-base64: ${{ secrets.IOS_P12_BASE64 }}
         mobileprovision-base64: ${{ secrets.IOS_MOBILE_PROVISION_BASE64 }}
         code-signing-identity: 'Apple Distribution'
         team-id: ${{ secrets.IOS_TEAM_ID }}
         certificate-password: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
         workspace-path: ios/MyApp.xcworkspace
         scheme: MyApp-dev
         export-method: "ad-hoc"
         output-path: "ios/MyApp.ipa"
         update-targets: |
           MyApp-dev

     - name: Upload artifact to deploy gate # explanation5
       run: |
         curl \
           -H "Authorization: token ${{secrets.DEPLOY_GATE_API_KEY}}" \
           -F "file=@MyApp.ipa" \
           -F "message=$(git rev-parse --short HEAD)" \
           -F "distribution_name=${{ steps.extract_branch.outputs.branch }}" \
           -v "https://deploygate.com/api/users/${{secrets.DEPLOY_GATE_USER_NAME}}/apps"        

Explanation 1: Ruby Prerequisite Ruby is an essential requirement for executing pod commands.

Explanation 2: Caching Pods Restoring the pods cache can significantly reduce build time.

Explanation 3: Pod Installation Before building the app, it's crucial to run 'pod install' to manage dependencies.

Explanation 4: Building the App For this step, I've utilized my own configuration settings. If you wish to define your own settings, please refer to the official documentation.

Explanation 5: Placing the IPA File In 'Explanation 4,' I've positioned the IPA file in the 'ios' directory because, at this point, I'm already within the 'ios' directory.


NOTE: The method used to generate a base64 string is the same as what was explained for the ANDROID_SIGNING_KEY above.


Conclusion

In this article, we've embarked on a journey to streamline the arduous process of building and deploying React Native applications. The manual labor of these tasks can be not only time-consuming but also prone to human error. With the power of automation using GitHub Actions, we've paved the way for a more efficient, error-free future in mobile app development.

Throughout our journey, we've covered a range of crucial steps, from setting up Android and iOS workflows to managing secrets and optimizing build times. We've demystified the process of transforming key files into base64 strings and configuring GitHub Actions for success.

As we wrap up this guide, it's essential to emphasize the importance of automation in modern app development. With this newfound knowledge, you're now equipped to leverage the full potential of GitHub Actions and harness its capabilities for your projects. By automating build and deployment processes, you'll save time, reduce errors, and ensure a smoother development experience for yourself and your team.

The road to automation might seem daunting at first, but the benefits it offers are undeniable. So, the next time you embark on a React Native project, remember that GitHub Actions is your trusted companion in achieving a seamless and efficient development process. Now, you can take this knowledge forward and apply it to your future mobile app endeavors.

Happy coding and automated deploying!

To view or add a comment, sign in

More articles by Ken Phanith

  • Using Google Cloud Pub/Sub in a Local Environment

    Google Cloud Pub/Sub is a robust, serverless messaging service that allows you to send and receive messages between…

  • 5 ways to turn WordPress into an API

    Introduction While I may not hold a strong preference for WordPress, it's important to acknowledge that recreating its…

Others also viewed

Explore content categories