How to build react-native iOS CI/CD pipeline using Github Actions + Ms AppCenter

Tharindu Ramesh Ketipearachchi
13 min readApr 14, 2022

--

In our previous article we have explained about how to implement a react-native CI/CD pipeline for android. Here, we are going to discuss about how to do it for iOS. We are going to use the same tool set, GitHub Actions with Ms AppCenter.

First, we take the source code from Github and build it using the Github actions then sign it with apple certificates. The built ipa will be uploaded to the Ms AppCenter. Then we’ll link the AppCenter with Apple Appstore. Then your testing team will be able to install the test builds via the AppCenter. After the testing process is completed, we will be pushing the same ipa to Appstore from the AppCenter.

01. Create a channel on Ms App Center

In the previous article we have explained that how to create channels on Ms AppCenter. Please refer it here and create your AppCenter channels first. Select app’s OS type as iOS. Please make sure the generate API token and add it to the Github secrets as well (APP_CENTER_TOKEN_MYAPP_IOS_DEV , TEST etc.)

For iOS, I have created an additional channel. Not like in android, now we have 4 channels as ENV_DEV, ENV_TEST, ENV_STAGING and ENV_PROD.

Not like in android, in iOS we have to sign our applications using apple certificates before distributing it to the users. App distribution platforms like AppCenter and Firebase required Ad-Hoc or Development certificate to distribute your app. Because they are doing sort of internal app distribution among registered devices of your Apple Developer Account. But when it goes to public via the Apple Appstore it is required a distribution certificate. Since we are using our internal testers to test our app, we can use Ad-Hoc certificate to sign our ENV_DEV and ENV_TEST builds. But we have to sign our ENV_PROD build with a distribution certificate. Then that build will not be able to install via the AppCenter and testers will not be able to carry the prod verification test before pushing it to the AppStore.

As a solution for that, in our production pipeline we are building a PROD ipa and will sign it with two different certificates. The ipa signed with Ad-Hoc certificate will be uploaded to ENV_STAGING channel and testers will be able to install it and do the prod verification there. The same ipa which is signed using distribution certificate will be uploaded to ENV_PROD channel. Since the both builds are identical once you completed the prod verification testing on the ENV_STAGING channel, you can go the ENV_PROD channel and can push the identical build to Appstore which is signed from distribution certificate.

As an alternative to that you can use single ENV_PROD channel and using distribution certificates will be able to push the build to the TestFlight. From there, testers will be able to do the prod verification and push the same TestFlight verified build to the live on Appstore. But TestFlight required an approval process and might take some time. Since I want to distribute my prod builds immediately for the prod verification, I prefer the first way. Please read here to learn more about the apple code signing process.

02. Create a workflow in Github Actions

Now we are going to create our pipeline using Github Actions. Go to your Github repo, select Actions tab and click the New workflow on top left.

There are so many predefine templates which you can use. But click on set up a workflow yourself button. Let’s create our pipeline from the scratch.

Then give the name to yml file (deploy-ios-prod.yml) and commit the changes.

03. Add code signing certificates to Github Secrets

We have to add our Apple app distribution certificate, distribution provisioning profile and Ad-Hoc provisioning profile to Github secrets. But to do that first, we need to convert those certificates to base64 format.

First download distribution certificate from apple developer account and install it by double clicking on it. Now go to your keychain app > certificates and export it as p12 file. You need to enter a password for this and we have to save that password also in Github secrets . Save it as CERTIFICATE_PASSWORD. Read this for learn more about p12 file creation.

Now save the generated p12 file in your local folder. Then download the provisioning profiles from app developer account and save those in the same folder as well. Then open the mac terminal and go to the particular folder.

Now execute the following command on your terminal.

base64 DistributionCertificate.p12 | pbcopy

This command will convert your certificate to base64 format and automatically copy that value to your clipboard. Now go to the Github secrets, create new entry called APPLE_APP_DISTRIBUTION_CERTIFICATE and paste the copied value there.

Do this for your provisioning profile files as well. Add two new variables to your Github secrets as APPLE_DISTRIBUTION_PROFILE, APPLE_AD-HOC_PROFILE. You don’t need to convert provisioning profiles to p12 form. Just converting it to the base64 is enough.

Script will create a mac environment on the VM container to use your certificates and profiles. It will create a separate keychain there as well. For that we need to give a keychain password. Create another secret called KEY_PWD and add a strong password there.

04. Build the workflow

There’s an already generated template code is there on the yml file. But let’s delete it and build our own script from the beginning. Let’s start to edit our deploy-ios-prod.yml file.

Here I’m going to explain the workflow that I have setup for my PROD pipeline. First you should give a name to your workflow. Then we have to set a trigger to run the pipeline. For PROD, I have triggered from a tag. Developer should create a tag with the following format vX.Y.Z you can customise this anyway. Once they create a tag and push it to the origin, then the our PROD pipeline will start to run. As an example create a tag v2.2.3 and push it to origin, then your pipeline will automatically be triggering.

name: Deploy-iOS-PRODon:
push:
tags:
- ‘v[0–9]+.[0–9]+.[0–9]+’

Then we need to add our AppCenter tokens, app names, signing certificates, profiles and all other constants as environmental variables. Then we can use those anywhere in our script. Make sure to enter the app name as organization_name/app_name format and give the same testing group name which you have created in AppCenter channel as the TESTING_GROUP. This time our variables list is going to be a long one.

env:
WORKSPACE: ${{ ‘ios/MyApp.xcworkspace’ }}
SCHEME: ${{ ‘MyAppProd’ }}
CONFIGURATION: ${{ ‘Release’ }}
ARCHIVE_PATH: ${{ ‘build/MyAppProd.xcarchive’ }}
EXPORT_PATH_STAGING: ${{ ‘staging/’ }}
EXPORT_PATH_PROD: ${{ ‘prod/’ }}
PLIST_PATH_STAGING: ${{‘ios/MyApp/StagingExport.plist’ }}
PLIST_PATH_PROD: ${{ ‘ios/MyApp/ProdExport.plist’ }}
APP_CENTER_TOKEN_PROD: ${{ secrets.APP_CENTER_TOKEN_MYAPP_IOS_PROD }}
APP_CENTER_TOKEN_STAGING: ${{ secrets.APP_CENTER_TOKEN_MYAPP_IOS_STAGING }}
ARTIFACT_NAME: ${{ ‘MyApp-ipa’ }}
ARTIFACT_PATH_STAGING: ${{ ‘staging/’ }}
ARTIFACT_PATH_PROD: ${{ ‘prod/’ }}
APP_NAME_STAGING: ${{ ‘MyApp-iOS/ENV_STAGING’ }}
APP_NAME_PROD: ${{ ‘MyApp-iOS/ENV_PROD’ }}
TESTING_GROUP_STAGING: ${{ ‘ENV_STAGING’ }}
TESTING_GROUP_PROD: ${{ ‘ENV_PROD’ }}
UPLOAD_FILE_STAGING: ${{ ‘staging/MyApp.ipa’ }}
UPLOAD_FILE_PROD: ${{ ‘prod/MyApp.ipa’ }}
DISTRIBUTION_CERTIFICATE: ${{ secrets.APPLE_APP_DISTRIBUTION_CERTIFICATE }}
CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}
DISTRIBUTION_PROFILE_PROD:${{secrets.APPLE_DISTRIBUTION_PROFILE }}
DISTRIBUTION_PROFILE_STAGING: ${{ secrets.APPLE_AD-HOC_PROFILE }}
KEY_PWD: ${{ secrets.KEY_PWD }}

Here I have divided our environment variables in to three different sections. I want to explain each section separately.

When we implement our pipeline, we have to build our ipas which supports to different environments and different API base urls(Dev, Test, Staging, Prod etc.). So we can use Xcode for this purpose. We can create different targets in Xcode and identical schemes to build ipa over different configurations. We can add all the configurations files, resources to each targets separately as we used to do on Xcode.

Give the path to your workplace from the root directory as the WORKSPACE. Then give the sheme name as same as your export project for SCHEME (MyAppProd for prod, MyAppTest for test). CONFIGURATION should be the Build Configuration of your scheme.

ARCHIVE_PATH and the EXPORT_PATH are the paths where your archive file and ipas going to save. If you don’t have such a directories in your Github repo, please make sure to create those in your virtual container first. Keep reading the directory creation has been explained here as well.

Apple has introduced command line build tool for iOS apps. But not like in Xcode, the options are limited. In Xcode GUI we can add different configurations from capabilities section such as push notifications, iCloud, wallet. But Xcode command line build doesn’t provide additional options for each of these configurations. Instead you can create plist file, add all those additional configurations there and can give that plist file path as the -exportOptionsPlist parameter of the xcodebuild command. Read here to learn more about Xcode command line build. I have iCloud enabled one of my apps and drop here the sample plist file which included iCloud environment configurations.

<?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"><! — ProdExpor.plistMyAppProdCreated by Tharindu Ketipearachchi on 2022–02–15.Copyright © 2022 ___ORGANIZATIONNAME___. All rights reserved.<plist version=”1.0"><dict><key>iCloudContainerEnvironment</key><string>Production</string><key>method</key><string>ad-hoc</string> <! — app-store -production | ad-hoc — ad hoc distribution →<key>provisioningProfiles</key><dict><key>com.tharindu.MyApp</key><string>My App Ad-Hoc</string></dict><key>signingCertificate</key><string>Apple Distribution: Tharindu Pvt Ltd </string><key>signingStyle</key><string>manual</string><key>teamID</key><string>XXXXXXXX</string></dict></plist>

Please be make sure to give the correct names for distribution profiles and certificates. The same certificate names should be there on singing & capabilities section of your Xcode target as well. Otherwise your pipeline build will be failed. It doesn’t matter you are missing private key for particular profiles and certificates, but correct profile name should be there on Xcode. Because our pipeline script is getting all the build configurations from your Xcode target.

And you should select a correct certificate on Build Settings section as well. Don’t worry about those certificates aren’t configured well and not working on Xcode. We just need the correct name. We have already uploaded all the certificates to Github secrets.

In second part we have created env variables for AppCenter tokens. I’ll explain later what is artifact and about artifact paths. APP_NAME is our AppCenter channel name organisation_name/app_name format. TESTING_GROUP is also the name of testing group that we have created inside our AppCenter channels. UPLOAD_FILE is path to save your ipa file. You can give the name to your ipa file here as well.

The third section we have created env variables for our distribution certificates and profiles which we have created in our 3rd section.

Now we need to start our job, setup our container and setup the environment. Here we are using a mac container.

jobs:
build:
name: Build
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up Node.js 12.16.1
uses: actions/setup-node@v1
with:
node-version: 12.16.1

Then we are going to build our ipa, run npm install and all the necessary commands that you need setup your environments and react-native project. Then we are running pod install to install all the iOS dependencies.

- name: Install dependencies
run: npm install
- name: Install pod dependencies
run: |
cd ios && pod install

Now we are going to create our virtual keychain and install our signing certificates which are already stored on Github secrets.

- name: Signing & Provisioning
run: |
# create variables
CERT_PATH=$RUNNER_TEMP/dist_certificate.p12
PP_PATH_PROD=$RUNNER_TEMP/dist_pp.mobileprovision
PP_PATH_STAGING=$RUNNER_TEMP/dist_pp_adc.mobileprovision
KCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db

# import certificate and provisioning profile from secrets
echo -n “$DISTRIBUTION_CERTIFICATE” | base64 — decode — output $CERT_PATH
echo -n “$DISTRIBUTION_PROFILE_STAGING” | base64 — decode — output $PP_PATH_STAGING
echo -n “$DISTRIBUTION_PROFILE_PROD” | base64 — decode — output $PP_PATH_PROD
# create temporary keychain
security create-keychain -p “$KEY_PWD” $KCHAIN_PATH
security set-keychain-settings -lut 21600 $KCHAIN_PATH
security unlock-keychain -p “$KEY_PWD” $KCHAIN_PATH
# import certificate to keychain
security import $CERT_PATH -P “$CERTIFICATE_PASSWORD” -A -t cert -f pkcs12 -k $KCHAIN_PATH
security list-keychain -d user -s $KCHAIN_PATH
# apply provisioning profile
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH_PROD ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH_STAGING ~/Library/MobileDevice/Provisioning\ Profiles

Bold text variables are directly refer from the environment variable set which we declared at the beginning of our script. Please do care about other variable names as well.

Next we are selecting proper Xcode version to build and create two directories to store our ipa files(export_path) .

- name: Select Xcode
run: sudo xcode-select -switch /Applications/Xcode_13.2.1.app
- name: Xcode Version
run: /usr/bin/xcodebuild -version
- name: Create build folder
run: |
mkdir -p build && mkdir -p staging && mkdir -p prod

Then we are going to build our ipa.

- name: Build Archive
run: |
xcodebuild -workspace $WORKPLACE -scheme $SCHEME -configuration $CONFIGURATION \
archive -archivePath $ARCHIVE_PATH -allowProvisioningUpdates
PROVISIONING_STYLE=”Manual” \
PROVISIONING_PROFILE={$DISTRIBUTION_PROFILE_STAGING} \
CODE_SIGN_STYLE=”Manual” \
CODE_SIGN_IDENTITY={$DISTRIBUTION_CERTIFICATE} \

We are going to build ipa with ad-hoc profile and export it with distribution profile for PROD channel.

- name: Export STAGING
run: |
xcodebuild -exportArchive -archivePath $ARCHIVE_PATH -exportPath $EXPORT_PATH_STAGING . -exportOptionsPlist $PLIST_PATH_STAGING \
- name: Export PROD
run: |
xcodebuild -exportArchive -archivePath $ARCHIVE_PATH -exportPath $EXPORT_PATH_PROD . -exportOptionsPlist $PLIST_PATH_PROD \

Here we have exported the previously built ipa with two different certificates. For -exportOptionsPlist parameter, we have given two different export plist files. The staging one include an ad-hoc certificate and prod one included a distribution certificate. Refer the above posted plist file and customise it with your certificates and profile names and use for this step. Now the signed ipa files will be saved to directories in our export_path.

We are using wzieba plugin for upload our ipas to AppCenter. But currently this plugin is only supporting for ubuntu containers. So we have to create another job in ubuntu container for upload process. But before that we have to upload this two ipa files as an artifiact, then we’ll be able to download it on our other job and upload those to AppCenter.

- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: ${{ env.ARTIFACT_NAME }}
path: |
${{ env.ARTIFACT_PATH_STAGING }}
${{ env.ARTIFACT_PATH_PROD }}

Now we need to clean our all certificates and keychain on this virtual environment.

- name: Clean up keychain and provisioning profile
if: ${{ always() }}
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
rm ~/Library/MobileDevice/Provisioning\ Profiles/dist_pp.mobileprovision
rm ~/Library/MobileDevice/Provisioning\ Profiles/dist_pp_adc.mobileprovision

Now our build job is completed. We have to start a new job for our deploy process with ubuntu container.

deploy:
needs: [build]
if: success()
name: Deploy
runs-on: ubuntu-latest

First we should check our build job is success, otherwise no point of starting the deploy process. Then we should download the artifact files that we have uploaded in our build job. Then setup the environment.

steps:- uses: actions/checkout@master- uses: actions/actions/download-artifact@v3
with:
name: ${{ env.ARTIFACT_NAME }}
- name: set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8

Well, let’s update downloaded ipa files to AppCenter. PROD build will be uploaded to ENV_PROD channel and STAGING build will be uploaded to ENV_STAGING channel. Configurations are same as android.

- name: Upload to App Center STAGING
uses: wzieba/AppCenter-Github-Action@v1
with:
appName: ${{ env.APP_NAME_STAGING }}
token: ${{ env.APP_CENTER_TOKEN_STAGING }}
group: ${{ env.TESTING_GROUP_STAGING }}
file: ${{ env.UPLOAD_FILE_STAGING }}
notifyTesters: true
debug: false
- name: Upload to App Center PROD
uses: wzieba/AppCenter-Github-Action@v1
with:
appName: ${{ env.APP_NAME_PROD }}
token: ${{ env.APP_CENTER_TOKEN_PROD }}
group: ${{ env.TESTING_GROUP_PROD }}
file: ${{ env.UPLOAD_FILE_PROD }}
notifyTesters: false
debug: false

These artifacts will be stored in your Github actions server space and will causes to memory issues of your server. So we will delete all the artifacts once we done with uploading. We are using geekyeggo plugin for that.

- name: Delete Artifact
uses: geekyeggo/delete-artifact@v1
with:
name: ${{ env.ARTIFACT_NAME }}

Now we are done with our Github actions script. Here is our full script.

05. Link Apple Appstore with AppCenter

Once we have completed the pipelines for all the environments, then we can link the AppCenter ENV_PROD channel with Appstore. After you have completed the prod verification testing on the ENV_STAGING, you can publish the identical ipa from ENV_PROD to Appstore. This process is really easy and bug free.

First you need to enable tow factor authentication of your apple account. Now you can go to Account Settings > Developer Accounts on AppCenter and add your apple account. But don’t give your apple id password here. Create an app-specific-password from Apple ID portal and add that password here. If you don’t know how to generate it, please read this.

Now go to the AppCenter and select the ENV_PROD channel. Go to Distribute > Stores, you can see a Connect to Store button there.

Select your apple account. If you want you can add a new account from here as well.

Now all available apps of your Appstore will be listed here. Select the app that you want to connect and click assign.

Now you can see the Appstore distribution options on your Stores window.

06. Publish the Application on Appstore

Go to Distribute > Releases and select the release that you want to publish on Appstore.

Select Distribute on top right and click Store.

Select Production and click next.

Give the release note that you need to display on Appstore and click next.

Click publish and you are done.

Congratulations! Now you have completed the end to end pipeline from Github to Apple Appstore as well. Cheers!!

07. Where to go from here?

If you are using Js based technologies like react-native or cordova to implement a mobile app, CodePush is really cool platform to update your application without approval from the Appstore or Google play. Here we have explained what is it and how to integrate it for your mobile app.

Do you feel that Github Actions taking too much time to build your pipeline? Want to build a CI/CD pipeline quickly within 20–30 minutes? Just refer my next article on Mobile CI/CD.

--

--

Tharindu Ramesh Ketipearachchi

Technical Lead (Swift, Objective C, Flutter, react-native) | iOS Developer | Mobile Development Lecturer |MSc in CS, BSc in CS (Col)