Bare Bones CI/CD for iOS with Azure Devops & App Center

As an iOS developer in an agency, I have had the opportunity to work on a wide variety of projects with a huge variation of team setups.

Some clients would have a dedicated Continuous Integration (CI) and Continuous Delivery (CD) teams and pipelines, others may have an ageing set of scripts they wished to maintain/tweak, or in other cases a brand new setup is required. Depending on the stack and the project requirements, this can end up taking up a lot of time at the start of a project, which is sometimes not known or accounted before you land on a project.

Before we get too far into detail, let's back up and talk a little about what CI/CD is and where it can help you.

Continuous Integration / Continuous Delivery

CI/CD is a strategy / process that aimed to help you speed up testing, building and delivering your apps. Depending on your config, CI/CD allows for the automated running of tests, build, deploy and distribution of your app, to your testers (or app store). The beauty of this is that upon pushing a build, the manual aspect of a developer being ensuring the tests are passing, having the right certs on their local machine & then locking up their Xcode while archiving a project and uploading to a distribution platform (Such as App center). The above tasks often fall on the lap of one person and invariably causes mass panic when that person takes holiday or leaves. So a centralised automated process helps the whole team out with both quality, delivery and makes tests automation visible to the whole team.

Over the last few years I have worked with Fastlane, Jenkins, Bamboo, CircleCI, BuddyBuild (RIP — but very excited to see a resurrection in Xcode Cloud coming soon🔥). However I recently started working on a project that had no working devops project, or team to support it. The client was all in on Azure devops, which is not something I am was familiar with.

Azure Devops

Azure has a host of services we would be using, as well as ticket management (ala Jira), it would be our code repo, create our build, run our tests and deploy to App Center for distribution. All of this would be done via a Pipeline in azure.

Our process only needed to be simple, we were a small team of iOS developers that were producing daily builds for our QA team. The team was familiar with running unit tests before submitting PR’s, but we wanted to add a step in the pipeline to ensure tests were passing. We also wanted the build to go to App Center and the QA team to get a notification a build was ready, with release notes attached.

Virtual machine

Along with these steps, there were additional processes required to build the app in Pipelines. As you are running your code on a Virtual Machine (VM) you would need to install the distribution certificate, provisioning profile, Archive Export Options Plist, and download any dependancies(Cocoa pods in this instance) .

So our process on the VM needs to essentially to run along the lines of

  • Install the Apple distro Cert
  • Install the provisioning profile
  • Download the Xcode Export options Plist
  • Run CocoaPods install
  • Build project
  • Run tests
  • Create archive
  • Export Archive using Export Options Plist to an .ipa
  • Distribute to App Center attaching release notes


So with the above steps in mind, the first steps were to gather the files we would need to upload to our Pipeline.

Distro cert — assuming you have builds exporting from your current Mac, you can grab this from Keychain. If not you would need to get this file from the Apple developer portal.

Keychain export of distribution certificates

Note when exporting this make sure you expand the cert and select both items before exporting (make a note of the password you use here too as you will need to use this when setting up your build script)

Provisioning profile — The Dev tools team at Apple have made this super easy to obtain from the IDE. Simply select your project in Xcode and from the “Signing and capabilities” tab and then from your release section, select the information disclosure button (‘i’) and a pop-out menu will show details of the profile. Drag the “Prov” icon (represented by cog) into a finder window to save this for upload to your Pipeline.

Exporting provision profile from Xcode

Lastly you will need your ExportOptions.plist. The easiest way to get this is to do a local archive / export then from the folder containing your exported .IPA file, grab the ExportOptions.Plist

Library / Variables

We are now ready to hop over to devops and begin uploading the aforementioned files. To do this you will need to select Library from the pipelines menu of devops.

Dev ops pipelines and libraries

From here we will work with 2 areas, Secure files and Variables

Variables and Secure files

Selecting Secure files, uploading you Provisioning profile, Distro Cert and Export options Plist — Ensuring you have made a note of the file names.

Next in the Variables section, add a new variable and add the password you used when exporting your distribution certificate. Make a note of the name that you assign to this as you will need to later.

Build Script

With all the file uploads and variables created, we can now get down to the business of creating our build script.

The build script itself is written in Yaml, which is nice and simple in terms of syntax.

Azure gives you the option to edit the yaml file in an online editor complete with ‘task’ library. The task library has a searchable list of components that take care of the main part of the scripts and just accept variable values as form entries.

However, I found this to not work for my project setup and experienced a lot of time wasted with various Xcode tasks that would just fail. I will note that the documentation and help text for these appears way better than when I was initially setting up my pipeline, but your milage may vary.

With this in mind, for the remainder of the this article we will use the scripting window rather than the tasks components.

Important note, medium is pretty bad for code formatting, its not immediately clear, that there are spaces in some of the formatting below, so I will include both an image and some text you can copy

Setting a VM

First step is to select your build machine, in this case a Virtual Mac — so our script will look like :

setting a virtual machine

vmImage: ‘macos-latest’


Next up are our steps, which is the series of tasks we wish to execute in order, the first being running Cocoa pods

Running cocoa pods install


- task: CocoaPods@0


forceRepoUpdate: false

Download resources

Now we need to install our resources onto the VM, this will be the Distro cert, the Provisioning profile and the Export Options plist

Distro Cert

At this point you will need to remember the file name and the variable name you created for your password

Installing the Distro certificate

- task: InstallAppleCertificate@2


certSecureFile: ‘DistroCertName .p12’

certPwd: ‘$(DistroCertPassword)’

keychain: ‘temp’

Provisioning profile

Now the same for your provisioning profile..

Install the provisioning profile

- task: InstallAppleProvisioningProfile@1


provisioningProfileLocation: ‘secureFiles’

provProfileSecureFile: ‘Distribution2021.mobileprovision’

Export Options Plist

Our final download step is the Export Options Plist

Install the Export options plist

- task: DownloadSecureFile@1

name: plist

displayName: ‘Download plist’


secureFile: ‘ExportOptions.plist’

Build commands

With all our resources on the build machine, we are now good to get our build steps added. As I mentioned I had no luck with the out of the box ‘tasks’ so I utilised command line script tasks, these are the same scripts you could run from your project directory (note thats a good place to test these before attempting to run them in devops as your local machine wont need to download dependancies each time, so will fail faster if there is an issue)

For these steps I included a workspace and scheme, if your project isn’t configured in this way, you will need to amend these to use a `.xcodeproj` etc


Our first step is to run our unit tests.

run unit tests

- task: CmdLine@2


script: ‘xcodebuild test -workspace yourworkspace.xcworkspace -scheme yourScheme -destination “platform=iOS Simulator,name=iPhone 12 mini,OS=14.5”’


Followed by our building our app — in this instance I am using our release configuration, which is set up in Xcode in our environment settings (you will need to configure this as per the step you use in your manual archive process).


- task: CmdLine@2


script: ‘xcodebuild archive -workspace yourworkspace.xcworkspace -scheme yourScheme -configuration Release clean’


Next we add our export path which will be used to create and distribute our IPA file shortly


- task: CmdLine@2


script: ‘xcodebuild -workspace yourworkspace.xcworkspace -scheme yourScheme -archivePath $(Build.BinariesDirectory).archive.xcarchive archive’

Create IPA

Our final build step is to create a .IPA file from the archive using our ExportOptions.plist

Create IPA

- task: CmdLine@2


script: ‘xcodebuild -exportArchive -archivePath $(Build.BinariesDirectory).archive.xcarchive -exportPath $(Build.BinariesDirectory)/exported -exportOptionsPlist $(plist.secureFilePath)’


Phew, with those steps out of the way, our final steps are going to be to upload and distribute our newly built IPA file to App center. Before we do this, you will need to grab a few details from App center.

You are going to need the following bits of info:

  • Server Endpoint — This requires you to creat a new App Center service connectio in the azure DevOps project settings. Once granted persmission add the end point to the script.
  • Release Notes File -The file name of your release notes — this is a markdown file you create in your root project directory e.g
  • App Slug — the URL of your distro group e.g my-org/Mobile-App-IOS-UAT
  • Distribution Group Id — this is the group ID of your testers in App Center, you can find this out from the setting in the distro group in App Center. You can add multiple group id’s just use a comma to separate these .
Distribute to App Center

- task: AppCenterDistribute@3


serverEndpoint: ‘MyOrg App Center’

releaseNotesOption: ‘file’

releaseNotesFile: ‘’

destinationType: ‘groups’

appFile: ‘$(Build.BinariesDirectory)/exported/appname.ipa’

appSlug: ‘my-org/Mobile-App-IOS-UAT’

distributionGroupId: ‘gsdfgr-8676–4e80-b839–23k178642744,d9996af-3ada6–4j038-afgh2db-8aa9fhjfhgjdd4c50aa’

This is the final step for your script. Saving this will add a commit to your repo and you are ready to roll.

I personally found the documentation a little challenging for this process, with many of the above property names miss-matching in some of the Azure documentation. When the tasks failed it was often tricky to track down the issue, if it was a project setting or it was a task config error miss-match, so a lot of the above was trial and error.

With my project being on the large size, finding errors on the script could take up to 40 minutes, so my advice would be to run the as many of the CmdLine@2 phases on your local machine to validate the project / workspace names and schemes etc.

Once this pipeline was up and running, it has been rock solid and a welcome addition to our workflow. The devops setup process can be painful and time consuming, so I am hoping this helps someone by saving some time figuring this process out and less “xcodebuild Exit Code 65” errors 😆.

Mobile Lead at Publicis Sapient