iOS CI/CD With Azure DevOps: Automate Your Release Process

Speed up and automate your release process by setting up some pipelines

Facundo Acosta
The Tech Collective
7 min readJun 20, 2024

--

Introduction

As mobile developers, we need to carry out many different tasks when working on a project. Not only do you need to code and ensure that every pixel matches the designs, but you also need to make sure that your app is bug-free. On top of that, you need to build a release version, upload it to TestFlight and send it to Apple for review.

This process can be very tedious if you’ve never done it before, and one of the most overlooked aspects is automation. Getting this sorted properly will make everything work like a breeze, and you won’t need to worry about it again.

An image of some real pipelines
Some pipelines, but real (Source: unsplash.com)

There are many platforms to handle CI/CD, but in this case, we will be focusing on Azure DevOps. Let’s have a look!

1. Set up your Apple Developer Portal

If you haven’t already, you must register for the Apple Developer Program by paying a $99 annual fee.

Once you’ve done that, you need to create an app identifier for your app. This is usually a reverse-domain name-style string, for example, com.example.app.

To do that, head to the Apple Developer portal, click on “Identifiers,” and follow the process to create your ID. Make sure this ID is unique to your app. Select the capabilities that your app is using, and once you are done, your new identifier will be listed.

App ID creation process. Image by Facundo Acosta via miro.com

The next thing to do is to create a certificate to sign your app. To do this, head to “Certificates” and follow the process to generate it, ensuring that “Apple Distribution” is selected. Once you are done, download the generated certificate; you will need it later on.

Certificate creation process. Image by Facundo Acosta via miro.com

Another task before setting up your pipeline is to create a “Profile”. Click on “Profiles”, add a new “App Store Connect” profile, and associate it with both the app identifier you created earlier and the certificate you just generated. Download it to your machine.

Profile creation process. Image by Facundo Acosta via miro.com

Finally, create an API key. This key will enable you to upload your app from the pipeline without navigating through Apple’s login flows.

To do this, go to “Users and Access”, click on “Integrations”, and create a new “Team Key” with “Admin” permissions. Once created, make note of the key ID and issuer ID.

API key creation process. Image by Facundo Acosta via miro.com

2. Configure Azure DevOps Variables and Secure Files

Once you have the app identifier, certificate, profile, and API key, you can finally proceed to DevOps.

The first step is to securely store all the files and variables we created earlier. To do this, navigate to “Pipelines > Library” and create a new variable group. Give this group a meaningful name that can be referenced later in the pipeline, and include all necessary variables, such as the issuer ID and API key ID generated earlier.

Additionally, you will need to generate a base64-encoded string from your API key. On macOS, you can do this by running the following command:

openssl base64 -in <infile> -out <outfile>

Next, open the generated file, copy the string, and add it as a variable within the group:

Variable group creation in ADO. Image by Facundo Acosta via miro.com

The next important step is to upload the previously generated files (certificate and profile) to the “Secure files” vault.

Go to “Secure files”, click “Add”, and upload your files to DevOps. After uploading, feel free to rename them to something more meaningful if desired.

Secure file upload in ADO. Image by Facundo Acosta via miro.com

3. Configure Azure DevOps Pipeline

Create a new empty pipeline. At the very top, add the trigger that will initiate our pipeline. If you want to run it manually, add:

trigger: none

On the other hand, if you want the pipeline to be triggered when changes are merged into a specific branch, add the following:

trigger:
branches:
include:
- develop

Where “include” has a list of the branches that will trigger this pipeline.

The next step in the pipeline will be retrieving the variables you added to the variable group. To do this, add a step with the following structure:

variables:
- group: ios-variable-group
- name: api_issuer_id
value: $[variables.issuer_id]
- name: api_key_id
value: $[variables.api_key_id]
  • group: Name of the variable group you created earlier.
  • name: For each variable, specify the name to be used in the pipeline file.
  • value: For each variable, specify the name of the variable in the vault.

After loading the variables in the pipeline, we need to add some configuration for the pipeline environment, such as the virtual image, where to checkout our code, and install Node.

pool:
vmImage: 'macos-13'
name: 'Azure Pipelines'
steps:
- checkout: self
persistCredentials: true
clean: true
- task: NodeTool@0
displayName: 'Install Node.js'
inputs:
versionSpec: '20.3.1'

We also need to install the npm dependencies and install the Apple certificate and provisioning profiles we created earlier. Both certSecureFile and provisioningProfileLocation are the names you used before after uploading the files to the DevOps Library:

- script: npm install
displayName: Install dependencies
- task: InstallAppleCertificate@2
displayName: Install Apple Certificate
inputs:
certSecureFile: 'YOUR_CERTIFICATE.p12'
keychain: 'temp'
- task: InstallAppleProvisioningProfile@1
displayName: 'Install Apple Provisioning Profile'
inputs:
provisioningProfileLocation: 'secureFiles'
provProfileSecureFile: 'YOUR_PROVISIONING_PROFILE.mobileprovision'

If you are targeting multiple environments, you will need to create and upload to Secure Files an ExportOptions.plist that tells xCode how to match the app identifiers with its corresponding profile. The file should look like this:

ExportOptions file example. Image by Facundo Acosta via miro.com

After uploading the file to Azure DevOps, retrieve it in the pipeline by adding the following step:

- task: DownloadSecureFile@1
name: exportOptions
displayName: 'Download ExportOptions.plist file'
inputs:
secureFile: 'ExportOptions.plist'

The next steps before building our app will be installing the CocoaPods dependencies and bumping the version number. You can achieve this by adding the following steps:

- task: CocoaPods@0
displayName: 'Install CocoaPods dependencies'
inputs:
workingDirectory: 'ios'
forceRepoUpdate: true
projectDirectory: 'ios'
- task: ios-bundle-version@1
displayName: 'Bump version number'
inputs:
sourcePath: 'ios/YOUR_PROJECT/Info.plist'
versionCodeOption: 'buildid'
versionCode: '$(Build.BuildId)'
versionCodeOffset: '0'
versionName: '1.0.0'
printFile: true

The task ios-bundle-version@1 is an Azure Extension that needs to be added to your DevOps organisation by an Administrator. You can install it or request someone else to do it by following this link.

The final steps involve building the app with Xcode and releasing it to the App Store. This can be achieved by adding two more steps:

- task: Xcode@5
displayName: 'Build IPA'
inputs:
actions: 'build'
configuration: 'Release'
sdk: 'iphoneos'
xcWorkspacePath: 'ios/YOUR_PROJECT.xcworkspace'
scheme: 'YOUR_PROJECT_SCHEME'
packageApp: true
exportPath: 'output'
exportOptions: 'plist'
exportOptionsPlist: $(exportOptions.secureFilePath)
useXcpretty: false
signingOption: 'manual'
signingIdentity: '$(APPLE_CERTIFICATE_SIGNING_IDENTITY)'
provisioningProfileUuid: $(APPLE_PROV_PROFILE_UUID)'
provisioningProfileName: 'YOUR_PROVISIONING_PROFILE_NAME'

There are some important parameters to fill in these two steps. In the Xcode step:

  • exportOptions: Specify how Xcode will access ExportOptions, set it to “plist”.
  • exportOptionsPlist: Provide the path for the previously created and loaded file.
  • signingIdentity: This is provided by the InstallAppleCertificate step.
  • provisioningProfileUuid: This is provided by the InstallAppleProvisioningProfile step.
  • provisioningProfileName: This should match your provisioning profile name.
- task: AppStoreRelease@1
inputs:
authType: 'ApiKey'
apiKeyId: $(api_key_id)
apiKeyIssuerId: $(api_issuer_id)
apitoken: $(api_token)
releaseTrack: 'TestFlight'
appIdentifier: 'your.app.identrifier'
appType: 'iOS'
shouldSkipWaitingForProcessing: true
shouldSkipSubmission: true
appSpecificId: 'YOUR_APP_ID'

In the AppStoreRelease step:

  • apiKeyId: This should be the key ID that was added and loaded in the pipeline variables.
  • apiKeyIssuerId: This should be the issuer ID that was added and loaded in the pipeline variables.
  • apiToken: This should be the API token that was added and loaded in the pipeline variables.
  • appSpecificId: This is your app ID, which can be found in the Apple Developer Portal.

With that last step, you are ready to save your pipeline, click “Run pipeline”, and hope for the Automation Gods to make it work!

Pipeline success. Screenshot by Facundo Acosta

Conclusion

Building and uploading an iOS app to TestFlight for your testers to play around with can be a tedious and repetitive task if done manually. With the right tools and setup, you can easily automate this process so you only worry about it once, and can focus on other tasks. This guide helps you set up Azure DevOps, but other tools follow almost the same process, so don’t hesitate to give it a try!

--

--