Few months ago Microsoft has announced they plans to sunset AppCenter, a popular platform for building and releasing mobile applications. I needed to find another solution for distributing React Native application. My research has brought me to the conclusion that there are not many options except Fastlane. As such the journey of automating the builds with Fastlane has began.
For those who doesn’t know it’s a tool for automating mobile app development and release process. Although it’s a powerful tool it is also confusing to understand in many parts. This post is my attempt to shed a bit of light on the topic and share the things I learned in the process.
Lets start from defining the main concepts.
Actions They are the main building blocks of Fastlane. With them you can perform specific tasks like building the app, code signing, taking screenshots, changing icons, uploading the app to the stores etc.
Lanes
Allow you to build custom workflows like building and sending the app to the store by using actions. You define the lanes in the Fastfile
by using Ruby syntax.
Plugins Allow you to extend the Fastlane with extra functionality that allows you to perform custom tasks. For example Firebase provides a plugin that simplifies uploading your application to their platform.
Fastfile
This is a central configuration file where you define your lanes (workflows) and their actions. As mentioned above you’re using Ruby syntax when editing this file.
First you need to choose one of the recommended methods and install the Fastlane. I’m using Brew, so it will be:
brew install fastlane
For the sake of this article we’ll automate iOS build distribution to Firebase. The approach to automating Android is similar, but involves different actions for building the app.
Lets start with generating Fastfile
and Appfile
. To do that you should run:
fastlane init
Choose the “manual setup” option when prompted. You’ll end up with a folder called fastlane
containing both of those files.
Appfile
Appfile
serves as a central repository for storing configuration information that is used across all Fastlane tools and actions. For example you can store here App Identifier, Apple ID, Apple Developer Team ID etc. Here is an example of how it looks like:
# The bundle identifier of your app
app_identifier("com.almynotes.app")
# Your Apple ID or App Store Connect API Key
apple_id("[email protected]")
# App Store Connect Team ID
itc_team_id("123987")
# Developer Portal Team ID
team_id("0ALM00YNO9")
# For more information about the Appfile, see:
# https://docs.fastlane.tools/advanced/#appfile
If you belong to a single team in Apple Store you can skip defining team_id
property.
Fastfile
We’ve briefly touched on the role of Fastfile
above. Please see the example below. It doesn’t do much except outputting the message to user:
platform :ios do
desc 'Distribute to Firebase'
lane :distribute_to_firebase do
UI.message('Define the actions to start distribution...')
end
end
As you can see it defines the platform on which it operates - ios
and a single lane distribute_to_firebase
. Other than that it doesn’t do anything useful. You can run it from terminal and it will print you the message Define the actions to start distribution...
:
bundle exec fastlane distribute_to_firebase
With Fastlane we don’t need to generate provisioning profiles and signing certificates manually. App Store Connect exposes the API allowing us to automate this process. To interact with the API we first need to authenticate Fastlane.
Instead of relying on your Apple ID and password, which can be a security risk and may require two-factor authentication, we can generate an API Key and authenticate Fastlane scripts without exposing personal credentials.
You need to head over to App Store Connect and generate API Key manually. Depending on the actions you need to perform select appropriate permissions for it.
To enable our scripts to communicate with the App Store Connect API we’ll leverage app_store_connect_api_key
action. It expects an API key file (.p8
), along with other required parameters like the key_id
and issuer_id
.
platform :ios do
desc 'Distribute to Firebase'
lane :distribute_to_firebase do
app_store_connect_api_key(
key_id: 'X1ALMY49AL',
issuer_id: '1al89my9-al54-417m-y8no-1677579t8es7',
key_filepath: 'AuthKey_X1ALMY49AL.p8',
duration: 1200,
in_house: false
)
end
end
With this we can perform a wide range of actions, such as:
Whether we’re distributing our app through the App Store, TestFlight, or ad-hoc distribution, code signing is a mandatory requirement. Apple’s distribution channels rely on code signatures to verify the legitimacy of apps and ensure a secure user experience.
For the purposes of this article we’ll be using an Ad Hoc distribution certificate and an Ad Hoc provisioning profile. With this setup the app can be distributed to a limited number of registered devices for testing. That’s exactly what we need. The other options are:
Fastlane has match
action that is designed to generate and manage needed certificates. Not only that, it automatically fetches the latest certificates and profiles from the storage repository and installs them into your local keychain. As such it will make them available for Xcode to use during the build process.
In Apple world keychains are secure storage containers for certificates, keys, and passwords used in code signing and authentication processes. To isolate the certificates and keys used for automation from your personal or system keychains it’s better to create a dedicated keychain and remove it after building the app. This prevents potential conflicts or accidental modifications to your regular keychains. Fastlane allows us to create keychains with the create_keychain
action to store the generated certificates.
platform :ios do
desc 'Distribute to Firebase'
lane :distribute_to_firebase do
app_store_connect_api_key(
key_id: 'X1ALMY49AL',
issuer_id: '1al89my9-al54-417m-y8no-1677579t8es7',
key_filepath: "AuthKey_X1ALMY49AL.p8",
duration: 1200,
in_house: false
)
keychain_name = 'appsigning'
keychain_password = 'random-password'
create_keychain(
name: keychain_name,
password: keychain_password,
default_keychain: true,
unlock: true,
timeout: 3600
)
end
end
We used default_keychain
flag set to true
to make the created keychain the default for the duration of the Fastlane lane. We’ve also set unlock
to true
to automatically unlock the keychain after creation. As such our newly created keychain is ready for installing the certificated generated by match
.
Finally let’s configure match
action. It stores code signing certificates and provisioning profiles in the external storage. The available options are:
match
to access the storage. For our purposes we’ll be using Amazon S3 bucket.To configure AWS S3 access settings we’ll run fastlane match init
and in the prompt select Amazon S3 Storage
. It will generate a Matchfile
project’s fastlane
directory. Here is how mine looks like:
storage_mode("s3")
s3_region("eu-east-1")
s3_access_key("ALMYNOTES00ALM0YVYXN")
s3_secret_access_key("almynotes/Y/+0+0aLm0Y/AL0MY0wNOTeSUA0ACT")
s3_bucket("almynotes-certs")
type("adhoc")
And finally lets add match
invocation into the Fastfile
.
platform :ios do
desc 'Distribute to Firebase'
lane :distribute_to_firebase do
app_store_connect_api_key(
key_id: 'X1ALMY49AL',
issuer_id: '1al89my9-al54-417m-y8no-1677579t8es7',
key_filepath: "AuthKey_X1ALMY49AL.p8",
duration: 1200,
in_house: false
)
keychain_name = 'appsigning'
keychain_password = 'random-password'
create_keychain(
name: keychain_name,
password: keychain_password,
default_keychain: true,
unlock: true,
timeout: 3600
)
app_identifier = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier)
match(
keychain_name: keychain_name,
keychain_password: keychain_password,
app_identifier: app_identifier
)
end
end
You’ve might noticed the use of AppfileConfig.try_fetch_value
method from CredentialsManager
module. It is used to retrieve values from the Appfile
.
After executing updated script from the above with
bundle exec fastlane distribute_to_firebase
we’ll have Certificates and Provisioning Profiles generated in Apple Store and stored in our S3 bucket. match
will have stored them in our temporary keychain appsigning
we created in previous step. Besides that match will also populate a few shared values which we can access from other actions.
Among them we’re interested in MATCH_PROVISIONING_PROFILE_MAPPING
. It’s a dictionary storing mapping of app bundle identifiers to provisioning profile names. We need to take the name of newly generated certificate and put it into the .xcodeproj
file. Luckily for us Fastlane exposes an action update_code_signing_settings
designed to automate the process of modifying settings in .xcodeproj
or .workspace
files.
Let’s introduce update_code_signing_settings
action into our Fastfile
:
platform :ios do
desc 'Distribute to Firebase'
lane :distribute_to_firebase do
app_store_connect_api_key(
key_id: 'X1ALMY49AL',
issuer_id: '1al89my9-al54-417m-y8no-1677579t8es7',
key_filepath: "AuthKey_X1ALMY49AL.p8",
duration: 1200,
in_house: false
)
keychain_name = 'appsigning'
keychain_password = 'random-password'
create_keychain(
name: keychain_name,
password: keychain_password,
default_keychain: true,
unlock: true,
timeout: 3600
)
app_identifier = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier)
match(
keychain_name: keychain_name,
keychain_password: keychain_password,
app_identifier: app_identifier
)
profile_name: lane_context[SharedValues::MATCH_PROVISIONING_PROFILE_MAPPING][app_identifier]
update_code_signing_settings(
use_automatic_signing: false,
path: "myApp.xcodeproj",
profile_name: profile_name,
bundle_identifier: app_identifier
)
end
end
We’ve done all the preparation. The next important step is building the application. Luckily it’s pretty straightforward.
platform :ios do
desc 'Distribute to Firebase'
lane :distribute_to_firebase do
app_store_connect_api_key(
key_id: 'X1ALMY49AL',
issuer_id: '1al89my9-al54-417m-y8no-1677579t8es7',
key_filepath: "AuthKey_X1ALMY49AL.p8",
duration: 1200,
in_house: false
)
keychain_name = 'appsigning'
keychain_password = 'random-password'
create_keychain(
name: keychain_name,
password: keychain_password,
default_keychain: true,
unlock: true,
timeout: 3600
)
app_identifier = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier)
match(
keychain_name: keychain_name,
keychain_password: keychain_password,
app_identifier: app_identifier
)
profile_name: lane_context[SharedValues::MATCH_PROVISIONING_PROFILE_MAPPING][app_identifier]
update_code_signing_settings(
use_automatic_signing: false,
path: "myApp.xcodeproj",
profile_name: profile_name,
bundle_identifier: app_identifier
)
build_app(
workspace: "myApp.xcworkspace",
configuration: "Release",
scheme: "Dev",
clean: true,
export_method: "ad-hoc"
)
end
end
Firebase provides a service called App Distribution that allows you to easily distribute your mobile apps (iOS and Android) to trusted testers before they are released to the public app stores. There is Fastlane plugin firebase_app_distribution
that enables us to easily integrate it into our workflow.
To add it run the following command from the root of your iOS project:
fastlane add_plugin firebase_app_distribution
Then follow the official documentation to set Up Firebase Project and authenticate it. To run the action you’ll need an application ID and service account. Application ID can be found in the Fireabse Console on the settings page of you application. To create a service account follow instruction here. Once it’s done, update Fastfile
to add the invocation of this action:
platform :ios do
desc 'Distribute to Firebase'
lane :distribute_to_firebase do
app_store_connect_api_key(
key_id: 'X1ALMY49AL',
issuer_id: '1al89my9-al54-417m-y8no-1677579t8es7',
key_filepath: "AuthKey_X1ALMY49AL.p8",
duration: 1200,
in_house: false
)
keychain_name = 'appsigning'
keychain_password = 'random-password'
create_keychain(
name: keychain_name,
password: keychain_password,
default_keychain: true,
unlock: true,
timeout: 3600
)
app_identifier = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier)
match(
keychain_name: keychain_name,
keychain_password: keychain_password,
app_identifier: app_identifier
)
profile_name: lane_context[SharedValues::MATCH_PROVISIONING_PROFILE_MAPPING][app_identifier]
update_code_signing_settings(
use_automatic_signing: false,
path: "myApp.xcodeproj",
profile_name: profile_name,
bundle_identifier: app_identifier
)
build_app(
workspace: "myApp.xcworkspace",
configuration: "Release",
scheme: "Dev",
clean: true,
export_method: "ad-hoc"
)
changelog = changelog_from_git_commits(
merge_commit_filtering: 'exclude_merges',
commits_count: 1
)
firebase_app_distribution(
app: "FIREBASE_APP_ID",
service_credentials_file: "firebase-credentials.json",
release_notes: changelog,
# make sure to first create this group in Firebase
groups: "dev-team",
)
end
end
And that’s it. With those simple steps we’ve distributed the app to Firebase Distribution. Hopefully it will simplify your testing process, enabling you to get valuable feedback from testers early on.