• Home
  • Blog
  • Automating React Native Builds with Fastlane: A Comprehensive Guide

Automating React Native Builds with Fastlane: A Comprehensive Guide

fastlane-automating-react-native-builds

In the fast-paced world of mobile app development, efficiency and consistency are paramount. Fastlane is a powerful automation tool that streamlines the build and deployment processes, enabling developers to automate repetitive tasks, manage environment variables, and ensure consistent builds across different environments. This comprehensive guide will walk you through integrating Fastlane into your React Native project, focusing on building locally first, while also highlighting its seamless integration with Continuous Integration (CI) systems like GitHub Actions and CircleCI.

Introduction to Fastlane

Fastlane is an open-source platform aimed at simplifying Android and iOS deployment. It automates the tedious and error-prone tasks involved in app deployment, such as generating screenshots, dealing with provisioning profiles, and releasing apps to the App Store or Google Play. By integrating Fastlane into your React Native project, you can achieve a more streamlined, reliable, and efficient build and deployment process.

Prerequisites

Before diving into the integration process, ensure you have the following setup:

  • React Native Project: A functional React Native project ready for deployment.
  • Ruby Installed: Fastlane is built on Ruby, so ensure you have Ruby installed on your development machine.
  • Fastlane Installed: You can install Fastlane using RubyGems:

    sudo gem install fastlane -NV
  • Access to Developer Accounts: Ensure you have access to your Apple Developer and Google Play accounts for app signing and deployment.

Setting Up Fastlane

1. Initializing Fastlane

Navigate to your project directory and initialize Fastlane:

cd path-to-your-react-native-project
fastlane init

Follow the prompts to set up Fastlane for both iOS and Android platforms. This process will generate essential configuration files like Appfile and Fastfile.

2. Configuring Environment Variables

To manage sensitive information and configurations, it's best to use environment variables. Create a .env.default file in the root of your project with the following content:

//.env.default
ANDROID_SIGN_KEY_ALIAS="AndroidSignKeyAlias"
ANDROID_SIGN_KEY_PASSWORD="AndroidSignKeyPassword"
ANDROID_SIGN_KEY_FILE="/absolute-path-to-your-react-native-project/fastlane/somefile.keystore"
ANDROID_PACKAGE_NAME="com.appelian.mvpapp"
ANDROID_UPLOAD_TRACK="internal"
ANDROID_AAB_FILE_PATH="/absolute-path-to-your-react-native-project/android/app/build/outputs/bundle/release/app-release.aab"

IOS_KEYCHAIN_PASSWORD="MyAwesomeKeychainPassword"
IOS_CERTIFICATE_PASSWORD="MyAwesomeCertificatePassword"
IOS_CERTIFICATE_PATH="ios-team-id.cer"
IOS_PROVISIONING_PROFILE_PATH="AppStore_com.appelian.mvpapp.mobileprovision"
IOS_PROVISIONING_PROFILE_NAME="com.appelian.mvpapp AppStore"
IOS_TEAM_ID="ios-team-id"
IOS_TESTFLIGHT_EMAIL="support@appelian.com"
IOS_BUNDLE_ID="com.appelian.mvpapp"
IOS_TESTFLIGHT_KEY_ID="testflight-api-key-id"
IOS_TESTFLIGHT_ISSUER_ID="testflight-api-issuer-id"
IOS_TESTFLIGHT_KEY_FILEPATH="/absolute-path-to-your-react-native-project/fastlane/AuthKey_testflight-api-key-id.p8"
IOS_TESTFLIGHT_GROUP_NAME="Appelian"
IOS_WORKSPACE_PATH="ios/MvpApp.xcworkspace"
IOS_XCODEPROJ_PATH="ios/MvpApp.xcodeproj"
IOS_SCHEME="MvpApp"
IOS_IPA_PATH="./MvpApp.ipa"
IOS_DSYM_PATH="./MvpApp.app.dSYM.zip"

CRASHLYTICS_API_KEY="crashlytics-api-key"

SLACK_CHANNEL_ID="#my_channel"
3. Setting Up the .gitignore File

Ensure that sensitive files and build artifacts are excluded from version control by updating your .gitignore file:

//.gitignore
# Fastlane files
**/fastlane/report.xml
**/fastlane/Preview.html
**/fastlane/screenshots
**/fastlane/test_output

# Bundler
/vendor/bundle/

# Provisioning profiles and certificates
AppStore_*.mobileprovision
ios-team-id.cer
.env.default
MvpApp.app.dSYM.zip
MvpApp.ipa
AuthKey_*.p8

This configuration prevents sensitive data and unnecessary build files from being committed to your repository.

Understanding Configuration Files

.env.default

This file contains all the environment variables required for both iOS and Android build processes. It includes credentials, file paths, and other configurations essential for signing and deploying your app.

//.env.default
ANDROID_SIGN_KEY_ALIAS="AndroidSignKeyAlias"
ANDROID_SIGN_KEY_PASSWORD="AndroidSignKeyPassword"
ANDROID_SIGN_KEY_FILE="/absolute-path-to-your-react-native-project/fastlane/somefile.keystore"
ANDROID_PACKAGE_NAME="com.appelian.mvpapp"
ANDROID_UPLOAD_TRACK="internal"
ANDROID_AAB_FILE_PATH="/absolute-path-to-your-react-native-project/android/app/build/outputs/bundle/release/app-release.aab"

IOS_KEYCHAIN_PASSWORD="MyAwesomeKeychainPassword"
IOS_CERTIFICATE_PASSWORD="MyAwesomeCertificatePassword"
IOS_CERTIFICATE_PATH="ios-team-id.cer"
IOS_PROVISIONING_PROFILE_PATH="AppStore_com.appelian.mvpapp.mobileprovision"
IOS_PROVISIONING_PROFILE_NAME="com.appelian.mvpapp AppStore"
IOS_TEAM_ID="ios-team-id"
IOS_TESTFLIGHT_EMAIL="support@appelian.com"
IOS_BUNDLE_ID="com.appelian.mvpapp"
IOS_TESTFLIGHT_KEY_ID="testflight-api-key-id"
IOS_TESTFLIGHT_ISSUER_ID="testflight-api-issuer-id"
IOS_TESTFLIGHT_KEY_FILEPATH="/absolute-path-to-your-react-native-project/fastlane/AuthKey_testflight-api-key-id.p8"
IOS_TESTFLIGHT_GROUP_NAME="Appelian"
IOS_WORKSPACE_PATH="ios/MvpApp.xcworkspace"
IOS_XCODEPROJ_PATH="ios/MvpApp.xcodeproj"
IOS_SCHEME="MvpApp"
IOS_IPA_PATH="./MvpApp.ipa"
IOS_DSYM_PATH="./MvpApp.app.dSYM.zip"

CRASHLYTICS_API_KEY="crashlytics-api-key"

SLACK_CHANNEL_ID="#my_channel"
.gitignore

The .gitignore file ensures that sensitive files and unnecessary build artifacts are not tracked by Git. This helps in maintaining the security of your project and keeps the repository clean.

//.gitignore
# Fastlane files
**/fastlane/report.xml
**/fastlane/Preview.html
**/fastlane/screenshots
**/fastlane/test_output

# Bundler
/vendor/bundle/

# Provisioning profiles and certificates
AppStore_*.mobileprovision
ios-team-id.cer
.env.default
MvpApp.app.dSYM.zip
MvpApp.ipa
AuthKey_*.p8
Appfile

The Appfile contains project-specific configurations, primarily for iOS deployments. It references environment variables defined in your .env.default file.

// fastlane/Appfile
json_key_file(ENV['GOOGLE_PLAY_JSON_FILE'])
package_name(ENV['IOS_BUNDLE_ID'])
  • json_key_file: Path to the JSON key file for Google Play.
  • package_name: The bundle identifier for your iOS app.
Fastfile

The Fastfile is where you define your automation lanes and the actions Fastlane should perform. Below is the provided Fastfile content with annotations explaining each section.

// fastlane/Fastfile

lane :incrementAndroidBuildNumber do
    androidFilePath = '../android/app/build.gradle'
    version_code = /versionCode\s+(\d+)/
    version_number = /versionName\s+"\d+\.\d+\.(\d+)"/
    s = File.read(androidFilePath)
    
# increment the version code   
    versionCode = s[version_code, 1].to_i
    s[version_code, 1] = (versionCode + 1).to_s

# you can increment also the version number
#    versionNumber = s[version_number, 1].to_i
#    s[version_number, 1] = (versionNumber + 1).to_s

    f = File.new(androidFilePath, 'w')
    f.write(s)
    f.close
end

lane :incrementPackageNumber do
    packageFilePath = '../package.json'
    version_number = /"version"\s*:\s*"\d+\.\d+\.(\d+)"/
    s = File.read(packageFilePath)

    versionNumber = s[version_number, 1].to_i
    s[version_number, 1] = (versionNumber + 1).to_s

    f = File.new(packageFilePath, 'w')
    f.write(s)
    f.close
end

def setupCodeSigning(keychainPassword, certificatePassword, certificatePath, profilePath)
 create_keychain(
  name: "CI",
  password: keychainPassword,
  default_keychain: true,
  unlock: true,
  timeout: 360000,
  lock_when_sleeps: false
 )

 install_provisioning_profile(path: profilePath)

 import_certificate(
  certificate_path: certificatePath,
  certificate_password: certificatePassword,
  keychain_name: "CI",
  keychain_password: keychainPassword
 )
end

lane :beta do
  incrementPackageNumber

  betaAndroid()
  betaIos()

# you can also commit the version bump
#   commit_version_bump(xcodeproj: ENV['IOS_XCODEPROJ_PATH'])

# Send Slack message
  slack(
    message:  "AppelianApp is ready",
    channel: ENV['SLACK_CHANNEL_ID'],  # Optional, by default will post to the default channel configured for the POST URL.
    success: true,        # Optional, defaults to true.
    payload: {  # Optional, lets you specify any number of your own Slack attachments.
      "Version" => versionNumber,
      "Build Number" => versionCode,
      "Date" => Time.new.to_s,
      "Built by" => "Local",
    },
    default_payloads: [:git_branch, :git_author],
  )
end

lane :betaIos do
    setupCodeSigning(
      ENV['IOS_KEYCHAIN_PASSWORD'],
      ENV['IOS_CERTIFICATE_PASSWORD'],
      ENV['IOS_CERTIFICATE_PATH'],
      ENV['IOS_PROVISIONING_PROFILE_PATH']
    )

    update_code_signing_settings(
      use_automatic_signing: false,
      path: ENV['IOS_XCODEPROJ_PATH'],
      team_id: ENV['IOS_TEAM_ID'],
      bundle_identifier: ENV['IOS_BUNDLE_ID'],
      code_sign_identity: "iPhone Distribution",
      sdk: "iphoneos*",
      profile_name: ENV['IOS_PROVISIONING_PROFILE_NAME'],
    )

    update_project_provisioning(
      xcodeproj: ENV['IOS_XCODEPROJ_PATH'],
      profile: ENV['IOS_PROVISIONING_PROFILE_PATH'], # optional if you use sigh
      target_filter: ENV['IOS_SCHEME'], # matches name or type of a target
      build_configuration: "Release",
    )

    api_key = app_store_connect_api_key(
      key_id: ENV['IOS_TESTFLIGHT_KEY_ID'],
      issuer_id: ENV['IOS_TESTFLIGHT_ISSUER_ID'],
      key_filepath: ENV['IOS_TESTFLIGHT_KEY_FILEPATH'],
      duration: 1200, # optional (maximum 1200)
      in_house: false # optional but may be required if using match/sigh
    )

# increment the build number
    increment_build_number(
        xcodeproj: ENV['IOS_XCODEPROJ_PATH']
    )

# you can increment also the version number
#     increment_version_number(
#       bump_type: "patch",
#       xcodeproj: ENV['IOS_XCODEPROJ_PATH']
#     )

# iOS build and upload to the TestFlight
  build_app(scheme: ENV['IOS_SCHEME'],
            configuration: "Release",
            export_method: "app-store",
            export_options: {
                provisioningProfiles: {
                ENV['IOS_BUNDLE_ID'] => "#{ENV['IOS_BUNDLE_ID']} AppStore",
                }
            },
            workspace: ENV['IOS_WORKSPACE_PATH'],
#             include_bitcode: true
            )

    upload_to_testflight(
      username: ENV['IOS_TESTFLIGHT_EMAIL'],
      app_identifier: ENV['IOS_BUNDLE_ID'],
      skip_waiting_for_build_processing: true,
      skip_submission: true,
      groups: [
          ENV['IOS_TESTFLIGHT_GROUP_NAME']
      ]
     )

# if you use Firebase Crashlytics, you can upload debug symbols
     upload_symbols_to_crashlytics(
      binary_path: ENV['IOS_IPA_PATH'],
      dsym_path: ENV['IOS_DSYM_PATH'],
      api_token: ENV['CRASHLYTICS_API_KEY']
     )

     #    rescue => exception
     #      on_error(options[:slackUrl], "Build Failed", “#build-errors-channel”, exception)
end

lane :betaAndroid do
    incrementAndroidBuildNumber()

#   Android build and upload to beta
    gradle(
        task: 'bundle', # aab
#         task: 'assemble', # apk
        build_type: 'Release',
        project_dir: "./android",
        properties: {
            "android.injected.signing.store.file" => ENV['ANDROID_SIGN_KEY_FILE'],
            "android.injected.signing.store.password" => ENV['ANDROID_SIGN_KEY_PASSWORD'],
            "android.injected.signing.key.alias" => ENV['ANDROID_SIGN_KEY_ALIAS'],
            "android.injected.signing.key.password" => ENV['ANDROID_SIGN_KEY_PASSWORD'],
          }
      )
    supply(
        track: ENV['ANDROID_UPLOAD_TRACK'],
        package_name: ENV['ANDROID_PACKAGE_NAME'],
        skip_upload_metadata: true,
        skip_upload_images: true,
        skip_upload_screenshots: true,
        skip_upload_apk: true
    )
end
Explanation of Key Sections:
  • Incrementing Build Numbers:

    • incrementAndroidBuildNumber - reads the build.gradle file, increments the versionCode and versionName by 1, and writes the changes back to the file.
    • incrementPackageNumber - reads the package.json file, increments the version patch number by 1, and updates the file.
  • setupCodeSigning (Code Signing Setup) - creates a new keychain, installs the provisioning profile, and imports the necessary certificates for iOS code signing.
  • beta (Beta Lane) - a lane that orchestrates the beta build process by incrementing the package number, building both Android and iOS versions, and sending a Slack notification upon completion.
  • betaIos (iOS Beta Lane) - handles iOS-specific build tasks, including code signing, provisioning profile updates, building the app, uploading to TestFlight, and uploading symbols to Crashlytics.
  • betaAndroid (Android Beta Lane) - handles Android-specific build tasks, including incrementing the build number, building the app bundle, and uploading it to Google Play using supply.