How to upload React Native builds with Github Actions

Sanjin Sehic
ScaleUp
Published in
8 min readNov 30, 2023

Introduction

In my previous article I have explained how you can build both Android and iOS apps with Github actions. You can see that on this link: https://medium.com/scaleuptech/how-to-make-react-native-builds-with-github-actions-8d0203801eff

In this article I will explain how you can upload those builds to Firebase App Distribution for testing, as well as how to upload them to Play store and App store for production.

Uploading to Firebase App Distribution

Using the ‘staging’ branch for uploading to Firebase App Distribution streamlines the testing process. It provides a controlled environment for testers to thoroughly evaluate the app’s performance across various devices. This approach ensures that potential issues are identified and resolved before the app reaches a wider audience during the production phase.

Before we go to actual workflows you will need to do few things and add few Github secrets.

appId

Go to your project on Firebase and go to general settings page. There you can find iOS appId and Android appId.

Now that you have found them you will need to add them to repository secrets. To do this go to your project on Github, press Settings, then Secrets and variables and then Actions. There you will see button ‘New repository secret’. You are going to need two repository secrets. Here they are:

FIREBASE_ANDROID_APP_ID
Paste Android appId into the textbox labeled Secret.

FIREBASE_IOS_APP_ID
Paste iOS appId into the textbox labeled Secret.

serviceCredentialsFileContent

Follow these steps to get your serviceCredentialsFileContent:

  1. Open Google Cloud Console
  2. Select your project
  3. Click on Create Service Account
  4. Enter account name and press Create and Continue
  5. Select Firebase App Distribution Admin in Role input and click Done
  6. Then, on the list of accounts find your newly created service account. Click on 3-dots menu and click on Manage keys
  7. Click on Add key and then Create new key
  8. Select type JSON and click Create
  9. After that, the file should be downloaded to your disk. Open the file, and copy its content.
  10. Create a new secret. Name it CREDENTIAL_FILE_CONTENT. The value of the secret is the content from the file copied in point 9.

Now you are all set up and we can go to actual workflows.

Uploading iOS app to App Distribution

Here is the part of yaml file that is handling upload ipa file to App Distribution.

  distribute_on_linux:
runs-on: ubuntu-latest
needs: [build_with_signing]
steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Download artifact
uses: actions/download-artifact@v2
with:
name: your_app.ipa

- name: Upload Artifact to Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_IOS_APP_ID }}
serviceCredentialsFileContent: ${{ secrets.FIREBASE_CREDENTIAL_FILE_CONTENT }}
testers: |
example1@gmail.com
example2@gmail.com
file: your_app.ipa

And here is the whole yaml file:

name: "Build iOS Staging app"

on:
push:
branches:
- staging

jobs:
build_with_signing:
runs-on: macos-latest
steps:
- name: check Xcode version
run: /usr/bin/xcodebuild -version
- name: checkout repository
uses: actions/checkout@v3
- name: Debug Workflow Variables
run: |
echo "CERTIFICATE_PATH: $CERTIFICATE_PATH"
echo "PP_PATH: $PP_PATH"
echo "KEYCHAIN_PATH: $KEYCHAIN_PATH"
echo "P12_PASSWORD: $P12_PASSWORD"
- name: Install the Apple certificate and provisioning profile
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
PROVISION_PROFILES_BASE64: ${{ secrets.PROVISION_PROFILES_BASE64 }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
PP_ARCHIVE=$RUNNER_TEMP/mobile_pp.tgz
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db

echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
echo -n "$PROVISION_PROFILES_BASE64" | base64 --decode -o $PP_ARCHIVE

security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH

echo "P12_PASSWORD: $P12_PASSWORD"
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH

mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
tar xzvf $PP_ARCHIVE -C $RUNNER_TEMP
for PROVISION in `ls $RUNNER_TEMP/*.mobileprovision`
do
UUID=`/usr/libexec/PlistBuddy -c 'Print :UUID' /dev/stdin <<< $(security cms -D -i $PROVISION)`
cp $PROVISION ~/Library/MobileDevice/Provisioning\ Profiles/$UUID.mobileprovision
done

security find-identity -v -p codesigning
ls -l ~/Library/MobileDevice/Provisioning\ Profiles

- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'

- name: Clean workspace
run: |
git clean -ffdx
npm cache clean --force

- name: Clean Xcode Build
run: |
cd ios
xcodebuild clean -workspace your_app.xcworkspace -scheme your_app

- name: install yarn dependencies
run: |
cd ios
yarn install

- name: install Cocoapod dependencies
run: |
cd ios
pod repo update
pod install

- name: build archive
run: |
cd ios
xcodebuild -workspace your_app.xcworkspace \
-scheme "your_app" \
-sdk iphoneos \
-configuration Release \
-destination generic/platform=iOS \
-archivePath $RUNNER_TEMP/your_app.xcarchive \
archive

- name: export ipa
env:
EXPORT_OPTIONS_PLIST: ${{ secrets.EXPORT_OPTIONS_PLIST }}
run: |
EXPORT_OPTS_PATH=$RUNNER_TEMP/ExportOptions.plist
echo -n "$EXPORT_OPTIONS_PLIST" | base64 --decode -o $EXPORT_OPTS_PATH
xcodebuild -exportArchive -archivePath $RUNNER_TEMP/your_app.xcarchive -exportOptionsPlist $EXPORT_OPTS_PATH -exportPath $RUNNER_TEMP/build

- name: Upload application
uses: actions/upload-artifact@v3
with:
name: your_app.ipa
path: ${{ runner.temp }}/build/Apps/your_app.ipa

distribute_on_linux:
runs-on: ubuntu-latest
needs: [build_with_signing]
steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Download artifact
uses: actions/download-artifact@v2
with:
name: your_app.ipa

- name: Upload Artifact to Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_IOS_APP_ID }}
serviceCredentialsFileContent: ${{ secrets.FIREBASE_CREDENTIAL_FILE_CONTENT }}
testers: |
example1@gmail.com
example2@gmail.com
file: your_app.ipa

Important thing is that you must have two jobs for this, first is building and exporting ipa file that runs on macos, and second is downloading that ipa and uploading it to App Distribution that runs on ubuntu. In second job you must have this line:

needs: [build_with_signing]

because you need to first get your ipa file to be able to upload it.

Uploading Android app to App Distribution

Here is the part of yaml file that is handling upload apk file to App Distribution.

      - name: Upload Artifact to Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_ANDROID_APP_ID }}
serviceCredentialsFileContent: ${{ secrets.FIREBASE_CREDENTIAL_FILE_CONTENT }}
testers: |
examle1@gmail.com
example2@gmail.com
file: android/app/build/outputs/apk/release/app-release.apk

And here is the whole yaml file:

name: "Build Android Staging app"

on:
push:
branches:
- staging

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Install Java
uses: actions/setup-java@v3
with:
java-version: 17
distribution: adopt
cache: gradle

- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@v1

- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'

- name: Run Yarn Install
run: |
npm i -g corepack
yarn install

- name: Build application
run: |
cd android
./gradlew assembleRelease

- name: Upload application
uses: actions/upload-artifact@v2
with:
name: app
path: android/app/build/outputs/apk/release/app-release.apk
retention-days: 3

- name: Upload Artifact to Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_ANDROID_APP_ID }}
serviceCredentialsFileContent: ${{ secrets.FIREBASE_CREDENTIAL_FILE_CONTENT }}
testers: |
examle1@gmail.com
examle2@gmail.com
file: android/app/build/outputs/apk/release/app-release.apk

Uploading to App store and Play store

When your app is primed for production, the next step is uploading it to the respective stores from the ‘main’ branch.

Uploading iOS app to App store Testflight

Before we go to actual workflow you will need to do few things and add few Github secrets.

Create a new secret. Name it APPLE_DEVELOPER_USERNAME. The value of the secret is username you specify is your App Store Connect user name.

Create one more secret. Name it APPLE_DEVELOPER_PASSWORD. You will have to go here: https://appleid.apple.com/account/manage and create new App-specific password. Input that password as a value of this secret.

Here is the part of yaml file that is handling upload ipa file to Testflight:

   - name: Publish to TestFlight
env:
USERNAME: ${{ secrets.APPLE_DEVELOPER_USERNAME }}
PASSWORD: ${{ secrets.APPLE_DEVELOPER_PASSWORD }}
run: |
cd ios && xcrun altool --upload-app -f $RUNNER_TEMP/build/Apps/your_app.ipa -t ios -u $USERNAME -p $PASSWORD

And here is the whole yaml file:

name: "Build iOS Production app"

on:
push:
branches:
- main

jobs:
build_with_signing:
runs-on: macos-latest
steps:
- name: check Xcode version
run: /usr/bin/xcodebuild -version
- name: checkout repository
uses: actions/checkout@v3
- name: Debug Workflow Variables
run: |
echo "CERTIFICATE_PATH: $CERTIFICATE_PATH"
echo "PP_PATH: $PP_PATH"
echo "KEYCHAIN_PATH: $KEYCHAIN_PATH"
echo "P12_PASSWORD: $P12_PASSWORD"
- name: Install the Apple certificate and provisioning profile
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
PROVISION_PROFILES_BASE64: ${{ secrets.PROVISION_PROFILES_BASE64 }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
PP_ARCHIVE=$RUNNER_TEMP/mobile_pp.tgz
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db

echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
echo -n "$PROVISION_PROFILES_BASE64" | base64 --decode -o $PP_ARCHIVE

security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH

echo "P12_PASSWORD: $P12_PASSWORD"
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH

mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
tar xzvf $PP_ARCHIVE -C $RUNNER_TEMP
for PROVISION in `ls $RUNNER_TEMP/*.mobileprovision`
do
UUID=`/usr/libexec/PlistBuddy -c 'Print :UUID' /dev/stdin <<< $(security cms -D -i $PROVISION)`
cp $PROVISION ~/Library/MobileDevice/Provisioning\ Profiles/$UUID.mobileprovision
done

security find-identity -v -p codesigning
ls -l ~/Library/MobileDevice/Provisioning\ Profiles

- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'

- name: Clean workspace
run: |
git clean -ffdx
npm cache clean --force

- name: Clean Xcode Build
run: |
cd ios
xcodebuild clean -workspace your_app.xcworkspace -scheme your_app

- name: install yarn dependencies
run: |
cd ios
yarn install

- name: install Cocoapod dependencies
run: |
cd ios
pod repo update
pod install

- name: build archive
run: |
cd ios
xcodebuild -workspace your_app.xcworkspace \
-scheme "your_app" \
-sdk iphoneos \
-configuration Debug \
-destination generic/platform=iOS \
-archivePath $RUNNER_TEMP/your_app.xcarchive \
archive

- name: export ipa
env:
EXPORT_OPTIONS_PLIST: ${{ secrets.EXPORT_OPTIONS_PLIST }}
run: |
EXPORT_OPTS_PATH=$RUNNER_TEMP/ExportOptions.plist
echo -n "$EXPORT_OPTIONS_PLIST" | base64 --decode -o $EXPORT_OPTS_PATH
xcodebuild -exportArchive -archivePath $RUNNER_TEMP/your_app.xcarchive -exportOptionsPlist $EXPORT_OPTS_PATH -exportPath $RUNNER_TEMP/build

- name: Upload application
uses: actions/upload-artifact@v3
with:
name: app
path: ${{ runner.temp }}/build
retention-days: 3

- name: Publish to TestFlight
env:
USERNAME: ${{ secrets.APPLE_DEVELOPER_USERNAME }}
PASSWORD: ${{ secrets.APPLE_DEVELOPER_PASSWORD }}
run: |
cd ios && xcrun altool --upload-app -f $RUNNER_TEMP/build/Apps/your_app.ipa -t ios -u $USERNAME -p $PASSWORD

Uploading Android app to Play store Internal testing

You will need to create new secret and name it SERVICE_ACCOUNT_JSON.
Follow these steps to get your service account json:

  1. Open Google Cloud Console
  2. Select your project
  3. Click on Create Service Account
  4. Pick a name and add appropriate permissions (e.g. ‘owner)
  5. Open the newly created service account, click on keys tab and add a new key, JSON type
  6. When successful, a JSON file will be automatically downloaded on your machine
  7. Store the content of this file to your SERVICE_ACCOUNT_JSON Github secret.

Now you will need to add user to Google Play Console. Do the following:

  1. Open Google Play Console and pick your developer account
  2. Open Users and permissions
  3. Click invite new user and add the email of the service account created in the previous step
  4. Grant permissions to the app that you want the service account to deploy in app permissions

Here is the part of yaml file that is handling upload aab file to Internal testing:

      - name: Upload application to Play Store
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.your_app
releaseFiles: android/app/build/outputs/bundle/release/app-release.aab
track: internal

By altering the value of ‘track’ in this configuration, you can determine the destination for your AAB file upload. When using ‘production’ as the track, your app will be automatically submitted for review. However, for more control over the release process, I’ve used ‘internal’ here. With this track, the app is uploaded for internal testing, allowing you to manually promote it to production at a later stage, ensuring better control and oversight over the release cycle.

And here is the whole yaml file:

name: "Build Android Production app"

on:
push:
branches:
- main

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Install Java
uses: actions/setup-java@v3
with:
java-version: 17
distribution: adopt
cache: gradle

- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@v1

- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'

- name: Run Yarn Install
run: |
npm i -g corepack
yarn install

- name: Build application
run: |
cd android
./gradlew bundleRelease

- name: Upload application
uses: actions/upload-artifact@v2
with:
name: app
path: android/app/build/outputs/bundle/release/app-release.aab
retention-days: 3

- name: Upload application to Play Store
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.your_app
releaseFiles: android/app/build/outputs/bundle/release/app-release.aab
track: internal

Conclusion

In this comprehensive guide, we’ve explored the power of GitHub Actions in automating the build and deployment processes for both iOS and Android apps. By implementing these workflows, developers can significantly streamline app distribution, simplify testing, and ensure a smoother transition to production.

The ability to seamlessly integrate GitHub Actions into your app development pipeline not only saves time but also minimizes manual errors, allowing teams to focus more on innovation and less on repetitive tasks.

As you embark on your automation journey, remember that these workflows are customizable. Tailor them to fit your project’s unique requirements and experiment with additional actions or configurations to optimize efficiency further.

Share your thoughts, experiences, or any questions you may have in the comments section below.

--

--

Sanjin Sehic
ScaleUp
Editor for

Front-end developer with three years of experience, I specialize in building mobile applications using React Native and web applications using React.