At Soluto, we’ve created a platform that we provide to different mobile phone carriers and service providers. It’s only one codebase, but it gives each partner their own “flavor” of the app by allowing them to control the color palette, assets, and other features to their liking. The thing is, to develop, test, and deploy this kind of app, we need the ability to use multiple deployment platforms and support multiple build configurations for each “flavor” of the app. And that’s no fun at all.

So let’s say we want to test a version of the app, we need to upload it to (1) Soluto’s Testflight to release it internally, and also to (2) Fabric Beta for internal demos, and also to (3) Asurion, our parent company’s Testflight so their sales teams can try new features. And when it comes to releasing a new build, some major clients require that it be done through their iTunes Connect account so they can sign it themselves. That means that we need to provide them with the app, while they be the ones to distribute it.

 

Implementing “flavors”

In our Xcode project, we have a single Framework Target to share code and multiple App Targets that are linked to this framework, one for each partner, with different assets and configurations. Each target has its own bundle ID and is essentially a different app.

This created another challenge – we needed a way to customize signing configurations for each app separately, and use different build configurations for every output type. All the branding is done as a build phase, and the process is the same for every output.

 

Highway to Deployment Hell

At first we tried to do all of this manually, we’d decide we need to release an app to one of our partners. Then we’d archive it and run our end-to-end tests on it, which required us to sign it with a development certificate from Soluto’s developer account and make sure to set the bundle ID correctly. Then, we’d upload it to Fabric Beta so that Product Managers can run last-minute sanity checks and demo new features. Then we would upload it to TestFlight and expose it to sale teams and Asurion employees to demo and play with. And then, if it all went well, we’d archive it with wildcard app ID and send it to our partner. Like I said – no fun.

We tried to work like this for about a month… then we realized it’s way too complicated and error-prone to continue this way. What we really needed was to automate this entire process.

Of course, our biggest challenge was signing the app correctly for each deployment platform and build target. For Soluto’s TestFlight we needed a release profile from Soluto’s developer account. For each client we needed the app signed with a wildcard profile. And for Fabric Beta, the app needed to be signed with an ad-hoc or development certificate… that’s a lot of configuration!

 

Taking the Fastlane to Salvation

Our solution was to use Fastlane lanes to build the app with different configurations and to deploy it to the different deployment platforms we need to support (i.e. TestFlight, Fabric and iTunes Connect).

If you’re not familiar with Fastlane, let me fill you in: Fastlane is a tool for automating CI and CD tasks. We use it to deploy apps to Fabric, TestFlight, and to submit to the App Store. We also use it to run our unit tests, gather coverage data, create new app flavors and create archives for client delivery (send an archive to a client so they can submit it to the store).

Our lanes:

:test – Boring configuration, but it makes us happy. We use Scan to run tests and Slather to get coverage information.

:crashlytics – We use this lane to deploy apps from development branches to Fabric Beta (It used to be called Crashlytics and we never bothered to change the name of the lane). This allows us to demo new features on every test device we got. We sign the app with a development profile.

:testflight – This is the first of 2 release lanes. We don’t use Deliver to actually deliver. We upload the IPA to TestFlight, using Pilot, and release it manually when we’re ready.

:xcarchive – This is the second release lane. We build the app, sign it with a wildcard app ID and send the generated archive to the client so that they can sign it again with their production certificate and upload it.

We don’t need more than 4 lanes because we use Fastlane’s dotenv support. We got a .env file for every configuration we need, e.g. for ExampleCarrier that needs to release to Fabric, Soluto TestFlight, Asurion TestFlight, and archive we’ll have:

.env.ExampleCarrier_soluto – Used for Fabric, Soluto’s TestFlight, and archive.

.env.ExampleCarrier_asurion – Used for Asurion’s TestFlight.

We use the .env files to define the following environment variables:

  • APP_IDENTIFIER
  • SCHEME
  • TEAM_ID
  • APP_NAME
  • APPLE_ID
  • TARGET
  • ARCHIVE_PATH
  • PROVISIONING_PROFILE
  • WORKSPACE_NAME
  • PATH_TO_PLIST
  • PROJECT_NAME

Some of these are optional and have fallback values.

All of these values are affected by the output type we want. For example, to upload to Soluto’s TestFlight we need to use a different app ID than the one we would use for archive or Asurion’s TestFlight.

We’ll focus only on the TestFlight lane because it does a good job of illustrating most of our overall process and the decisions behind it.

 

TestFlight lane:

First thing, if the app doesn’t exist, create it:

 produce(
app_identifier: ENV["APP_IDENTIFIER"],
app_name:       ENV["APP_NAME"],
team_id:        ENV["TEAM_ID"],
app_version:    "1.0.0"
)

This allows us to create a new client app by selecting the assets and creating a .env file – that’s it!

 

Code signing assets and provisioning

The major advantage of using Fastlane is that it handles the entire code signing headache.

We use Cert to create a private/public key if needed, PEM to create APNs certificates, and Sigh to create provisioning profiles.

 cert(
team_id:ENV["TEAM_ID"],
output_path:"../products/certificates/identities/production"
)
FileUtils::mkdir_p '../products/certificates/APNS/production'
FileUtils::mkdir_p '../products/certificates/APNS/development'
# production APNS certificate
pem(
app_identifier: ENV["APP_IDENTIFIER"],
team_id:ENV["TEAM_ID"],
output_path:"products/certificates/APNS/production"
)
# development APNS certificate
pem(
app_identifier: ENV["APP_IDENTIFIER"],
team_id:ENV["TEAM_ID"],
output_path:"products/certificates/APNS/development",
development:true
)
#need to manually create folders because sigh cant handle the truth (non existing folders)
FileUtils::mkdir_p '../products/provisioning_profiles'
sigh(output_path:"products/provisioning_profiles")

We save all of these assets and expose them as build artifacts. In the future we plan on automatically uploading APNs certificates to our push notifications service.

 

Configuring the project and building the app

We use update_project_provisioning to set the correct provisioning profile:

 update_project_provisioning(
xcodeproj: ENV["PROJECT_NAME"],
profile: ENV["SIGH_PROFILE_PATH"],
target_filter:ENV["TARGET"],
build_configuration:"Release"
)

This is actually not one of the recommended ways to set a specific profile to an app according to Fastlane’s code signing guide.

At the time of writing the script, the recommended way to set the provisioning profile in the project was to manually edit the pbxproj file and set it to an environment variable.

We didn’t like editing the pbxproj file manually… We didn’t know what structural changes we might have to deal with if we change it manually.

Today, Fastlane offers Match, a tool to manage your code signing assets on a git server. We didn’t have a chance to do it yet, but we definitely want to try it out.

Now we need to set the bundle ID to the one that matches the desired output:

 update_app_identifier(
plist_path:ENV["PATH_TO_PLIST"],
app_identifier: ENV["APP_IDENTIFIER"]
)

To build the app we use Gym.

 gym(
workspace: ENV["WORKSPACE_NAME"] + ".xcworkspace",
configuration: "Release",
scheme: ENV["SCHEME"],
silent:false,
clean:true,
output_directory: "products/IPA",
export_team_id:ENV["TEAM_ID"],
output_name: ENV["WORKSPACE_NAME"] + ".ipa"
)

Again, we save the output folder and expose it as an artifact. We don’t upload it to our artifact repository (we use Sonatype Nexus) or save it in any other meaningful way because the IPA file itself is pretty useless once it’s available through TestFlight. It’s also the easiest way to install it since it’s signed for distribution through the App Store. The only reason we might use it after the build is finished, is if the upload fails and we need to do this step manually but don’t want to build again.

 

Uploading

The last step is to upload the app to TestFlight. We use Pilot:

 pilot(
ipa: "./artifacts/products/"+ENV["WORKSPACE_NAME"]+".ipa",
skip_submission:true
)

Currently we skip submission because it’s still manageable to do it manually, but as we get more clients we will probably automate it as well.

 

If you found this useful…

The good news are, that you don’t need to start from scratch!

Have a look at our Fastlane scripts GitHub! Try taking the script as it’s written, make your own .env file and upload your app to TestFlight.