Automate iOS builds distribution with Firebase and Fastlane

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.

How do I start?

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

Authenticating in Apple Store

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:

  • Uploading app builds
  • Managing app metadata (app name, description, screenshots, etc.)
  • Submitting apps for review
  • Releasing apps to the App Store
  • Accessing sales and financial reports
  • Managing beta testers and groups
  • And much more

Code signing

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:

  • Development - for development and testing on registered devices
  • App Store - for submitting your app to the App Store for public release
  • Enterprise - used for distributing your app in-house within your organization.

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:

  • Git repository
  • Google Cloud
  • Amazon S3. Depending on the option you choose, you’ll need to provide different settings for 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

Building the app

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

Distributing to Firebase

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.