Continuous delivery for iOS apps

Helder Pinhal
Helder Pinhal
Mar 4 2022
Posted in Engineering & Technology

Automate the distribution of your iOS apps.

Continuous delivery for iOS apps

In a previous article, we covered building and distributing an Android through GitHub Actions, touching on the fundamental aspects of GitHub Actions itself.

In this article, we'll skip the introduction to GitHub Actions and go straight into the technical bits to build your iOS app.

Life in the fast lane

For those who are unfamiliar with the tool, Fastlane helps us automate the build and release process. We can skip the tool and manually take care of every aspect involved in building an iOS app, but why should we reinvent the wheel? Fastlane has been around for a pretty long time and is well maintained.

Recapping on the build requirements

To build an iOS app there are a couple of things we need to take care of before anything else.

The distribution certificate

In most cases, Xcode is the preferred way of requesting and installing certificates. However, since our workflow can run on any number of machines, we don't want to request a new certificate every time it runs. We'll find out how Fastlane can help with that.

The provisioning profile

Quoting from the Apple documentation, a provisioning profile is a type of system profile used to launch one or more apps on devices and use certain services.

That being said, in this article, we are aiming to distribute an app through Firebase App Distribution therefore it requires a distribution type of profile.

There are two ways to achieve this. With an Enterprise plan and certificate, we can distribute an app outside of the AppStore. However, that requires a different kind of Apple Developer Membership which has its eligibility criteria. Alternatively, the most common approach is to use an AdHoc kind of provisioning profile tied with a distribution certificate. This kind of profile enumerates the devices allowed to run the application so bear that in mind when you register a new device in the Developer portal.

Just like the distribution certificate, we want to preserve the profile across workflow executions and Fastlane also helps with that task.

Into Fastlane

Without going into the fundamental aspects of Fastlane and its inner workings, we will set up the Fastfile to create or use the existing distribution certificate and provisioning profile, and to build the IPA.

Setting up code signing

Before leaping into trying to build the app, we have to ensure the process has access to the requirements described above. Fastlane takes care of the heavy lifting by communicating with the AppStore Connect API and creating the certificate and profile. However, it needs to store the created resources somewhere. The standard approach is to use a git repo, but there are other storage options.

You should create a certificates repo, set up a deploy key that has read/write access and store the base64 encoded private key as a GitHub Actions secret so Fastlane can tap into that.

Additionally, to access the AppStore Connect API, we need to generate an API key and store the base64 version of it as a GitHub Actions secret as well. For information on creating the API key, refer to Apple's documentation.

Finally, we can create the necessary lanes to load the API key and prepare the code signing resources. Below we assume the key file exists in the current directory. That will be ensured by the workflow later on.

desc 'Load ASC API Key information to use in subsequent lanes.'
private_lane :load_asc_api_key do
  app_store_connect_api_key(
    key_id: 'my-key-id',
    issuer_id: 'my-issuer-id',
    key_filepath: 'app-store-connect-auth.p8'
  )
end

desc 'Prepare code signing.'
private_lane :prepare_code_signing do |options|
  match(
    type: options[:type],
    force_for_new_devices: true,
    git_branch: 'main',
    git_url: 'git@github.com:my-org/certificates.git',
    git_private_key: 'certificates-deploy-key.ssh'
  )
end

Setting up a custom keychain

Fastlane stores the certificate in the default keychain and since we cannot be certain of how the default keychain is configured, we find it best to create a custom keychain and let it become the default keychain on that runner.

desc 'Ensure the custom keychain exists.'
private_lane :ensure_keychain do
  ENV['MATCH_KEYCHAIN_NAME'] = 'my-custom-keychain.keychain'

  delete_keychain(
    name: ENV['MATCH_KEYCHAIN_NAME'],
  ) if File.exist? File.expand_path("~/Library/Keychains/#{ENV['MATCH_KEYCHAIN_NAME']}-db")

  create_keychain(
    name: ENV['MATCH_KEYCHAIN_NAME'],
    password: ENV['MATCH_KEYCHAIN_PASSWORD'],
    unlock: true,
    timeout: 900,
    default_keychain: true
  )
end

Disabling automatic code signing

It is quite common to let Xcode automatically manage code signing on our behalf, but since we are automating this process, we should update the project with the new code signing resources.

desc 'Disable automatic code signing to ensure the appropriate provisioning profiles are used.'
private_lane :disable_automatic_code_signing do |options|
  app_identifier = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier)
  profile_mapping = lane_context[SharedValues::MATCH_PROVISIONING_PROFILE_MAPPING]

  # turn off automatic signing during build so correct code signing identity is guaranteed to be used
  update_code_signing_settings(
    use_automatic_signing: false,
    path: options[:project_path],
    code_sign_identity: 'Apple Distribution',
    profile_name: profile_mapping[app_identifier],
    build_configurations: [options[:build_configuration]]
  )
end

Building the app 🚀

At this point, the last individual step we're missing is to build the IPA. Provided everything has been set up beforehand, it's as easy as running an xcodebuild command, but Fastlane encapsulates this with the build_app action.

desc 'Build the application.'
private_lane :build do |options|
  build_app(
    workspace: 'MyWorkspace.xcworkspace',
    scheme: options[:scheme],
    configuration: options[:build_configuration],
    export_options: {
      compileBitcode: true
    },
    output_name: options[:output_name]
  )
end

Glueing everything together

Lastly, we need to declare a public lane responsible for taking care of the whole process.

lane :build_my_app do |options|
  xcodeproj_path = 'MyApp.xcodeproj'
  export_method = 'adhoc'
  scheme = 'MyApp'
  configuration = 'Release'

  ensure_keychain
  load_asc_api_key
  prepare_code_signing(type: export_method)
  disable_automatic_code_signing(project_path: xcodeproj_path, build_configuration: configuration)
  build(scheme: scheme, build_configuration: configuration, output_name: options[:ipa_name])
end

Enter the GitHub Actions workflow

Without going into much detail on the structure of the workflow itself, compared to the Android workflow, there are some additional preparations — decoding the private keys into files. Naturally, instead of using Gradle to build the app, we have to update that to use Fastlane instead.

jobs:
  build:
    name: Build
    runs-on: macos-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.1'
          bundler-cache: true
      - name: Prepare App Store Connect API Key
        run: echo '${{ secrets.app_store_connect_auth_base64 }}' | base64 --decode > app-store-connect-auth.p8
      - name: Prepare certificates repo ssh key
        run: |
          echo '${{ secrets.certificates_repo_ssh_key_base64 }}' | base64 --decode > certificates-deploy-key.ssh
          chmod 400 certificates-deploy-key.ssh
      - name: Build
        run: |
          bundle install
          bundle exec fastlane build_my_app ipa_name:'my_app'
        env:
          MATCH_PASSWORD: '${{ secrets.fastlane_match_password }}'
          MATCH_KEYCHAIN_PASSWORD: '${{ secrets.fastlane_match_keychain_password }}'
      - name: Upload binaries
        uses: actions/upload-artifact@v2
        with:
          name: app-release
          path: |
            my_app.ipa
            my_app.app.dSYM.zip

One small yet very important detail about the workflow above is related to the secrets being used there. Some of them have been hinted at when setting up code signing but you may notice others like the keychain password. While that one is pretty obvious, the match_password is not. When Fastlane stores the certificates and profiles in the git repo, it also encrypts them, hence providing a passphrase for it. Be aware that losing this passphrase means invalidating existing certificates and creating new ones.

Distributing to Firebase

To distribute the IPA to Firebase, all that we have to adjust in the Android step is the artefact name. If you're looking for detailed instructions, step into the previous article.

Conclusion

Phew, that was quite the task to fit all the pieces together. However, once you have that structure in place, it becomes far easier and less time consuming to distribute your builds. With a few tweaks, you can also automate the AppStore distribution.

As always, we hope you liked this article and if you have anything to add, we are available via our Support Channel.

Keep up-to-date with the latest news