How to set up React Native CI/CD using GitHub Actions: Part 2

How to set up React Native CI/CD using GitHub Actions: Part 2
×

About the author

Abhishek Tiwari
Software Engineer
Abhishek Tiwari, a Software Engineer at Nitor Infotech, has more than two years of experience in React Native, mobile app development, and i... Read More

Software Engineering   |      23 Dec 2022   |     25 min  |

Welcome back to the CI/CD realm! I’m not exaggerating when I say that GitHub Actions makes setting up React Native CI/CD a cakewalk. I hope you had a chance to read Part 1 of this blog series: Github actions and how to set up manual triggers! If not, do check it out; it will help you to understand the initial setup.

Our 3-phase DevOps implementation improved the CI/CD engine and streamlined deployment for a software company. Know how!

In this blog, I’ll mainly focus on steps related to iOS like Ruby setup, pod install, build, and upload. So, let’s get started!

How to Set up Ruby using ruby/setup-ruby@v1 (iOS only)?

- name: ruby setup
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.6
bundler-cache: true
env:
ImageOS: macos1015

If the virtual environment is extremely similar to the ones used by GitHub runners, this action will work with self-hosted runners. Notably:

  • Use the exact same version.
  • Set the environment variable ImageOS on the runner (for example, ubuntu18/macos1015/win19) to the appropriate value on GitHub-hosted runners. To determine the operating system and version, this is required.
  • The libssl version must be the same.
  • Ensure that libgmp and libyaml-0 are installed on the operating system.
  • The runner user must have written access to the default tool cache directory (/opt/hostedtoolcache on Linux, /Users/runner/hostedtoolcache on macOS, and C:/hostedtoolcache/windows on Windows). Since the install path is permanently included in the Ruby builds and cannot be changed, this is required.
  • The runner user must have written access to /home/runner.

Now, allow me to explain how you can restore Pods cache using actions/cache@v3.

How to Restore Pods cache using actions/cache@v3 ?

- name: Restore Pods cache

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

Now it’s time to install the Pods!

How to Install Pods?

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

Here’s how you can bump the iOS version.

How to Bump iOS version?

- name: bump version

uses: abhishek219tiwari/[email protected]

with:

marketingVersion: ${{ github.event.inputs.version }}

project-path: ios/Nitor/Config/Info-plist/Nitor-Dev-info.plist

It’s not necessary to use this action, this is under development if agvTool is working fine for you. You can go with other options as well, such as the iOS bump version.

Let’s look at how you can build an iOS app.

How to Build an iOS app?

- name: Build IOS App NitorPro / staging
if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'staging' }}
uses: yukiarrr/[email protected]
with:
project-path: ios/Nitor.xcodeproj
p12-base64: ${{ secrets.IOS_P12_BASE64 }}
mobileprovision-base64: ${{ secrets.IOS_MOBILE_PROVISION_BASE64 }}
code-signing-identity: "iPhone Distribution"
team-id: ${{ secrets.IOS_TEAM_ID }}
certificate-password: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
workspace-path: ios/Nitor.xcworkspace
scheme: NitorPro - Staging

Now that you have the app in place, read on to know how you can configure your iOS project on Xcode.

How to Configure iOS project on Xcode?

  1. Double-clicking the file ios/XXXX.xcworkspace (not the.xcodeproj file) will launch your project in Xcode.
  2. Choose the project target, singing and capabilities, then release.
  3. Uncheck Automatically manage singing.
  4. Import the downloaded profile into Provisioning Profile.
  5. If the certificate connected to the profile is already installed on your device, you should see it there as well. The team name should also instantly display.

We utilize yukiarrr/[email protected] to create the app and export the IPA for TestFlight submission.

The following variables must be added as GitHub Secrets for the build action:

  • IOS MOBILE PROVISION BASE64: Provisioning profile file in Base64 encoding.

Use the following script to build base64 encoded format after downloading it from your Apple developer portal.

openssl base64 < YOUR_Profile.mobileprovision | tr -d ‘\n’ | tee my-profile.base64.txt

  • IOS P12 BASE64:.p12 file with base64 encoding (key + cert)

Once the certificate has been installed on your Mac, launch the Keychain Access app, choose ‘My Certificates’ from the menu, and then find the downloaded certificate.

To view the matching private key, expand the certificate. Next, choose ‘Export 2 items…’ from the context menu when you right-click on the certificate and private key.

Choose a location on your computer to save the file as a.p12 and give it a secure password (IOS CERTIFICATE PASSWORD).

For the .p12 file, create a base64 using

openssl base64 < cert.p12 | tr -d ‘\n’ | tee cert.base64.txt

The final step is to use upload-TestFlight-build to upload the created IPA to TestFlight.

  • The issuer ID is something like 598542-36fe-1a63-e073-0824d0166672a; go to Users and Access > API Keys.
  • The AppStore Connect API Key ID is APPSTORE API KEY ID.
  • APPSTORE API PRIVATE KEY: The AppStore Connect API’s private key in PKCS8 format. The information in the created file AUTH Key xxxxxx.p8

Note:  As we are using a personal Mac system as self-hosted, we need to download Apple Worldwide Developer Relations Certificate Authority.

Now it’s time to see how you can upload your app to TestFlight.

How to Upload the app to TestFlight?

#Upload app to TestFlight For NitorPro / staging
- name: Upload app to TestFlight NitorPro / staging
if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'staging' }}
uses: apple-actions/upload-testflight-build@v1
with:
app-path: "output.ipa"
issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }}
api-private-key: ${{ secrets.APPSTORE_API_PRIVATE_KEY }}

If you get stuck in any of the steps, you can get some help from here, I have put my .yml.

name: iOS Build Deployment
on:
workflow_dispatch:
inputs:
target:
description: "Target"
required: true
default: "Nitor"
type: choice
options:
- Nitor
- NitorPro
flavor:
description: "Environment"
required: true
default: "staging"
type: choice
options:
- staging
- development
- production
version:
description: new app version x.x.x.
required: true
jobs:
ios-build:
name: IOS Production Build
runs-on: self-hosted
defaults:
run:
working-directory: ios
steps:
- name: Check out Git repository

uses: actions/checkout@v2

with:

persist-credentials: false # make it false if you are going to clone private repositories in yarn



- name: Set up our node configuration

uses: actions/setup-node@v1

with:

node-version: 16.x



- name: Decode .env.Nitor.staging # decode .env.Nitor.staging and generate .env.Nitor.staging file

if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'staging'}}

id: decode_Nitor_staging

uses: timheuer/base64-to-file@v1

with:

fileName: ".env"

encodedString: ${{ secrets.NITOR_STAGING}}



- name: Decode .env.Nitor.development # decode .env.Nitor.development and generate .env.Nitor.development file

if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'development'}}

id: decode_Nitor_development

uses: timheuer/base64-to-file@v1

with:

fileName: ".env"

encodedString: ${{ secrets.NITOR_DEVELOPMENT}}



- name: Decode .env.Nitor.production # decode .env.Nitor.production and generate .env.Nitor.production file

if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'production'}}

id: decode_Nitor_production

uses: timheuer/base64-to-file@v1

with:

fileName: ".env"

encodedString: ${{ secrets.NITOR_PRODUCTION}}



- name: Decode .env.NitorPro.staging # decode .env.NitorPro.stagingand generate .env.NitorPro.staging file

if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'staging'}}

id: decode_NitorPro_staging

uses: timheuer/base64-to-file@v1

with:

fileName: ".env"

encodedString: ${{ secrets.NITORPRO_STAGING}}



- name: Decode .env.NitorPro.development # decode .env.NitorPro.development and generate .env.NitorPro.development file

if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'development'}}

id: decode_NitorPro_development

uses: timheuer/base64-to-file@v1

with:

fileName: ".env"

encodedString: ${{ secrets.NITORPRO_DEVELOPMENT}}




- name: Decode .env.NitorPro.production # decode .env.NitorPro.production and generate .env.NitorPro.production file

if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'production'}}

id: decode_NitorPro_production

uses: timheuer/base64-to-file@v1

with:

fileName: ".env"

encodedString: ${{ secrets.NITORPRO_PRODUCTION}}

- name: "move Nitor_staging"

if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'staging'}}

run: |

mv ${{steps.decode_Nitor_staging.outputs.filePath}} ./

pwd

cat .env

- name: "move Nitor_development"

if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'development'}}

run: |

mv ${{steps.decode_Nitor_development.outputs.filePath}} ./

pwd

cat .env



- name: "move Nitor_production"

if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'production'}}

run: |

mv ${{steps.decode_Nitor_production.outputs.filePath}} ./

pwd

cat .env



- name: "move NitorPro_staging"

if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'staging'}}

run: |

mv ${{steps.decode_NitorPro_staging.outputs.filePath}} ./

pwd

cat .env



- name: "move NitorPro_development"

if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'development'}}

run: |

mv ${{steps.decode_NitorPro_development.outputs.filePath}} ./

pwd

cat .env



- name: "move NitorPro_production"

if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'production'}}

run: |

mv ${{steps.decode_NitorPro_production.outputs.filePath}} ./

pwd

cat .env



- name: git config with PAT_GITHUB

run: git config --global url.https://${{ secrets.PAT_GITHUB }}@github.com/.insteadOf https://github.com



- run: |

echo "github.event.inputs.version: ${{ github.event.inputs.version }}"




- name : ruby setup

uses: ruby/setup-ruby@v1

with:

ruby-version: 2.6

bundler-cache: true

env:

ImageOS: macos1015

LANG: en_US.UTF-8



- name: bump version

uses: abhishek219tiwari/[email protected]

with:

marketingVersion: ${{ github.event.inputs.version }}

project-path: ios/Nitor/Config/Info-plist/Nitor-Dev-info.plist



- 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@v3

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: yarn install

if: |

steps.cache-yarn-cache.outputs.cache-hit != 'true' ||

steps.cache-node-modules.outputs.cache-hit != 'true'

run: |

yarn install --network-concurrency 1

- name: Restore Pods cache

uses: actions/cache@v3

with:

path: |

ios/Pods

~/Library/Caches/CocoaPods

~/.cocoapods

key: ${{ runner.os }}-pods-${{ hashFiles('ios/Podfile.lock') }}

restore-keys: |

${{ runner.os }}-pods-

- name: Install Pods

run: pod install --repo-update && cd ..

- name: check .env

run: cat .env

# Build IOS App Nitor / staging

- name: Build IOS App Nitor / staging

if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'staging' }}

uses: yukiarrr/[email protected]

with:

project-path: ios/Nitor.xcodeproj

p12-base64: ${{ secrets.IOS_P12_BASE64 }}

mobileprovision-base64: ${{ secrets.IOS_MOBILE_PROVISION_BASE64 }}

code-signing-identity: "iPhone Distribution"

team-id: ${{ secrets.IOS_TEAM_ID }}

certificate-password: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}

workspace-path: ios/Nitor.xcworkspace

scheme: Nitor - Staging

# Build IOS App Nitor / development

- name: Build IOS App Nitor / development

if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'development' }}

uses: yukiarrr/[email protected]

with:

project-path: ios/Nitor.xcodeproj

p12-base64: ${{ secrets.IOS_P12_BASE64 }}

mobileprovision-base64: ${{ secrets.IOS_MOBILE_PROVISION_BASE64 }}

code-signing-identity: "iPhone Distribution"

team-id: ${{ secrets.IOS_TEAM_ID }}

certificate-password: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}

workspace-path: ios/Nitor.xcworkspace

scheme: Nitor - Dev

# Build IOS App Nitor / production

- name: Build IOS App Nitor / production

if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'production' }}

uses: yukiarrr/[email protected]

with:

project-path: ios/Nitor.xcodeproj

p12-base64: ${{ secrets.IOS_P12_BASE64 }}

mobileprovision-base64: ${{ secrets.IOS_MOBILE_PROVISION_BASE64 }}

code-signing-identity: "iPhone Distribution"

team-id: ${{ secrets.IOS_TEAM_ID }}

certificate-password: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}

workspace-path: ios/Nitor.xcworkspace

scheme: Nitor

# Build IOS App NitorPro / staging

- name: Build IOS App NitorPro / staging

if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'staging' }}

uses: yukiarrr/[email protected]

with:

project-path: ios/Nitor.xcodeproj

p12-base64: ${{ secrets.IOS_P12_BASE64 }}

mobileprovision-base64: ${{ secrets.IOS_MOBILE_PROVISION_BASE64 }}

code-signing-identity: "iPhone Distribution"

team-id: ${{ secrets.IOS_TEAM_ID }}

certificate-password: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}

workspace-path: ios/Nitor.xcworkspace

scheme: NitorPro - Staging

# Build IOS App NitorPro / development

- name: Build IOS App NitorPro / development

if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'development' }}

uses: yukiarrr/[email protected]

with:

project-path: ios/Nitor.xcodeproj

p12-base64: ${{ secrets.IOS_P12_BASE64 }}

mobileprovision-base64: ${{ secrets.IOS_MOBILE_PROVISION_BASE64 }}

code-signing-identity: "iPhone Distribution"

team-id: ${{ secrets.IOS_TEAM_ID }}

certificate-password: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}

workspace-path: ios/Nitor.xcworkspace

scheme: NitorPro - Dev

# Build IOS App NitorPro / production

- name: Build IOS App NitorPro / production

if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'production' }}

uses: yukiarrr/[email protected]

with:

project-path: ios/Nitor.xcodeproj

p12-base64: ${{ secrets.IOS_P12_BASE64 }}

mobileprovision-base64: ${{ secrets.IOS_MOBILE_PROVISION_BASE64 }}

code-signing-identity: "iPhone Distribution"

team-id: ${{ secrets.IOS_TEAM_ID }}

certificate-password: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}

workspace-path: ios/Nitor.xcworkspace

scheme: NitorPro

#Upload app to TestFlight For Nitor / staging

- name: Upload app to TestFlight Nitor / staging

if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'staging' }}

uses: apple-actions/upload-testflight-build@v1

with:

app-path: "output.ipa"

issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}

api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }}

api-private-key: ${{ secrets.APPSTORE_API_PRIVATE_KEY }}

#Upload app to TestFlight For Nitor / development

- name: Upload app to TestFlight Nitor / development

if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'development' }}

uses: apple-actions/upload-testflight-build@v1

with:

app-path: "output.ipa"

issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}

api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }}

api-private-key: ${{ secrets.APPSTORE_API_PRIVATE_KEY }}

#Upload app to TestFlight For Nitor / production

- name: Upload app to TestFlight Nitor / production

if: ${{ github.event.inputs.target == 'Nitor' && github.event.inputs.flavor == 'production' }}

uses: apple-actions/upload-testflight-build@v1

with:

app-path: "output.ipa"

issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}

api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }}

api-private-key: ${{ secrets.APPSTORE_API_PRIVATE_KEY }}

#Upload app to TestFlight For NitorPro / staging

- name: Upload app to TestFlight NitorPro / staging

if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'staging' }}

uses: apple-actions/upload-testflight-build@v1

with:

app-path: "output.ipa"

issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}

api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }}

api-private-key: ${{ secrets.APPSTORE_API_PRIVATE_KEY }}

#Upload app to TestFlight For NitorPro / development

- name: Upload app to TestFlight NitorPro / development

if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'development' }}

uses: apple-actions/upload-testflight-build@v1

with:

app-path: "output.ipa"

issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}

api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }}

api-private-key: ${{ secrets.APPSTORE_API_PRIVATE_KEY }}

#Upload app to TestFlight For NitorPro / production

- name: Upload app to TestFlight NitorPro / production

if: ${{ github.event.inputs.target == 'NitorPro' && github.event.inputs.flavor == 'production' }}

uses: apple-actions/upload-testflight-build@v1

with:

app-path: "output.ipa"

issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}

api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }}

api-private-key: ${{ secrets.APPSTORE_API_PRIVATE_KEY }}


That’s all for today’s blog! You can save this blog as a pocket guide to the iOS setup of CI/CD with GitHub Actions! Stay tuned for Part 3 in this series which will walk you through setting up CI/CD in the world of Android! Meanwhile, write to us with your thoughts about Part 2 and visit us at Nitor Infotech to learn about what we do.

subscribe image

Subscribe to our
fortnightly newsletter!

we'll keep you in the loop with everything that's trending in the tech world.

We use cookies to ensure that we give you the best experience on our website. If you continue to use this site we will assume that you are happy with it.