🚀 Fastlane Match & App Store Connect API Integration

2020-12-12

I recently migrated a project to fastlane match and I was interested in fastlane support for the App Store Connect API (announced at WWDC18) - I've always found email/password auth in fastlane match a bit iffy and even more so with the push to 2FA, which can be notoriously difficult on CI/CD setups.

Turns out fastlane has done some great work to support the App Store Connect API, and it's now widely supported by the actions.

This post assumes you have some experience with fastlane. If you want to get started with fastlane, you can find out more about it here.

Getting an API Key

In order to get a App Store Connect API key, the Account Holder of the Apple Developer account/team will need to request access on App Store Connect - This seems like mostly a formality.

I've outlined the process below, which can also be forwarded to the relevant person if needed.

  1. Log in to App Store Connect
  2. Click the "Users and Access" icon
  3. Click the Keys tab at top of the page.
  4. You see the message "Permission is required to access the App Store Connect API. You can request access on behalf of your organization", along with a Request Access button.
  5. Click Request Access.
  6. Review the terms and, if acceptable, click the checkbox and then Submit.

Access seems to be granted automatically and you should be able to create a key immediately once the Account Holder has requested access. When creating an API key remember to only provide the minimum access needed - in most cases the Developer role should suffice.

Your API key will be in the form of a private key (saved as a p8 file). In order to prevent running into this issue you'll need to base64 encode the private key.

cat AuthKey_ABCDEFGH.p8 | base64

For this tutorial, I'll refer to the base64-encoded private key from an environment variable (named APP_STORE_CONNECT_API_KEY_B64). It's important to store secrets in a safe, secure place. Most CI/CD solutions provide secret management.

We'll also save as few other API key details in environment variables:

  • APP_STORE_CONNECT_KEY_ID - The Key ID is available on the API Keys page on the corresponding API key generated.
  • APP_STORE_CONNECT_ISSUER_ID - The Issuer ID is available on the API Keys page.

Setting up Fastlane Match

Fastlane Match allows you to easily sync your certificates and provisioning profiles across your team. You can find out more about it in the fastlane guide.

  1. Run fastlane match init
  2. Configure your Matchfile - storage options, app identifiers etc.
  3. Here's where things differ - to make sure that your match commands use the API key for authentication, use your Fastfile and create a lane for configuring your app.

In order to make use of our API key, we'll use a fastlane lane to generate the certificates and provisioning profiles for our app. This could likely also be done via command line arguments, but a fastlane lane provides a better format and allows for comments.

# Fastfile snippet
...
 
# A helper lane we'll use throughout the post
private_lane :match_configuration do |options|
  api_key = app_store_connect_api_key(
    key_id: ENV["APP_STORE_CONNECT_KEY_ID"],
    issuer_id: ENV["APP_STORE_CONNECT_ISSUER_ID"],
    key_content: ENV["APP_STORE_CONNECT_API_KEY_B64"],
    is_key_content_base64: true,
    in_house: false
  )
 
  sync_code_signing(
    type: options[:type],
    app_identifier: ["xyz.alihen.app"], # optional, likely already defined in your Matchfile
    api_key: api_key,
    readonly: options[:readonly],
    verbose: true
  )
end
 
lane :generate_match do
  match_configuration(
    type: "development",
    readonly: false
  )
 
  match_configuration(
    type: "appstore",
    readonly: false
  )
end
...

You should then be able run fastlane generate_match to generate certificates and provisioning profiles for development and App Store releases. These certificates and provisioning profiles will be saved to the storage option you chose in your Matchfile.



Setting up your Fastfile

Now that we've done the generatiing of the certificates and provisioning profiles, we can setup our fastfile for regular usage. A lot of the private lanes we create will be similar to the generate_match lane above and we'll re-use the match_configuration private lane.


Lanes for Installing Certificates

When a new engineer joins your team you'll probably want to provide a frictionless onboarding experience and this applies to your iOS project too. In order to do so, it's helpful to create some fastlane lanes that allow an engineer to get set up quickly.

A quick example of a lane to install all the certificates:

# Fastfile snippet
...
 
# A helper lane we'll use throughout the post
private_lane :match_configuration do |options|
  api_key = app_store_connect_api_key(
    key_id: ENV["APP_STORE_CONNECT_KEY_ID"],
    issuer_id: ENV["APP_STORE_CONNECT_ISSUER_ID"],
    key_content: ENV["APP_STORE_CONNECT_API_KEY_B64"],
    is_key_content_base64: true,
    in_house: false
  )
 
  sync_code_signing(
    type: options[:type],
    app_identifier: ["xyz.alihen.app"], # optional, likely already defined in your Matchfile
    api_key: api_key,
    readonly: options[:readonly],
    verbose: true
  )
end
 
lane :install_match_dependencies do
  match_configuration(
    type: "development",
    readonly: true
  )
 
  match_configuration(
    type: "appstore",
    readonly: true
  )
end
...

Some Notes:

Here we use the readonly: true option in order to not unintentionally update our certificates and provisioning profiles, which may require other developers to install their certificates and provisioning profiles again.

We should also generate a separate API Key for development and our CI environment and regularly rotate the keys in both environments - our evironment variables are helpful in this case.


Updating Certificates & Provisioning Profiles

There are some scenarios where you'll need to update your certificates or provisioning profiles. You should then be able to use the match_configuration private lane again with the readonly: false option. I usually opt to make this explicit, as it can have consequences for other engineers on the team.

A quick example of a lane to update certificates and provisioning profiles for a particular type:

# Fastfile snippet
...
 
# A helper lane we'll use throughout the post
private_lane :match_configuration do |options|
  api_key = app_store_connect_api_key(
    key_id: ENV["APP_STORE_CONNECT_KEY_ID"],
    issuer_id: ENV["APP_STORE_CONNECT_ISSUER_ID"],
    key_content: ENV["APP_STORE_CONNECT_API_KEY_B64"],
    is_key_content_base64: true,
    in_house: false
  )
 
  sync_code_signing(
    type: options[:type],
    app_identifier: ["xyz.alihen.app"], # optional, likely already defined in your Matchfile
    api_key: api_key,
    readonly: options[:readonly],
    verbose: true
  )
end
 
lane :update_match_development_dependencies do
  match_configuration(
    type: "development",
    readonly: false
  )
end
 
lane :update_match_appstore_dependencies do
  match_configuration(
    type: "appstore",
    readonly: false
  )
end
...

If you need to update your certificates in a specific environment, you can then run update_match_development_dependencies or update_match_appstore_dependencies - other engineers might need to run install_match_dependencies after you've done this to pull down updated certificates and provisioning profiles.


Using Match for Building

Bringing everything we know about Match & App Store Connect API keys together, we can now use Fastlane to run a typical build that would be run by a CI/CD environment.

  • Installing the certificates & provisoning profiles on the machine.
  • Building a release using gym.
  • Bonus: Uploading the the release to the App Store using the App Store Connect API key.
fastlane_version "2.150.0"
default_platform(:ios)
 
lane :production do |options|
  match_configuration(
    type: "development",
    readonly: true
  )
  match_configuration(
    type: "appstore",
    readonly: true
  )
 
  gym(
    clean: false,
    scheme: "ExampleApp",
    export_method: "app-store",
    configuration: "Release"
  )
  upload_app_to_app_store()
end
 
# A helper lane we'll use throughout the post
private_lane :match_configuration do |options|
  api_key = app_store_connect_api_key(
    key_id: ENV["APP_STORE_CONNECT_KEY_ID"],
    issuer_id: ENV["APP_STORE_CONNECT_ISSUER_ID"],
    key_content: ENV["APP_STORE_CONNECT_API_KEY_B64"],
    is_key_content_base64: true,
    in_house: false
  )
 
  sync_code_signing(
    type: options[:type],
    app_identifier: ["xyz.alihen.app"], # optional, likely already defined in your Matchfile
    api_key: api_key,
    readonly: options[:readonly],
    verbose: true
  )
end
 
# Upload to App Store using API Key
private_lane :upload_app_to_app_store do |options|
  # A small teak to improve upload speeds due to issues we encountered.
  ENV["DELIVER_ITMSTRANSPORTER_ADDITIONAL_UPLOAD_PARAMETERS"] = "-t Signiant"
 
  api_key = app_store_connect_api_key(
    key_id: ENV["APP_STORE_CONNECT_KEY_ID"],
    issuer_id: ENV["APP_STORE_CONNECT_ISSUER_ID"],
    key_content: ENV["APP_STORE_CONNECT_API_KEY_B64"],
    is_key_content_base64: true,
    in_house: false
  )
 
  deliver(
    app_identifier: "xyz.alihen.app",
    api_key: api_key,
    skip_screenshots: true,
    skip_metadata: true,
    run_precheck_before_submit: false,
  )
end


Wrapping up 🚀

This post should have provided you a (not so quick) summary of using fastlane with the App Store Connect API. Fastlane also has some great documentation on the subject:

Let me know if you run into any issues following this post on Twitter and share this post if you find it helpful.