Deploying beta versions of your app to TestFlight and AppCenter using Xcode Cloud

Sponsored

Zenitizer logo
Zenitizer - Meditation Timer

Enjoy clutter-free meditation with Zenitizer's clean UI and soothing sounds. For Apple geeks: Shortcuts, Apple Health, widgets and fully-featured watchOS and visionOS apps! Get 25% OFF with code: POL24

In preparation for my talk at NYSwifty next week, I have been writing about all things Xcode Cloud on my blog. This has helped me prepare for my talk and look at Xcode Cloud in depth.

One of the workflows that I will be showing how to build during my talk deploys beta versions of an iOS app for internal testing and distributes them both to TestFlight and Microsoft’s AppCenter.

I work at a company where we use AppCenter to distribute all our internal builds to other developers, testers and stakeholders. For this reason, I was keen to find a way to upload builds to an external service using Xcode Cloud by making use of existing Fastlane logic. 🎉

Creating a workflow

The first step is to create a new workflow in Xcode Cloud. To do this, open Xcode and show the report navigator in the sidebar. On the cloud tab, select your Xcode Cloud product, click on Manage Workflows and press the + button on the bottom left of the Manage Workflows screen.

You will now be prompted with a modal containing general information on the workflow. Modify the name and description and select Restrict editing if you want the workflow to only be editable by users with Admin or App Manager roles in App Store Connect:

The general configuration of the workflow with a name, description and the restrict editing option checked.

Setting up the environment

Next, select the Environment tab on the left-hand side. Pick a macOS and an Xcode version for this workflow and tick the Clean box if you want the workflow to ignore the derived data and caches from previous jobs.

To upload to AppCenter, you will need an API key. Once you have generated this from AppCenter’s dashboard, make sure you add it to the Environment Variables section of the workflow so you can retrieve it later on. Tick the secret box to ensure Xcode Cloud stores the variable securely and does not display it in the logs.

The environment configuration of the workflow which uses the latest release of both macOS and Xcode and has AppCenter's API key as a secret.

Setting up start conditions

The next step is to set up the start conditions for the workflow. This workflow archives and distributes the application on every push to the main branch, but you can change this to run on PR changes, on tag pushes or even on a schedule.

The start conditions for the new workflow. The workflow will run on every push to the main branch. The option to auto cancel builds is also selected.

I suggest you select the Auto-cancels Builds option to ensure that any ongoing builds of this workflow are cancelled when a new commit is pushed to the main branch. This ensures that you only keep the builds you are interested in and don’t waste resources on builds that are no longer relevant.

Archiving the application

Now that you have set up the environment, you need to add an action to the workflow. Click on the + button next to the Actions section title on the left-hand side and select the Archive action from the list.

In the action’s settings, select the scheme you want to archive the app for and set the Deployment Preparation to Internal Testing Only:

The configuration for the archive step

Uploading to TestFlight

The final step of the workflow is to upload the build to TestFlight. To achieve this, click on the + button next to the Post-Actions section title and select TestFlight Internal Testing from the list.

In the post-action configuration, select the archive from the previous step as the artefact and a distribution group of internal beta testers to upload the build to.

The configuration for the post archive step which uploads the build to TestFlight for internal testing and selects a testing group

Uploading to AppCenter

So far, the workflow only archives the application and distributes the generated artefact to TestFlight. As it stands, there is no way to add a custom post-action to the workflow from Xcode’s user interface.

Despite this, Xcode Cloud provides a way of running custom actions at three different stages of a workflow’s lifecycle (after the repository is cloned and before and after the xcodebuild command runs) by creating scripts with specific names in a directory called ci_scripts. If you want to learn more about this, you can check out my blog post on the topic.

Installing Fastlane

The easiest way to install Fastlane is using Homebrew, but the way Fastlane recommends installing it is by using ruby’s Bundler. I have personally also found this way to be faster and more reliable.

To install Fastlane using ruby’s Bundler, you need to create a Gemfile at the root of the repository and add Fastlane as a dependency:

Gemfile
source "https://rubygems.org"

gem "fastlane"

plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)

As Apple recommends in the Xcode Cloud documentation, the best time to install third-party dependencies is directly after the repository is cloned.

Create a file called ci_post_clone.sh under the ci_scripts directory with the following content:

ci_post_clone.sh
#!/bin/sh

# 📁 Set the install path to a local directory
bundle config set --local path 'vendor'

# ⬇️ Install all dependencies in the `Gemfile`
bundle install

Creating a lane

Now that Fastlane is installed, you need to create a lane that uploads the build to AppCenter. In your project’s Fastfile, add the following code:

Fastfile
lane :upload_to_appcenter do
    # 1
    archive_path_dir = ENV["CI_ARCHIVE_PATH"]
    # 2
    pkg_file = File.join(archive_path_dir, "Products", "Applications", "QReate.app")
    dsym_file = File.join(archive_path_dir, "dSYMs", "QReate.app.dSYM")

    # 3
    appcenter_upload(
        api_token: ENV["APP_CENTER_API_KEY"],
        owner_name: "polpielladev",
        app_name: "QReate",
        release_notes: "Bug fixing and new features",
        file: pkg_file,
        dsym: dsym_file,
        notify_testers: false
    )
end

In a nutshell, the lane above does the following:

  1. Retrieve the path to the archive generated by Xcode Cloud using the CI_ARCHIVE_PATH environment variable. This environment variable is set by Xcode Cloud automatically on every build with an archive step.
  2. Retrieve the paths to the binary file and the dSYM file from the archive.
  3. Call Microsoft’s open source appcenter_upload fastlane action to upload the build to AppCenter. The action takes several arguments but I want to focus only on api_token, which gets its value from the APP_CENTER_API_KEY environment variable that you set up in the workflow’s environment earlier on.

Running Fastlane from Xcode Cloud

All that’s left for you to do now is to add a second CI script that runs after the archive step and calls the new lane.

To do so, create a file called ci_post_xcodebuild.sh under the ci_scripts directory with the following content:

ci_post_xcodebuild.sh
# 🆙 Go one directory up to the root of the repository
cd ..

# 🏃‍♂️ Run the new lane using the Fastlane binary installed by Bundler
bundle exec fastlane upload_to_appcenter

That’s it! Next time you push to main, Xcode Cloud will generate an archive for your application and it will distribute it to TestFlight and AppCenter 🎉.