Author: Gergő Fándly
				
			
			Date:   2024-08-27
		
So you’ve been working on your shiny new React Native app for months, and now you’re ready to deploy it and share it with the public. You could do this manually for every release, but you’ve decided to do it the smart way and use a CI/CD pipeline to automate this tedious process.
In this guide I’m going to walk you through every aspect of creating an automated build pipeline using GitHub Actions both for Android and iOS, as well as generating the required signing certificates. The commands I’ll be using are executed on Linux, but they should work on MacOS as well without any changes. For Windows I’d recommend WSL.
To follow along with this guide you need to have a React Native application (if you’re using Expo, you need to eject) with its source code hosted on GitHub, and I assume you have already set up a Google Play developer account and an Apple Developer account.
Also, you should have already created the applications with the corresponding bundle IDs in App Store Connect and Google Play Console.
First of all, we need to make some changes to our application to allow automating its build process.
I’m going to start with the Android version of the application, since its human readable configuration format makes it easier to work with.
We need to make some configuration changes in android/app/build.gradle – the main configuration file for the android build:
At the very beginning of the file insert this:
plugins {
    id 'com.github.triplet.play' version '3.9.1'
}
This will install the Google Play Publisher plugin we will use to deploy the application.
After the lines starting with apply, add these lines:
// Load the application version from the package.json file
import groovy.json.JsonSlurper
def packageJson = new JsonSlurper().parseText(file("../../package.json").text)
// Get the build number. This needs to be an integer which is incremented with
// every build. We'll be using the GITHUB_RUN_NUMBER that fits perfectly
// these requirements. And we're defaulting to 1 to allow debug builds
def buildNumber = Integer.parseInt(System.getenv("GITHUB_RUN_NUMBER") ?: "1")
Now go to the defaultConfig block. It should look something like this:
defaultConfig {
    applicationId "com.example.your_app"
    minSdkVersion rootProject.ext.minSdkVersion
    targetSdkVersion rootProject.ext.targetSdkVersion
    versionCode 1
    versionName "1.0"
}
You need to change versionCode and versionName so the block will loke like this:
defaultConfig {
    applicationId "com.example.your_app"
    minSdkVersion rootProject.ext.minSdkVersion
    targetSdkVersion rootProject.ext.targetSdkVersion
    versionCode buildNumber
    versionName packageJson.version
}
Go to the signingConfigs block (it should be right under defaultConfig) and note that you already have a debug configuration that you have probably been using until now. We also need to add a release configuration, so paste this block right under the debug block:
release {
    if (System.getenv('ANDROID_KEYSTORE')) {
        storeFile file(System.getenv('ANDROID_KEYSTORE'))
        storePassword System.getenv('ANDROID_KEYSTORE_PASSWORD')
        keyAlias "upload-key"
        keyPassword System.getenv('ANDROID_KEYSTORE_PASSWORD')
    }
}
Now we have to tell gradle to use our new release configuration to make release builds. Find the buildTypes block and locate release inside it. You will probably see a warning in there: Caution! In production, you need to generate your own keystore file. We will do just that, so change the signingConfig parameter to signingConfigs.release.
At the end of the file add the following configuration for the play publisher:
play {
    if (System.getenv('PLAY_CREDENTIALS')) {
        serviceAccountCredentials.set(file(System.getenv('PLAY_CREDENTIALS')))
    }
    track.set('internal')
}
This tells the plugin which credentials to use and to publish the application on the internal testing track first.
Believe it or not, that’s all we need to do for the Android application. Basically we’re setting the version string to the version from package.json, the version code to the build number provided by GitHub Actions and we provide the code signing certificate using environment variables.
Now let’s continue with the iOS application, which can be a bit trickier. I’m not willing to use Xcode in this guide, since you can edit the files manually as well (and that would also force you to use MacOS), you just need to pay a bit more attention.
Let’s start with the harder part: open ios/<your project>.xcodeproj/project.pbxproj and find the configuration block which contains the Pods-<your project>.release.xcconfig comment for the baseConfigurationReference parameter. It will be something like this:
13B07F951A680F5B00A75B9A /* Release */ = {
	isa = XCBuildConfiguration;
	baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-example.release.xcconfig */;
	buildSettings = {
		ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
		CLANG_ENABLE_MODULES = YES;
		CURRENT_PROJECT_VERSION = 1;
		DEVELOPMENT_TEAM = XXXXXXXXXX;
		INFOPLIST_FILE = example/Info.plist;
		LD_RUNPATH_SEARCH_PATHS = (
			"$(inherited)",
			"@executable_path/Frameworks",
		);
		MARKETING_VERSION = 1.0;
		OTHER_LDFLAGS = (
			"$(inherited)",
			"-ObjC",
			"-lc++",
		);
		PRODUCT_BUNDLE_IDENTIFIER = "com.example.$(PRODUCT_NAME:rfc1034identifier)";
		PRODUCT_NAME = example;
		SWIFT_VERSION = 5.0;
		TARGETED_DEVICE_FAMILY = "1,2";
		VERSIONING_SYSTEM = "apple-generic";
	};
	name = Release;
};
We have to do the following in the buildSettings block:
DEVELOPMENT_TEAM to the Apple Team ID you can find in the Apple Developer PortalCODE_SIGN_IDENTITY = "Apple Development"; – This sets the signing identityCODE_SIGN_STYLE = Manual; – This enables manual signingPRODUCT_BUNDLE_IDENTIFIER to represent exactly the identifier you’ve registered on Appstore Connect.PROVISIONING_PROFILE_SPECIFIER = $PROVISIONING_PROFILE_APP; – This allows us to specify the provisioning profile from the CLIWe’re done with the hard part, the rest of the iOS config files are easier to manage.
Open ios/<your project>/Info.plist add the following at the bottom of the main <dict>:
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
This basically tells Apple that you’re not using any kind of special encryption in the application. This will help you skip some forms each time you submit an app to testflight. See more here.
And now finally create a new file and save it to ios/ExportOptions.plist. Don’t forget to change the app id (com.example.app) to your specific app ID:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>compileBitcode</key>
	<false/>
	<key>method</key>
	<string>app-store</string>
	<key>provisioningProfiles</key>
	<dict>
		<key>com.example.app</key>
		<string>@PROVISIONING_PROFILE_APP@</string>
	</dict>
</dict>
</plist>
To make our life a bit easier, we also add a ruby gem dependency which makes the xcodebuild output a bit more digestable. In the Gemfile in the root of our project insert the following line:
gem 'xcpretty', '~> 0.3.0'
Both for Android and iOS you need to create code signing certificates. You can follow the guide from this post to create the certificates: Create code signing certificates for React Native.
I’m recommending to use GitHub Actions for React Native since it also offers MacOS runners and has 2000 free minutes included each month. The MacOS runners are taxed with a 10x multiplier, but since both the android and the iOS build only take about 15-20 minutes, you will have plenty of free builds each month.
To get going create your workflow definition
In the root of your repository create the folder structure .github/workflows and create a new file named build.yml inside it.
Give that pipeline a name. It can be anything you want:
name: "Build application"
Define the triggers of the job
The job has to be triggered by some event. In this case I’ll trigger a build whenever a version tag (a tag matching the v* pattern) is pushed, but you can use any of these triggers.
on:
push:
  tags:
    - "v*"
Define the android build job
This will be the job that builds the android application and uploads it to Google Play.
jobs:
  build-android:
    runs-on: ubuntu-latest
    steps:
As for the steps, this is what this job need to do:
Check out the source code
- name: Checkout
  uses: actions/checkout@v4
Install NodeJS 20.x
- name: Install NodeJS
    uses: actions/setup-node@v4
    with:
      node-version: "20.x"
Install Java
- name: Install Java
  uses: actions/setup-java@v4
  with:
    distribution: temurin
    java-version: '17'
Set up Gradle
- name: Setup Gradle
  uses: gradle/actions/setup-gradle@v3
  with:
    gradle-version: wrapper
Retrieve code signing certificate (the keystore) from the secrets
- name: Retrieve signing certificate
  env:
    KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
  run: |
    echo $KEYSTORE_BASE64 | base64 -di > android/app/release.keystore    
Retrieve the Google Play credentials JSON from the secrets
- name: Retrieve play credentials
  env:
    PLAY_CREDENTIALS_BASE64: ${{ secrets.ANDROID_PLAY_CREDENTIALS_BASE64 }}
  run: |
    echo $PLAY_CREDENTIALS_BASE64 | base64 -di > android/app/play-credentials.json    
Install NPM dependencies
- name: Install dependencies
  run: |
    npm ci    
Build the application and create both APK and AAB files
- name: Build
  working-directory: android
  env:
    ANDROID_KEYSTORE: release.keystore
    ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
    PLAY_CREDENTIALS: play-credentials.json
  run: |
    ./gradlew assembleRelease
    ./gradlew bundleRelease    
Upload the APK to build artifacts. You can use this APK for debug testing
- name: Upload APK
  uses: actions/upload-artifact@v4
  with:
    name: android-release
    path: android/app/build/outputs/apk/release/app-release.apk
Upload the AAB to the Play Store using Google Play Publisher
- name: Upload to Google Play
  working-directory: android
  env:
    ANDROID_KEYSTORE: release.keystore
    ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
    PLAY_CREDENTIALS: play-credentials.json
  run: |
    ./gradlew publishBundle    
Define to iOS build job
This will be the job that builds the iOS application and uploads it to App Store.
build-ios:
  runs-on: macos-latest
  steps:
As for the steps, this is what this job need to do:
Check out the source code
- name: Checkout
  uses: actions/checkout@v4
Install NodeJS 20.x
- name: Install NodeJS
  uses: actions/setup-node@v4
  with:
    node-version: "20.x"
Generate a temporary keychain password
- name: Generate temporary keychain password
  id: keychain-password
  run: |
    PASSWORD=$(openssl rand -base64 32)
    echo "::add-mask::$PASSWORD"
    echo "password=$PASSWORD" >> $GITHUB_OUTPUT    
To use the code signing certificate we’ve created earlier, we need to import it to the build system. This step creates a password for the keychain we import the key and the certificate into, outputs that password to make it available for other steps and masks the password whenever it would appear in terminal output.
Retrieve and import the code signing certificate
- name: Retrieve signing certificate
  id: retrieve-signing-certificate
  env:
    P12_BASE64: ${{ secrets.IOS_P12_BASE64 }}
    P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }}
    KEYCHAIN_PASSWORD: ${{ steps.keychain-password.outputs.password }}
  run: |
    echo $P12_BASE64 | base64 -d > $RUNNER_TEMP/ios-build.p12
    CN=$(openssl pkcs12 -in $RUNNER_TEMP/ios-build.p12 -nodes -passin pass:"$P12_PASSWORD" | openssl x509 -noout -subject -nameopt multiline | sed -n 's/ *commonName *= //p')
    echo "commonName=$CN" >> $GITHUB_OUTPUT
    KEYCHAIN=$RUNNER_TEMP/ios-build.keychain
    security create-keychain -p $KEYCHAIN_PASSWORD $KEYCHAIN
    security set-keychain-settings -lut 21600 $KEYCHAIN
    security unlock-keychain -p $KEYCHAIN_PASSWORD $KEYCHAIN
    security import $RUNNER_TEMP/ios-build.p12 -P $P12_PASSWORD -A -t cert -f pkcs12 -k $KEYCHAIN
    security list-keychains -d user -s $KEYCHAIN
    security find-identity -v -p codesigning    
There’s a lot to digest here, so let’s see what this step does:
Retrieve the provisioning profiles
- name: Retrieve provisioning profiles
  id: retrieve-provisioning-profiles
  env:
    PROVISIONING_PROFILES_ZIP_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILES_ZIP_BASE64 }}
  run: |
    echo $PROVISIONING_PROFILES_ZIP_BASE64 | base64 -d > $RUNNER_TEMP/provisioning_profiles.zip
    mkdir -p $RUNNER_TEMP/provisioning_profiles
    unzip $RUNNER_TEMP/provisioning_profiles.zip -d $RUNNER_TEMP/provisioning_profiles
    mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
    for PROFILE in $(ls $RUNNER_TEMP/provisioning_profiles/*.mobileprovision); do
      PROFILE_NAME="$(basename $PROFILE | awk -F. '{print $1}')"
      UUID=$(/usr/libexec/PlistBuddy -c 'Print :UUID' /dev/stdin <<< $(security cms -D -i $PROFILE))
      echo "$PROFILE_NAME -> $UUID"
      cp $PROFILE ~/Library/MobileDevice/Provisioning\ Profiles/$UUID.mobileprovision
      echo "$PROFILE_NAME=$UUID" >> $GITHUB_OUTPUT
    done    
This step does a lot as well, so let’s break it down:
~/Library/MobileDevice/Provisioning Profiles folder which has to contain the provisioning profiles used during buildInstall NPM dependencies
- name: Install dependencies
  run: |
    npm ci    
Install the required Ruby Gems
- name: Install gems
  run: |
    bundle install    
Install the required CocoaPods
- name: Install CocoaPods
  working-directory: ios
  run: |
    bundle exec pod repo update
    bundle exec pod install    
Build the application
- name: Build
  working-directory: ios
  env:
    PROVISIONING_PROFILE_APP: ${{ steps.retrieve-provisioning-profiles.outputs.app }}
    CODE_SIGN_IDENTITY: ${{ steps.retrieve-signing-certificate.outputs.commonName }}
  run: |
    set -o pipefail
    PACKAGE_VERSION=$(node -e "console.log(require('../package.json').version.split('-')[0])")
    xcodebuild archive -workspace <your_app>.xcworkspace -scheme <your_app> -configuration Release -archivePath build/<your_app>.xcarchive CODE_SIGN_IDENTITY="$CODE_SIGN_IDENTITY" PROVISIONING_PROFILE_APP="$PROVISIONING_PROFILE_APP" MARKETING_VERSION="$PACKAGE_VERSION" CURRENT_PROJECT_VERSION="$GITHUB_RUN_NUMBER" | bundle exec xcpretty
    sed -i.bak "s/@PROVISIONING_PROFILE_APP@/$PROVISIONING_PROFILE_APP/g" ExportOptions.plist
    xcodebuild -exportArchive -archivePath build/<your_app>.xcarchive -exportOptionsPlist ExportOptions.plist -exportPath build | bundle exec xcpretty    
This step contains a lot of complicated commands, so this is what it does:
package.jsonUpload the generated IPA to the pipeline artifacts
- name: Upload IPA
  uses: actions/upload-artifact@v4
  with:
    name: ios-release
    path: ios/build/<your_app>.ipa
Upload application to App Store Connect
- name: Upload to Appstore Connect
  env:
    APPLEID_USER: ${{ secrets.IOS_APPLEID_USER }}
    APPLEID_PASSWORD: ${{ secrets.IOS_APPLEID_PASSWORD }}
  run: |
    xcrun altool --upload-app -f ios/build/your_app.ipa -u $APPLEID_USER -p $APPLEID_PASSWORD --type ios    
Clean up
- name: Cleanup
  if: always()
  run: |
    security delete-keychain $RUNNER_TEMP/ios-build.keychain
    rm -rf "~/Library/MobileDevice/Provisioning Profiles"    
This step will run whatever happens (even if the pipeline is cancelled) and will delete the keychain and the imported provisioning profiles.
Putting it all together
Your final build pipeline should look something like this:
name: "Build application"
on:
  push:
    tags:
      - "v*"
jobs:
  build-android:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Install NodeJS
        uses: actions/setup-node@v4
        with:
          node-version: "20.x"
      - name: Install Java
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '17'
      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v3
        with:
          gradle-version: wrapper
      - name: Retrieve signing certificate
        env:
          KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
        run: |
          echo $KEYSTORE_BASE64 | base64 -di > android/app/release.keystore          
      - name: Retrieve play credentials
        env:
          PLAY_CREDENTIALS_BASE64: ${{ secrets.ANDROID_PLAY_CREDENTIALS_BASE64 }}
        run: |
          echo $PLAY_CREDENTIALS_BASE64 | base64 -di > android/app/play-credentials.json          
      - name: Install dependencies
        run: |
          npm ci          
      - name: Build
        working-directory: android
        env:
          ANDROID_KEYSTORE: release.keystore
          ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
        run: |
          ./gradlew assembleRelease
          ./gradlew bundleRelease          
      - name: Upload APK
        uses: actions/upload-artifact@v4
        with:
          name: android-release
          path: android/app/build/outputs/apk/release/app-release.apk
      - name: Upload to Google Play
        working-directory: android
        env:
          ANDROID_KEYSTORE: release.keystore
          ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
          PLAY_CREDENTIALS: play-credentials.json
        run: |
          ./gradlew publishBundle          
  build-ios:
    runs-on: macos-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Install NodeJS
        uses: actions/setup-node@v4
        with:
          node-version: "20.x"
      - name: Generate temporary keychain password
        id: keychain-password
        run: |
          PASSWORD=$(openssl rand -base64 32)
          echo "::add-mask::$PASSWORD"
          echo "password=$PASSWORD" >> $GITHUB_OUTPUT          
      - name: Retrieve signing certificate
        id: retrieve-signing-certificate
        env:
          P12_BASE64: ${{ secrets.IOS_P12_BASE64 }}
          P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }}
          KEYCHAIN_PASSWORD: ${{ steps.keychain-password.outputs.password }}
        run: |
          echo $P12_BASE64 | base64 -d > $RUNNER_TEMP/ios-build.p12
          CN=$(openssl pkcs12 -in $RUNNER_TEMP/ios-build.p12 -nodes -passin pass:"$P12_PASSWORD" | openssl x509 -noout -subject -nameopt multiline | sed -n 's/ *commonName *= //p')
          echo "commonName=$CN" >> $GITHUB_OUTPUT
          KEYCHAIN=$RUNNER_TEMP/ios-build.keychain
          security create-keychain -p $KEYCHAIN_PASSWORD $KEYCHAIN
          security set-keychain-settings -lut 21600 $KEYCHAIN
          security unlock-keychain -p $KEYCHAIN_PASSWORD $KEYCHAIN
          security import $RUNNER_TEMP/ios-build.p12 -P $P12_PASSWORD -A -t cert -f pkcs12 -k $KEYCHAIN
          security list-keychains -d user -s $KEYCHAIN
          security find-identity -v -p codesigning          
      - name: Retrieve provisioning profiles
        id: retrieve-provisioning-profiles
        env:
          PROVISIONING_PROFILES_ZIP_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILES_ZIP_BASE64 }}
        run: |
          echo $PROVISIONING_PROFILES_ZIP_BASE64 | base64 -d > $RUNNER_TEMP/provisioning_profiles.zip
          mkdir -p $RUNNER_TEMP/provisioning_profiles
          unzip $RUNNER_TEMP/provisioning_profiles.zip -d $RUNNER_TEMP/provisioning_profiles
          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
          for PROFILE in $(ls $RUNNER_TEMP/provisioning_profiles/*.mobileprovision); do
            PROFILE_NAME="$(basename $PROFILE | awk -F. '{print $1}')"
            UUID=$(/usr/libexec/PlistBuddy -c 'Print :UUID' /dev/stdin <<< $(security cms -D -i $PROFILE))
            echo "$PROFILE_NAME -> $UUID"
            cp $PROFILE ~/Library/MobileDevice/Provisioning\ Profiles/$UUID.mobileprovision
            echo "$PROFILE_NAME=$UUID" >> $GITHUB_OUTPUT
          done          
      - name: Install dependencies
        run: |
          npm ci          
      - name: Install gems
        run: |
          bundle install          
      - name: Install pods
        working-directory: ios
        run: |
          bundle exec pod repo update
          bundle exec pod install          
      - name: Build
        working-directory: ios
        env:
          PROVISIONING_PROFILE_APP: ${{ steps.retrieve-provisioning-profiles.outputs.app }}
          CODE_SIGN_IDENTITY: ${{ steps.retrieve-signing-certificate.outputs.commonName }}
        run: |
          set -o pipefail
          PACKAGE_VERSION=$(node -e "console.log(require('../package.json').version.split('-')[0])")
          xcodebuild archive -workspace your_app.xcworkspace -scheme your_app -configuration Release -archivePath build/your_app.xcarchive CODE_SIGN_IDENTITY="$CODE_SIGN_IDENTITY" PROVISIONING_PROFILE_APP="$PROVISIONING_PROFILE_APP" MARKETING_VERSION="$PACKAGE_VERSION" CURRENT_PROJECT_VERSION="$GITHUB_RUN_NUMBER" | bundle exec xcpretty
          sed -i.bak "s/@PROVISIONING_PROFILE_APP@/$PROVISIONING_PROFILE_APP/g" ExportOptions.plist
          xcodebuild -exportArchive -archivePath build/your_app.xcarchive -exportOptionsPlist ExportOptions.plist -exportPath build | bundle exec xcpretty          
      - name: Upload IPA
        uses: actions/upload-artifact@v4
        with:
          name: ios-release
          path: ios/build/your_app.ipa
      - name: Upload to Appstore Connect
        env:
          APPLEID_USER: ${{ secrets.IOS_APPLEID_USER }}
          APPLEID_PASSWORD: ${{ secrets.IOS_APPLEID_PASSWORD }}
        run: |
          xcrun altool --upload-app -f ios/build/your_app.ipa -u $APPLEID_USER -p $APPLEID_PASSWORD --type ios          
      - name: Cleanup
        if: always()
        run: |
          security delete-keychain $RUNNER_TEMP/ios-build.keychain
          rm -rf "~/Library/MobileDevice/Provisioning Profiles"          
As you could see, we’re using the secrets feature of GitHub actions to store some information required for the pipeline to run.
You can set these variables if you open your repo, go to Settings, then Secrets and variables, then Actions.
You need to add the following secrets:
ANDROID_KEYSTORE_BASE64
You need to set this one to the base64 encoded keystore file you’ve created for Android.
You can get the value using the following command:
openssl base64 -in android-upload-key.keystore -A
ANDROID_KEYSTORE_PASSWORD
Set it to the password generated when creating the keystore for Android.
ANDROID_PLAY_CREDENTIALS_BASE64
Set it to the credentials JSON file used by Google Play Publisher, encoded as base64.
To create this file you have to use both the Play Console and the GCloud console. Follow the instructions from here.
After you got the JSON file, encode it as base64 like this:
openssl base64 -in credentials.json -A
IOS_P12_BASE64
Set this to the base64 encoded PKCS#12 archive containing the code signing certificate for iOS.
To do the encoding, use the following command:
openssl base64 -in ios-signing-cert.p12 -A
IOS_P12_PASSWORD
Set this to the password used to secure the PKCS#12 archive.
IOS_PROVISIONING_PROFILES_ZIP_BASE64
Set this to the base64 encoded zip file containing the provisioning profiles.
To do the encoding, use the following command:
openssl base64 -in provisioning-profiles.zip -A
IOS_APPLEID_USER
Set this to the username (email) of the Apple ID account which has permissions to upload application binaries to App Store Connect.
IOS_APPLEID_PASSWORD
Set this to an app-specific password for that user. You can create an app-specific password following this guide.
That’s it! You’ve just created a complete CI/CD pipeline for your React Native application. You can now push a version tag to your repository and the pipeline will automatically build and upload the application to Google Play and App Store Connect.
This pipeline can be further extended to include tests, static code analysis or changelog generation.
Feel free to reach out to us at [email protected] or comment below.
You need someone to implement this pipeline and more (like tests, static analysis, changelog generation, etc.) for you? Check out our DevOps services.
Comments