Using fastlane to automate your react native deployment
At Made by Prism we use fastlane to automate our iOS and Android deployment across teams. If you have deployed apps manually, you’ll know that this is a particularly lengthy, complicated and manual process. Fastlane is a great tool which will mitigate the time needed for each deployment freeing up time to concentrate on the product itself.
Definition:
Fastlane is an open source platform aimed at simplifying Android and iOS deployment. Fastlane lets you automate every aspect of your development and release workflow.
In this article I’ll run through our boilerplate setup which we use as a base for each project, and explain each part as we go. I’ll explain our iOS process only, as android is far less complicated. After reading this article, paired with the fastlane docs, it shouldn’t be too difficult to set this up yourself.
As for the prerequisites, you’ll need to have a react native project setup. We use build configurations to handle our environments, which isn’t essential for fastlane but it is engrained to our fastlane setup. So if you handle your react native environments another way you’ll need to read around what’s to come.
You’ll also need to install fastlane – https://docs.fastlane.tools/getting-started/ios/setup/.
The iOS process:
Firstly you’ll need a shared account user added to your iOS developer account. This user and password will be used across your team for deployments. This is important as provisioning profiles must be installed on your machine to deploy. If each member of your team used their own user, then you’ll have a nightmare with deployments and Git conflicts.
Create a fast file: ‘ios/fastlane/Fastfile’. In here we’ll use ruby to define our fastlane ‘lanes’ which we’ll be using. Lanes are like functions which you call via the command line with fastlane.
# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
# For a list of all available actions, check out
# https://docs.fastlane.tools/actions
# For a list of all available plugins, check out
# https://docs.fastlane.tools/plugins/available-plugins
# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane
default_platform(:ios)
platform :ios do
end
Code: ‘fastlane lane_name’.
Create an env file for the fastfile to read: ‘ios/fastlane/.env’.
In the fastfile we’ll create a new lane to produce the apps we need in the dev center and App Store connect. This lane will be run once on project setup and everything’s in place ready for deployment.
desc "Create Beta and App Store apps on both App Store Connect and the Apple Developer Portal"
lane :produce_apps do
# Debug
produce(
username: 'your-shared-user@test.com',
team_id: 'your-apple-team-id',
itc_team_id: 'your-itunes-connect-team-id',
app_identifier: "#{ENV['BASE_BUNDLE_ID']}.debug",
app_name: "#{ENV['APP_NAME']} debug",
enable_services: {
push_notification: "on",
app_group: "on",
}
)
# Debug onesignal extension
produce(
username: 'your-shared-user@test.com',
team_id: 'your-apple-team-id',
itc_team_id: 'your-itunes-connect-team-id',
app_identifier: "#{ENV['BASE_BUNDLE_ID']}.debug.OneSignalNotificationServiceExtension",
app_name: "#{ENV['APP_NAME']} debug onesignal",
skip_itc: true,
enable_services: {
app_group: "on",
}
)
# Beta
produce(
username: 'your-shared-user@test.com',
team_id: 'your-apple-team-id',
itc_team_id: 'your-itunes-connect-team-id',
app_identifier: "#{ENV['BASE_BUNDLE_ID']}.beta",
app_name: "#{ENV['APP_NAME']} beta",
enable_services: {
push_notification: "on",
app_group: "on",
}
)
# Beta onesignal extension
produce(
username: 'your-shared-user@test.com',
team_id: 'your-apple-team-id',
itc_team_id: 'your-itunes-connect-team-id',
app_identifier: "#{ENV['BASE_BUNDLE_ID']}.beta.OneSignalNotificationServiceExtension",
app_name: "#{ENV['APP_NAME']} beta onesignal",
skip_itc: true,
enable_services: {
app_group: "on",
}
)
# Release
produce(
username: 'your-shared-user@test.com',
team_id: 'your-apple-team-id',
itc_team_id: 'your-itunes-connect-team-id',
app_identifier: "#{ENV['BASE_BUNDLE_ID']}",
app_name: "#{ENV['APP_NAME']}",
enable_services: {
push_notification: "on",
app_group: "on",
}
)
# Release onesignal extension
produce(
username: 'your-shared-user@test.com',
team_id: 'your-apple-team-id',
itc_team_id: 'your-itunes-connect-team-id',
app_identifier: "#{ENV['BASE_BUNDLE_ID']}.OneSignalNotificationServiceExtension",
app_name: "#{ENV['APP_NAME']} onesignal",
skip_itc: true,
enable_services: {
app_group: "on",
}
)
end
Here we have a lane which is running the ‘produce’ (https://docs.fastlane.tools/actions/produce/) 6 times. This creates 3 apps we use for development setup in Xcode via build configurations. We use Debug, Beta and Release configurations. We also use one signal as default which requires a service extension, you’ll notice these are also added in this lane as we require these apps to be created in the developer portal, but not iTunes connect, you’ll notice the ‘skip_itc: true,’ as it’s not required as an actual app only an identifier in the developer portal.
You’ll notice the use of ‘ENV’, this will be picking up variables in your env you just made. We’ll start with ‘BASE_BUNDLE_ID’ and ‘APP_NAME’.
BASE_BUNDLE_ID=com.madebyprism.mbpcorern
APP_NAME='Your App Name'
We design our bundle ids for each configuration, we inject a base bundle id into fastlane of ‘com.madebyprism.fastlane.example’. As you can see, this is used directly for release, however debug and beta resolve as ‘com.madebyprism.fastlane.example.debug’ and ‘com.madebyprism.fastlane.example.beta’. This gives us a protocol for each project and is very easy to setup by choosing a base.
Now that we have the produce command setup, we’ll need to generate some ‘pem’s’ for OneSignal to work. Fastlane can also handle this for us.
desc "Create pems for push notifications"
lane :gen_pems do
# Debug
get_push_certificate(
username: 'your-shared-user@test.com',
team_id: 'your-apple-team-id',
# force: true, # create a new profile, even if the old one is still valid
development: true,
app_identifier: "#{ENV['BASE_BUNDLE_ID']}.debug", # optional app identifier,
save_private_key: true,
output_path: "save/file/to",
new_profile: proc do |profile_path| # this block gets called when a new profile was generated
# Update onesignal account with new pem
onesignal(
# app_id: "You can add the id and uncomment after first run",
app_name: "#{ENV['APP_NAME']}",
auth_token: "#{ENV['ONESIGNAL_DEBUG_AUTH_TOKEN']}",
apns_p12: "save/p12/to",
apns_env: "sandbox"
)
end
)
# Beta development
get_push_certificate(
username: 'your-shared-user@test.com',
team_id: 'your-apple-team-id',
# force: true, # create a new profile, even if the old one is still valid
development: true,
app_identifier: "#{ENV['BASE_BUNDLE_ID']}.beta", # optional app identifier,
save_private_key: true,
output_path: "save/file/to",
new_profile: proc do |profile_path| # this block gets called when a new profile was generated
# Update onesignal account with new pem
onesignal(
# app_id: "You can add the id and uncomment after first run",
app_name: "#{ENV['APP_NAME']} Beta",
auth_token: "#{ENV['ONESIGNAL_BETA_AUTH_TOKEN']}",
apns_p12: "save/p12/to",
apns_env: "sandbox"
)
end
)
# Beta production
get_push_certificate(
username: 'your-shared-user@test.com',
team_id: 'your-apple-team-id',
# force: true, # create a new profile, even if the old one is still valid
app_identifier: "#{ENV['BASE_BUNDLE_ID']}.beta", # optional app identifier,
save_private_key: true,
output_path: "save/certs/to",
new_profile: proc do |profile_path| # this block gets called when a new profile was generated
# Update onesignal account with new pem
onesignal(
# app_id: "You can add the id and uncomment after first run",
app_name: "#{ENV['APP_NAME']} Beta",
auth_token: "#{ENV['ONESIGNAL_BETA_AUTH_TOKEN']}",
apns_p12: "save/p12/to",
)
end
)
# Release
get_push_certificate(
username: 'your-shared-user@test.com',
team_id: 'your-apple-team-id',
# force: true, # create a new profile, even if the old one is still valid
app_identifier: "#{ENV['BASE_BUNDLE_ID']}", # optional app identifier,
save_private_key: true,
output_path: "save/file/to",
new_profile: proc do |profile_path| # this block gets called when a new profile was generated
# Update onesignal account with new pem
onesignal(
# app_id: "You can add the id and uncomment after first run",
app_name: "#{ENV['APP_NAME']} Beta",
auth_token: "#{ENV['ONESIGNAL_RELEASE_AUTH_TOKEN']}",
apns_p12: "save/p12/to",
)
end
)
end
Here we’re creating 4 push certificates via ‘get_push_certificate’ (https://docs.fastlane.tools/actions/pem/), two sandbox/development certs for debug and beta, and two production certs for beta and production. What happens here is fastlane will request the certificates be created for ‘app_identifier:’ If they don’t already exist or the current are expired. If a new cert is created the ‘new_profile:’ function will be called. Which we then use ‘onesignal’ (http://docs.fastlane.tools/actions/onesignal/), to upload the certificate and create a new one signal account.
You’ll notice that ‘app_id:’ is commented out in this function, that’s because on the first run we don’t have these accounts setup. Once you do, be sure to update this with the correct app id and uncomment. Then when you run this again, if the certs need renewing, it will do so and update your one signal app!
You’ll notice there’s 3 new env variables here, they are your auth tokens found in OneSignal which allow fastlane access. We use 3 different env variables in case we end up using different accounts, if you’re using the same account for all 3 then these will have the same value.
Now we’ll move on to code signing. The grunt of deployments. Luckily, fastlane can handle this too. We’ll use ‘match’ (https://docs.fastlane.tools/actions/match/) to do this.
desc "Creates all required certificates & provisioning profiles"
lane :gen_match do
# Beta adhoc
match(
username: 'your-shared-user@test.com',
team_id: 'your-apple-team-id',
type: "adhoc",
app_identifier: ["#{ENV['BASE_BUNDLE_ID']}.beta", "#{ENV['BASE_BUNDLE_ID']}.beta.OneSignalNotificationServiceExtension"],
git_url: "git@your-repo.git",
git_branch: "master",
)
# Beta appstore
match(
username: 'your-shared-user@test.com',
team_id: 'your-apple-team-id',
type: "appstore",
app_identifier: ["#{ENV['BASE_BUNDLE_ID']}.beta", "#{ENV['BASE_BUNDLE_ID']}.beta.OneSignalNotificationServiceExtension"],
git_url: "git@your-repo.git",
git_branch: "master",
)
# Release
match(
username: 'your-shared-user@test.com',
team_id: 'your-apple-team-id',
type: "appstore",
app_identifier: ["#{ENV['BASE_BUNDLE_ID']}", "#{ENV['BASE_BUNDLE_ID']}.OneSignalNotificationServiceExtension"],
git_url: "git@your-repo.git",
git_branch: "master",
)
end
Firstly, you’ll need to create Git repo where your code signing will be stored. This allows your code signing to be shared by your team, mitigating the chaos of multiple code signing and Git conflicts in the product’s code base. When you run this command, you’ll notice fastlane asks for a password to initialise the Git repo, make sure you make this strong and store it safely. Obviously the Git repo should be private as well, this is just an extra precaution from fastlane!
The match function can take multiple ‘app_identifier:’, which we’ll need to bundle together the main app and the OneSignal service extension.
So now after running this command we have code signing setup. We can go over to Xcode and set these up in our build configurations.
For debug, you can see in the fastfile we didn’t define a match function for debug, that’s because we don’t need one as it’s not going to be deployed. This should be set to “automatically manage signing” and Xcode will handle the rest for your team members individually.
Release and beta should not be set automatic, and you will now be able to select the provisioning profiles for each. Make sure you select AppStore for beta. The AdHoc is there for AdHoc builds in case you need it.
Now we have our code signing sorted, we can deploy!
Let’s set up 2 lanes, one for sending our beta configuration to test flight and one for our release build.
desc "Push a new beta build to TestFlight"
lane :beta do
increment_build_number(xcodeproj: "#{ENV['XPROJECT_NAME']}")
match(
username: 'your-shared-user@test.com',
team_id: 'your-apple-team-id',
type: "appstore",
app_identifier: ["#{ENV['BASE_BUNDLE_ID']}.beta", "#{ENV['BASE_BUNDLE_ID']}.beta.OneSignalNotificationServiceExtension"],
git_url: "git@your-repo.git",
git_branch: "master",
readonly: true,
)
sh('yarn', 'setup:all')
gym(
workspace: "#{ENV['XWORKSPACE_NAME']}",
scheme: "#{ENV['SCHEME_NAME']}",
configuration: "Beta",
export_method: 'app-store',
output_directory: "builds",
output_name: 'beta.ipa',
)
upload_to_testflight(
username: "your-shared-user@test.com",
team_id: "your-apple-team-id",
ipa: "builds/beta.ipa"
)
end
Here we’re using a few new functions:
- Increment build number, rather self explanatory but it should be used as it increments the build number across all targets.
- We’ll run ‘match’ again with the ‘readonly:’ flag set to true. This grabs our code signing we created earlier ready for the build.
- sh(‘yarn’, ‘setup:all’), this is a yarn command we use to run anything we need to do, project specifically, to make sure everything is in order before the build runs. Feel free to omit this or change to yarn install.
- Now we’ll run ‘gym’ (https://docs.fastlane.tools/actions/gym/), which will handle our build. We’ll need new env variables here, selecting your workspace and scheme. This outputs the build to file locally at ‘builds/beta.ipa’.
- We’ll then use the recent build to ‘upload_to_testflight’ (http://docs.fastlane.tools/actions/upload_to_testflight/) and we’re sorted! One command and your app is packaged up and sent to fastlane, fully automated!
- We can now swap update the gym configuration pretty quickly to build our release app, and swap ‘upload_to_testflight’ to ‘upload_to_app_store’.
desc "Push a new release build to the App Store"
lane :release do
increment_build_number(xcodeproj: "#{ENV['XPROJECT_NAME']}")
match(
username: 'your-shared-user@test.com',
team_id: 'your-apple-team-id',
type: "appstore",
app_identifier: ["#{ENV['BASE_BUNDLE_ID']}", "#{ENV['BASE_BUNDLE_ID']}.OneSignalNotificationServiceExtension"],
git_url: "git@your-repo.git",
git_branch: "master",
readonly: true,
)
sh('yarn', 'setup:all')
gym(
workspace: "#{ENV['XWORKSPACE_NAME']}",
scheme: "#{ENV['SCHEME_NAME']}",
configuration: "Release",
output_directory: "builds",
output_name: 'release.ipa',
)
upload_to_app_store(
skip_metadata: true,
skip_screenshots: true,
username: "your-shared-user@test.com",
team_id: "your-apple-team-id",
ipa: "builds/release.ipa",
)
end
So you’ve learnt how to setup iOS apps, pems, code signing and deployments all automated via fastlane. This relatively small investment in setup will save you and your team precious time going forward. If you know the hardship of doing this manually, or you’re new to the game, it’s a no brainer!