Sitemap
Press enter or click to view image in full size
CI/CD pipeline for React Native apps: use Fastlane and GitHub Actions- Part 2: Code and implementation

CI/CD pipeline for React Native apps: use Fastlane and GitHub Actions. -Part 2: Code and implementation

Introduction

--

In the first part, I talked mainly about Fastlane and explained it in more detail way so you can Engineer the solution rather than just copy-paste.

in this part, I will talk about Github actions, and move into a real example of how you can implement your Continuous Integration and Continuous Deployment solution for your React Native app, using Fastlane and Github actions.

For Github actions:

GitHub Actions is a CI/CD (continuous integration and continuous delivery) platform that integrates with GitHub repositories. It allows developers to define automated workflows triggered by events such as code pushes or pull requests.

When a GitHub Actions workflow is triggered, it is executed in a GitHub-hosted runner. The runner is a virtual machine that is configured with the tools and dependencies that are needed to run the workflow. The workflow is then executed in a series of steps, each defined in a YAML file.

Each step in a GitHub Actions workflow can perform a variety of tasks, such as running commands, installing dependencies, and uploading files. The steps can also be executed in parallel, which can help improve the workflow performance.

How Fastlane Match command work

The match command is a tool in the Fastlane framework that is used to manage and sync your certificates and provisioning profiles for iOS and Android apps. It allows you to store your certificates and profiles in a Git repository, Google Cloud Storage, or Amazon S3 bucket, and then have Fastlane automatically download and install the latest versions of these files when you build and release your app

Fastlane with GitHub Actions

Here is a more detailed explanation of how GitHub Actions and Fastlane work together:

  1. GitHub Actions Triggers Fastlane: A GitHub Actions workflow can include a step that triggers Fastlane. This is often done by invoking a specific Fastlane lane within a workflow step.
  2. Fastlane Executed in GitHub Actions Environment: Fastlane runs within the GitHub Actions environment when triggered. It can access the code repository, perform build tasks, and execute various actions defined in the Fastfile.
  3. Artifacts and Results: Fastlane can produce artifacts, such as build outputs or distribution packages. These artifacts can be published or used in subsequent workflow steps.
  4. GitHub Status and Notifications: Fastlane reports status and results back to GitHub Actions. This information is crucial for determining the success or failure of workflow steps and for triggering notifications.

Summary:

We will create our Fastlane lanes to submit the app to android and ios, with saving the certificates locally. Using the same approach, we will add that certificates to our secrets in github actions, and run the same fastlane.

Step by step to run Fastlane for Github actions

General

1- Make sure to have all the Dev tools needed for the React native app installed, check them here

2- Install Fastlane, follow the steps on this page

3- There are two ways of enabling Fastlane in your app, either by:

  • create a Fastlane folder, and put your Fastlane files there by running ‘’fastlane init’’
  • in both Android and ios folders, create a Fastlane folder and run ‘’ fastlane init’’

In our use case, we are going to use the second approach.

Use Fastlane AppFile

The Appfile is used to define a Bundle Identifier. By default, there is only one bundle identifier, but you can create one for each lane. You could also specify the app identifier directly in the Fastline file, but it’s more convenient to externalize it in the Appfile. I will share the certificate explanations later

check here for more details: https://docs.fastlane.tools/advanced/Appfile/

for ios

# ios/fastlane/AppFile
app_identifier(ENV["DEVELOPER_APP_IDENTIFIER"])
apple_id(ENV["FASTLANE_APPLE_ID"])
itc_team_id(ENV["APP_STORE_CONNECT_TEAM_ID"])
team_id(ENV["DEVELOPER_PORTAL_TEAM_ID"])

for Android

# android/fastlane/AppFile
json_key_file("<pathTosecretFile>.json") # Path to the json secret file -
package_name("appName....") # app package name for android

For Android, check this command for json_key_file

Add your certificates using ‘Fastlane match’

Step 1: run the following: fastlane match init

Step 2: you will have a folder like this, confirm the information:

#Matchfile
git_url("https://github.com/your_username/certificates.git")
type("appstore")
app_identifier("com.yourcompany.app")

Step 3: run ‘’fastlane match’’

Step 4: use ‘’fastlane match appstore’’ to synchronize your certificate in the ios folder

Step 5: Setup Ad-Hoc Provisioning Profile

Run this in the terminal at the root of this new repo from the previous step:

fastlane match adhoc

Follow the prompts to set up the provisioning profile. You will have to provide credentials to the App Store, keychain, and bundle ID. You may have to enter some of this information more than once.

check the official documentation for more details: https://docs.fastlane.tools/actions/match/

Use Fastlane

for Android

use the following script for Fastlane:

#android/fastlane/Fastlane
default_platform(:android)
platform :android do
desc 'Submit a new build to play store'
lane :deploy_to_play_store do |options|
#clean the build and create an .aab android build
gradle(task: 'clean bundleRelease', bundle_type: 'Release', project_dir: './andriod', properties: {
"android.injected.signing.store.file" => ENV['ANDROID_KEYSTORE_FILE'],
"android.injected.signing.store.password" => ENV['ANDROID_KEYSTORE_PASSWORD'],
"android.injected.signing.key.alias" => ENV['ANDROID_KEYSTORE_ALIAS'],
"android.injected.signing.key.password" => ENV['ANDROID_KEYSTORE_PASSWORD']
})
# send app to play store
upload_to_play_store(
track: 'production',
release_status: "draft",
aab: File.absolute_path('../android/app/build/outputs/bundle/release/app-release.aab'),
)
end
end

easy as that for Android, just check ‘’upload_to_play_store’’ for more details about the command if you got errors, and problems setting the certificates

In Android, we add the following certificates:

ANDROID_KEYSTORE_ALIAS

  • This refers to the alias used to identify a specific key entry within the Keystore. In Android, when you sign your app with a keystore, the keystore may contain multiple key entries, each identified by a unique alias. The alias helps differentiate between different keys within the keystore.

ANDROID_KEYSTORE_FILE

  • This is the path or filename of the keystore file. The Keystore is a binary file that holds cryptographic keys and certificates necessary for signing your Android app. It is used to verify the authenticity of the app and ensure that it has not been tampered with.

ANDROID_KEYSTORE_PASSWORD:

  • This is the password required to access and manipulate the keystore. The Keystore is typically protected with a password to ensure that only authorized individuals or processes can use it. The password is used when signing the app or when performing other operations related to the keystore.

How to get these certificates?

Generate a Keystore:

  • You can use the keytool utility that comes with the Java Development Kit (JDK) to generate a keystore. The command to create a Keystore might look like this:
keytool -genkeypair -v -keystore your_keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias your_alias
  • Replace your_keystore.jks with the desired keystore filename, and your_alias with the alias you want for your key entry.

Set Keystore Password:

  • During the keystore generation, you will be prompted to set a keystore password. This is the password you’ll use as ANDROID_KEYSTORE_PASSWORD.

Use Keystore Alias:

  • The alias you specify during keystore generation (your_alias in the example) is the value you use for ANDROID_KEYSTORE_ALIAS.

Locate the Keystore File:

  • The keystore file will be created in the directory where you run the keytool command unless you specify a different path. This file is what you use as ANDROID_KEYSTORE_FILE.

for iOS

The trick when it comes to pushing to ios, we have already set the certificates in the match directory. But we need more :

Certificates that we need:

PROVISIONING_PROFILE_SPECIFIER

We will get them from the match command

“match AppStore DEVELOPER_APP_IDENTIFIER”

DEVELOPER_APP_IDENTIFIER

If you have access to the Xcode project, you can find the Bundle Identifier (which is often referred to as the App Identifier) in your Xcode project settings:

  1. Open your Xcode project.
  2. Select your project in the Project Navigator.
  3. In the “General” tab, you will find a field named “Bundle Identifier.” This is your App Identifier.

DEVELOPER_APP_ID

The app ID in the Apple Store

DEVELOPER_PORTAL_TEAM_ID

Log in to the Apple Developer Portal:

  • Visit the Apple Developer portal.
  • Sign in with the Apple ID associated with your Apple Developer account.

Access Membership Information:

  • After logging in, you will be on the main page of the Developer portal.
  • Click on “Membership” in the left sidebar.

Find Team ID:

  • In the “Membership Information” section, you should see your Team ID listed.

The Team ID is a unique 10-character identifier associated with your Apple Developer account. It is often needed when interacting with Apple Developer services, APIs, or tools.

TEMP_KEYCHAIN_USER

set a user name for your temporary keychain, which can be used for getting local certificates in match

TEMP_KEYCHAIN_PASSWORD

set a password for your temporary keychain, which can be used for getting local certificates in match

APPLE_ISSUER_ID

  1. Go to https://developer.apple.com/ and sign in to your Apple Developer account.
  2. Click on the Account menu and select Certificates, Identifiers & and Profiles.
  3. In the Users and Access section, click on Keys.
  4. Your APPLE_ISSUER_ID will be displayed near the top of the page.

APPLE_KEY_ID

Log in to Apple Developer Account:

  • Visit the Apple Developer portal.
  • Sign in with your Apple ID.

Navigate to Certificates, Identifiers & Profiles:

  • In the Developer portal, go to the “Certificates, Identifiers & Profiles” section.

Access Keys:

  • Under “Keys,” you may find keys that you’ve created. If you don’t have one, you might need to create one.
  • Each key you create will have a unique identifier. This identifier is your APPLE_KEY_ID.

APPLE_KEY_CONTENT:

Generate a Key:

  • If you haven’t created a key, you might need to generate one. This often involves creating a key pair, with the private key being downloaded to your machine.

Download the Key:

  • After creating the key, download it to your machine. This file contains your private key.

Extract the Key Content:

  • Open the downloaded key file using a text editor.
  • The content of the key file is your APPLE_KEY_CONTENT. It usually starts with “ — — -BEGIN PRIVATE KEY — — -” and ends with “ — — -END PRIVATE KEY — — -”.

Note:

  • Keep your private key secure. It should be handled with care and not shared publicly.
  • Always refer to the latest Apple documentation for the most accurate and up-to-date information.

FASTLANE_APPLE_ID

The email used to access Github

MATCH_PASSWORD

password used to retrieve certificates from Github

GIT_AUTHORIZATION

also referred to as the Git authorization token

use this website to learn how

it should be ‘’<github username>:<token>’’

#ios/fastlane/Fastlane
default_platform(:ios)
#list of all secrets
DEVELOPER_APP_ID = ENV["DEVELOPER_APP_ID"]
DEVELOPER_APP_IDENTIFIER = ENV["DEVELOPER_APP_IDENTIFIER"]
PROVISIONING_PROFILE_SPECIFIER = ENV["PROVISIONING_PROFILE_SPECIFIER"]
TEMP_KEYCHAIN_USER = ENV["TEMP_KEYCHAIN_USER"]
TEMP_KEYCHAIN_PASSWORD = ENV["TEMP_KEYCHAIN_PASSWORD"]
APPLE_ISSUER_ID = ENV["APPLE_ISSUER_ID"]
APPLE_KEY_ID = ENV["APPLE_KEY_ID"]
APPLE_KEY_CONTENT = ENV["APPLE_KEY_CONTENT"]
GIT_AUTHORIZATION = ENV["GIT_AUTHORIZATION"]

# We will download and store our keychain into a file
def delete_temp_keychain(name)
delete_keychain(
name: name
) if File.exist? File.expand_path("~/Library/Keychains/#{name}-db")
end
def create_temp_keychain(name, password)
create_keychain(
name: name,
password: password,
unlock: false,
timeout: 0
)
end
def ensure_temp_keychain(name, password)
delete_temp_keychain(name)
create_temp_keychain(name, password)
end

platform :ios do
desc "Submit a new build to app store"
lane :deploy_to_app_store do
# Create the KeyChain certaficate and install it
ensure_temp_keychain(TEMP_KEYCHAIN_USER, TEMP_KEYCHAIN_PASSWORD)

# access to App Store Connect API
api_key = app_store_connect_api_key(
key_id: APPLE_KEY_ID,
issuer_id: APPLE_ISSUER_ID,
key_content: APPLE_KEY_CONTENT,
duration: 1200,
in_house: false
)

# get the certaficates from the Github account
match(
type: 'appstore',
app_identifier: "#{DEVELOPER_APP_IDENTIFIER}",
git_basic_authorization: Base64.strict_encode64(GIT_AUTHORIZATION),
readonly: true,
keychain_name: TEMP_KEYCHAIN_USER,
keychain_password: TEMP_KEYCHAIN_PASSWORD,
api_key: api_key
)
# Build the App
ipa = gym(
configuration: "Release",
export_xcargs: "-allowProvisioningUpdates",
workspace: 'AppName.xcworkspace',
scheme: "AppName Release",
export_method: "app-store",
export_options: {
provisioningProfiles: {
DEVELOPER_APP_ID => PROVISIONING_PROFILE_SPECIFIER
}
}
)
#Ship to TestFlight
pilot(
apple_id: "#{DEVELOPER_APP_ID}",
app_identifier: "#{DEVELOPER_APP_IDENTIFIER}",
distribute_external: false,
notify_external_testers: true,
uses_non_exempt_encryption: false,
ipa: ipa,
skip_waiting_for_build_processing: true
)
end
end

Handling Environment variables:

As you can see from our Fastlane file, we have different environmental variables, which need to be used locally, to build and update your app. To do that:

  1. Create a ‘.env’ file in each Fastlane folder in both Android and IoS
  2. For each one, use the variables that you need, for example, the IoS one:
// ios/fastlane/.env
APPLE_ISSUER_ID=""
APPLE_KEY_CONTENT="-----BEGIN PRIVATE KEY-----
//Private Key
-----END PRIVATE KEY-----"
APPLE_KEY_ID=""
APP_STORE_CONNECT_TEAM_ID=""
DEVELOPER_APP_ID=""
DEVELOPER_APP_IDENTIFIER=""
DEVELOPER_PORTAL_TEAM_ID=""
FASTLANE_APPLE_ID=""
MATCH_PASSWORD=""
PROVISIONING_PROFILE_SPECIFIER="match AppStore <DEVELOPER_APP_IDENTIFIER>"
TEMP_KEYCHAIN_PASSWORD=""
TEMP_KEYCHAIN_USER=""
GIT_AUTHORIZATION="<github username>:<token>"

Setup your GitHub actions

1- We need the rule to run the script for Github actions, we can use different methods to trigger the build such as when we push to a branch or any approach we want. check this cheat sheet about Github actions

for our case, we run Github actions when we do a Pull Request to a certain branch, we call it in our example ‘’production_ios_android’’, so the script will run.

To run that, we create a ‘’.yml’’ file for that. let’s call it ‘’ship-android-ios.yml’’, we add it to GitHub folder under

GitHub Actions for Android and IoS build:

#./github/workflows/ship-android-ios.yml
name: Create build for both Android and IoS, upload to Google and Apple Store
on:
pull_request:
branches:
- production_ios_android # This branch should be used for both Android and IOS build triggers.
jobs:
# ----------------------- ANDROID -----------------------------
# -------------------------------------------------------------
build-prod-android:
name: Build & ship android prod app
runs-on: ubuntu-latest
steps:
# Step 1: Setup java and node
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
distribution: zulu
java-version: 11
- name: Set up NODE
uses: actions/setup-node@v3
with:
node-version: 14 # using the .nvmrc file in future
# Step 2: Configure gradel and gradle wrapper with cache
- name: Cache Gradle Wrapper
uses: actions/cache@v2
with:
path: ~/.gradle/wrapper
key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
- name: Cache Gradle Dependencies
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-caches-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-caches-
# Step 3: Install node packages
- name: Install node modules
run: yarn install
env:
CI: true
# Step 4: Setup ruby
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
ruby-version: "2.7"
# Step 5: Install bundler, and fastlane
- name: Install Fastlane
run: cd android && bundle install && cd ../
env:
CI: true
# Step 6: Install reactnative community pacakge manually
- name: Add @react-native-community manually
run: yarn add @react-native-community/cli-platform-android@7.0.1
env:
CI: true

# Step 8: Fastlane build android
- name : Build android App via fastlane
run : fastlane deploy_to_play_store
env:
CI: true
CD: true
ANDROID_KEYSTORE_FILE: ${{ secrets.ANDROID_KEYSTORE_FILE }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_KEYSTORE_ALIAS }}

# -------------------------------------------------------------
# ----------------------- IOS ---------------------------------
# -------------------------------------------------------------
build-prod-ios:
runs-on: macos-12
steps:
- uses: actions/checkout@v2
# Step 1: Install Node Version 14
- name: Set up Node.js
uses: actions/setup-node@v1
with:
node-version: 14
# Step 2: Install Ruby
- name: Install Ruby
uses: actions/setup-ruby@v1
with:
ruby-version: 2.7
# Step 3: Install Node Modules
- name: Install Node Modules
run: yarn
env:
CI: true

# Step 6: Install Pods
- name: Install pods
run: cd ios && pod install --repo-update && cd ..

# Step 7: Install Fastlane
- name: Install Fastlane
run: bundle install && bundle update
# Step 8: Run Fastlane to build and ship the build
- name: Build and upload to TestFlight
run: fastlane deploy_to_app_store
env:
APP_STORE_CONNECT_TEAM_ID: '${{ secrets.APP_STORE_CONNECT_TEAM_ID }}'
DEVELOPER_APP_ID: '${{ secrets.DEVELOPER_APP_ID }}'
DEVELOPER_APP_IDENTIFIER: '${{ secrets.DEVELOPER_APP_IDENTIFIER }}'
DEVELOPER_PORTAL_TEAM_ID: '${{ secrets.DEVELOPER_PORTAL_TEAM_ID }}'
FASTLANE_APPLE_ID: '${{ secrets.FASTLANE_APPLE_ID }}'
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: '${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}'
MATCH_PASSWORD: '${{ secrets.TEMP_KEYCHAIN_PASSWORD }}'
GIT_AUTHORIZATION: '${{ secrets.GIT_AUTHORIZATION }}'
PROVISIONING_PROFILE_SPECIFIER: '${{ secrets.PROVISIONING_PROFILE_SPECIFIER }}'
TEMP_KEYCHAIN_PASSWORD: '${{ secrets.TEMP_KEYCHAIN_PASSWORD }}'
TEMP_KEYCHAIN_USER: '${{ secrets.TEMP_KEYCHAIN_USER }}'
APPLE_KEY_ID: '${{ secrets.APPLE_KEY_ID }}'
APPLE_ISSUER_ID: '${{ secrets.APPLE_ISSUER_ID }}'
APPLE_KEY_CONTENT: '${{ secrets.APPLE_KEY_CONTENT }}'

PS: the build number is done manually, we can use the Fastlane increment number lane.

Hope that I helped you understand and implement the CI-CD process with this article. Follow me on Linkedin for more insights

--

--

Malik Chohra
Malik Chohra

Written by Malik Chohra

React Native, AI, and Tech. I help you stay relevant as a Tech person in the AI age

No responses yet