šŸŽ How to automatize the iOS app release process with CircleCI

VerĆ³nica Valls
Game & Frontend Development Stuff
8 min readNov 29, 2022

Although the idea of releasing a new version of our app with new features and solving a bunch of bugs sounds happy, the reality is that the manual process is cumbersome and time-consuming, making this process not a happy one.

To relieve this pain, Iā€™ve automated this process with CircleCI. It hasnā€™t neither been an easy path because of the bad documentation and lack of working examples. Sometimes, I had the feeling that some parts of the docs were outdated and not maintained.

On the following article, you will find the process I followed to automatize the iOS app release process including the release notes, and links to all the documentation and examples I used.

šŸ” Define App Store Connect account settings & CircleCI environment variables

In first place, we need to gather all the related identifiers stuff to be able to connect with the App Store Connect API.

Create the Appfile

We need to create the Appfile containing the bundle identifier or app id and our Apple account email:

your_project/ios/fastlane/Appfile

app_identifier("com.bla.bla") # The bundle identifier of your app
apple_id("bla@mail.com") # Your Apple email address

Create the release settings

At the beginning of the Fastfile, weā€™ll define the app id, the provisioning profile and team id to be used on the release lane. This is very useful because if we have other lanes such as Adhoc, we can define different configuration objects to use depending on the lane:

your_project/ios/fastlane/Fastfile

APP_ID = "com.bla.bla"
PROVISIONING_PROFILE_APPSTORE = "match AppStore com.bla.bla"
TEAM_ID = "your_team_id"

settings_to_override_release = {
:BUNDLE_IDENTIFIER => APP_ID,
:PROVISIONING_PROFILE_SPECIFIER => PROVISIONING_PROFILE_APPSTORE,
:DEVELOPMENT_TEAM => TEAM_ID,
}

Create the api key and add it to CircleCI environment variables

First, we need to get the key_id, issuer_id and key_content from our App Store Connect account. We can get them from here:

Second, we should create CircleCI environment variables for these values by going to your project > āš™ļø Project Settings > Environment Variables > Add environment variable.

Finally, by using app_store_connect_api_key command on our Fastfile, we will read the api key values previously defined as environment variables, and thus, we will be able to identify ourselves on the App Store Connect API to access and send our project data.

your_project/ios/fastlane/Fastfile

desc "Export Release IPA & upload Release to App Store"
lane :release_ipa do
api_key = app_store_connect_api_key(
key_id: $APP_STORE_CONNECT_API_KEY_KEY_ID,
issuer_id: $APP_STORE_CONNECT_API_KEY_ISSUER_ID,
key_content: $APP_STORE_CONNECT_API_KEY_KEY
)
end

Doing it like this, itā€™s the recommended way to avoid the 2 authentication factor which is prompted when doing the login by using user and password.

šŸš€ Define the Fastfile Release lane

We need to create a new lane to specify the needed steps in order to fulfill successfully a new release to the App Store Connect.

Create release_ipa lane

Letā€™s go back to our Fastfile, on this file weā€™ll define all our desired lanes related with iOS processes such as exporting adhoc IPA, exporting release IPA or creating a new release and uploading the release IPA to the App store.

This article will cover creating a lane for launching a new release on the App store, besides the IPA generation and upload to the new release.

āž• Increment build number

Weā€™ll continue the development of our release_ipa lane by adding the call to increment_build_number:

your_project/ios/fastlane/Fastfile

desc "Export Release IPA & upload Release to App Store"
lane :release_ipa do
api_key = app_store_connect_api_key(
key_id: $APP_STORE_CONNECT_API_KEY_KEY_ID,
issuer_id: $APP_STORE_CONNECT_API_KEY_ISSUER_ID,
key_content: $APP_STORE_CONNECT_API_KEY_KEY
)
increment_build_number(
build_number: app_store_build_number(
api_key: api_key,
initial_build_number: 0,
version: get_version_number(xcodeproj: "your_project.xcodeproj"),
live: false
) + 1,
)
end

This piece of code will be very useful when uploading several builds for Testflight users. Automatically, it assigns the next build version taking into account if an IPA already exists on the new release.

Take into account that this call needs to include the api_key credentials, this is not well documented on the offical docs. More information on app_store_build_number.

šŸ”‘ match command

The match call is where we define the type of permissions / provisioning profiles applied to the App Store, on this case, the type keyword assigned to Release mode is appstore.

your_project/ios/fastlane/Fastfile

desc "Export Release IPA & upload Release to App Store"
lane :release_ipa do
api_key = app_store_connect_api_key(
key_id: $APP_STORE_CONNECT_API_KEY_KEY_ID,
issuer_id: $APP_STORE_CONNECT_API_KEY_ISSUER_ID,
key_content: $APP_STORE_CONNECT_API_KEY_KEY
)
increment_build_number(
build_number: app_store_build_number(
api_key: api_key,
initial_build_number: 0,
version: get_version_number(xcodeproj: "your_project.xcodeproj"),
live: false
) + 1,
)
match(
app_identifier: APP_ID,
readonly: is_ci,
type:"appstore"
)
end

šŸ— build app command

The build_app call builds the IPA with the desired configuration. On the next code snippet, you can see how to set the Release configuration:

your_project/ios/fastlane/Fastfile

desc "Export Release IPA & upload Release to App Store"
lane :release_ipa do
api_key = app_store_connect_api_key(
key_id: $APP_STORE_CONNECT_API_KEY_KEY_ID,
issuer_id: $APP_STORE_CONNECT_API_KEY_ISSUER_ID,
key_content: $APP_STORE_CONNECT_API_KEY_KEY
)
increment_build_number(
build_number: app_store_build_number(
api_key: api_key,
initial_build_number: 0,
version: get_version_number(xcodeproj: "your_project.xcodeproj"),
live: false
) + 1,
)
match(
app_identifier: APP_ID,
readonly: is_ci,
type:"appstore"
)
build_app(
scheme:"your_project_name",
export_method:"app-store",
skip_profile_detection:true,
configuration: "Release",
workspace: "your_project_name.xcworkspace",
xcargs: settings_to_override_release,
export_options: {
provisioningProfiles: {
APP_ID => PROVISIONING_PROFILE_APPSTORE
},
installerSigningCertificate: "your_installer_signing_certificate_name"
}
)
end

šŸ›µ deliver command

The deliver call creates a new release on the App Store and then uploads the IPA binary generated on the build_app step:

your_project/ios/fastlane/Fastfile

APP_ID = "com.bla.bla"
PROVISIONING_PROFILE_APPSTORE = "match AppStore com.bla.bla"
TEAM_ID = "your_team_id"

settings_to_override_release = {
:BUNDLE_IDENTIFIER => APP_ID,
:PROVISIONING_PROFILE_SPECIFIER => PROVISIONING_PROFILE_APPSTORE,
:DEVELOPMENT_TEAM => TEAM_ID,
}

default_platform(:ios)

platform :ios do

before_all do
setup_circle_ci
end

desc "Export Release IPA & upload Release to App Store"
lane :release_ipa do
api_key = app_store_connect_api_key(
key_id: $APP_STORE_CONNECT_API_KEY_KEY_ID,
issuer_id: $APP_STORE_CONNECT_API_KEY_ISSUER_ID,
key_content: $APP_STORE_CONNECT_API_KEY_KEY
)
increment_build_number(
build_number: app_store_build_number(
api_key: api_key,
initial_build_number: 0,
version: get_version_number(xcodeproj: "your_project.xcodeproj"),
live: false
) + 1,
)
match(
app_identifier: APP_ID,
readonly: is_ci,
type:"appstore"
)
build_app(
scheme:"your_project_name",
export_method:"app-store",
skip_profile_detection:true,
configuration: "Release",
workspace: "your_project_name.xcworkspace",
xcargs: settings_to_override_release,
export_options: {
provisioningProfiles: {
APP_ID => PROVISIONING_PROFILE_APPSTORE
},
installerSigningCertificate: "your_installer_signing_certificate_name"
}
)
deliver(
api_key: api_key,
submit_for_review: false,
force: true,
precheck_include_in_app_purchases: false
)
end
end

This step also needs to include the api_key credentials, it isnā€™t clearly documented on the official docs.

ā˜ļø If we set submit_for_review: true, the new release will be sent to review automatically, so we donā€™t need to click on it manually from Apple Store Connect website.

Extra tip: If you donā€™t have in-app purchases, set precheck_include_in_app_purchases: false , otherwise the workflow crashes on circleCI even though the IPA has been successfully sent to App Store.
Precheck cannot check In-app purchases with the App Store Connect API Key Ā· Issue #18250 Ā· fastlane/fastlane

Now, our Fastfile is finished šŸŽ‰

šŸ¾ Define the Release job on config.yml

Once our lane is finished, we need to create a job that calls this lane among other processes to have our flow ready.

Create the .env file

On the config.yml, weā€™ll create the ios-release job.

On this job, weā€™ll need a step to create the .env file containing the previously defined App Store Connect related keys on CircleCI environment variables:

- run:
name: "Create .env file"
command: echo -e "APP_STORE_CONNECT_API_KEY_ISSUER_ID=${APP_STORE_CONNECT_API_KEY_ISSUER_ID}\nAPP_STORE_CONNECT_API_KEY_KEY=${APP_STORE_CONNECT_API_KEY_KEY}\nAPP_STORE_CONNECT_API_KEY_KEY_ID=${APP_STORE_CONNECT_API_KEY_KEY_ID}" > .env

If you already had this step created, just add the new environment variables.

Call to the release_ipa lane

On the config.yml, weā€™ll add a new step to call our previously created release_ipa lane:

- run:
command: rm -rf Pods && pod install && bundle exec fastlane release_ipa --verbose
no_output_timeout: 30m
working_directory: ios

The config.yml release job skeleton will be something like this:

ios-release:
macos:
xcode: '13.3.0'
working_directory: ~/your-working-directory

# use a --login shell so our "set Ruby version" command gets picked up for later steps
shell: /bin/bash --login -o pipefail

steps:
- checkout

- run:
name: "Create .env file"
command: echo -e "APP_STORE_CONNECT_API_KEY_ISSUER_ID=${APP_STORE_CONNECT_API_KEY_ISSUER_ID}\nAPP_STORE_CONNECT_API_KEY_KEY=${APP_STORE_CONNECT_API_KEY_KEY}\nAPP_STORE_CONNECT_API_KEY_KEY_ID=${APP_STORE_CONNECT_API_KEY_KEY_ID}" > .env

(...)

- run:
command: rm -rf Pods && pod install && bundle exec fastlane release_ipa --verbose
no_output_timeout: 30m
working_directory: ios

- store_artifacts:
path: ios/your_project.ipa

šŸ•¹ Define the Release workflow on config.yml

Weā€™re going to define two jobs on the config.yml file:

  • request-ipa-and-prepare-release: In order to be able to decide when we want to create a release, this job will give us the control to decide on which branch we want to trigger a new iOS release.
  • ios-release: When clicking on the thumb up from request-ipa-and-prepare-release job, the iOS release process will be triggered, so a new release will be created on App Store Connect and the new generated IPA will be uploaded.

The workflows skeleton would be something like this:

workflows:
version: 2
ios:
jobs:
- request-ipa-and-prepare-release:
type: approval
filters:
<<: *filters-node
- ios-release:
requires:
- request-ipa-and-prepare-release
filters:
<<: *filters-node

And this is how it would look on circleCI:

šŸ†• Upload Release Notes automatically

Itā€™s very easy to automatize the upload of the release notes for each new iOS release, so we donā€™t need to go to the App Store Connect and fill it for each language. Here is the unique step to follow:

  • Create a folder for each one of your app supported languages on the following location:
ios/fastlane/metadata/en-US/release_notes.txt
ios/fastlane/metadata/es-ES/release_notes.txt
ios/fastlane/metadata/fr-FR/release_notes.txt

On the release_notes.txt files, add the text as you would do it: No extra steps nor extra flags needed.

This is automatically detected by the deliver command so thatā€™s why thereā€™s no need of extra configuration to be added.

Congratulations, youā€™ve made it! Now your iOS release process is automated! šŸ„³

šŸ„¹ When trying the new release process youā€™ll be aware that the IPA appears INSTANTLY on App Store Connect portal when doing it through circleCI, unlike when we did it manually from Xcode which took about 20 minutes to appear.

šŸž Bugs found during the process

CircleCI shows an error related with iTMSTransporter: An exception has occurred: issuerId is required error

[pilot] fails to upload build to TestFlight using api key after iTMSTransporter auto updated to version 3.0.0 with `An exception has occurred: issuerId is required` error Ā· Issue #20741 Ā· fastlane/fastlane

This has been solved by adding the following env variable to CircleCI: ITMSTRANSPORTER_FORCE_ITMS_PACKAGE_UPLOAD = true

And updating fastlane version to 2.210.1 on Gemfile.lock:

// on the Terminal, go to your project

cd ios
bundle update

// after these steps, commit and push the Gemfile.lock

On the next article, weā€™ll learn to how to automatize the Android app release process on Google Play Store and CircleCI šŸ¤–

šŸ“š Docs

Documentation & extra tutorials
- Fastlane official guide about Apple Store deployment
- How to build the perfect fastline pipeline for iOS
- How to use fastlane to deploy iOS app fast
- Build and deploy your iOS app to Testflight with Github Actions
- Set environment variables on CI
- Lanes
- App Store Connect API
- Authenticating with Apple Services

Examples
-
circleci-demo-ios/config.yml at master Ā· CircleCI-Public/circleci-demo-ios
- circleci-demo-ios/Fastfile at master Ā· CircleCI-Public/circleci-demo-ios
- https://webcache.googleusercontent.com/search?q=cache:DFydATxAjUEJ:https://circleci.com/docs/ios-tutorial&cd=2&hl=en&ct=clnk&gl=es

--

--

VerĆ³nica Valls
Game & Frontend Development Stuff

Mobile & frontend developer for real world projects. Game designer/developer for my mindā€™s delirium ideas. Cats & dogs dietetical and nutritional advisor.