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.
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.
Before diving into the integration process, ensure you have the following setup:
Fastlane Installed: You can install Fastlane using RubyGems:
sudo gem install fastlane -NV
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.
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"
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.
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"
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
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'])
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
Incrementing Build Numbers: