CI pipeline for a Swift application using Azure DevOps

Nowadays it is hard even to think on having to work on a project without having continuous integration (CI) and continuous deployment (CD) pipelines as part of your development flow. There are simple and super powerful tools for having a CI on any project without too much effort. One of these tools is Azure DevOps’ Pipelines.

When we started working on our latest Swift application, we decided to focus first on having our CI pipeline in place using Azure DevOps’ Pipelines. As we have not found a lot of information about this, we want to contribute sharing this step-by-step guide on how to run an Azure Pipeline for an out of store macOS project created with Swift.

Before starting, let me mention the team that work on this guide, contributing as well as giving feedback: Sergio Amoruso , Mateo Cannata, Sebastian Casaretto, Mauro Krikorian and Ricardo Segretin.

Introduction to Azure Pipelines

Before we start, let’s do a quick introduction to Azure Pipelines, which is one of the components of the Azure DevOps platform.

Azure Pipelines logo

Azure Pipelines is a cloud service that you can use to automatically build and test your code project and make it available to other users.

It combines continuous integration (CI) and continuous delivery (CD) to test and build your code as well as ship it to any target constantly and consistently.

You can use many languages with Azure Pipelines, such as Python, Java, JavaScript, PHP, Ruby, C#, C++, and Go. Additionally, there are a lot of tasks already created and you can even extend it with your custom tasks.

Continuous Integration

The idea of this practice is to automate part of your development flow, especially the tests validations and builds for your project. It usually runs other tasks like linters and other kind of validations of your solution quality. By having this extra hand, it helps you to catch issues early in the development cycle, when it is easier and faster to fix them.

Items known as artifacts are produced from CI systems which are usually used later by a continuous delivery release pipelines to drive automatic deployments.

As a summary, some CI’s advantages are:

  • Continually running your tests to ensure that the solution is working as expected.
  • Improving your code coverage by running the tests and revealing when you add new code without the corresponding tests.
  • Making a faster development flow by splitting testing and building runs and avoiding some of the revision tasks.
  • Automatically ensure you do not ship broken code.
  • Helping you to improve the quality of your code.

Continuous Delivery

The other practice that most of the times goes together with CI is Continuous delivery which automatically deploys and tests code in multiple stages to help drive quality.

While continuous integration systems produce deployable artifacts, which includes infrastructure and apps, automated release pipelines consume these artifacts to release new versions and fixes to the target of your choice.

Some of CD’s advantages are:

  • Automatically deploy code to production avoiding errors in the process.
  • Ensures deployment targets have the latest code.
  • Use tested code from CI process.

For more information on Azure Pipelines, please refer to the official Microsoft documentation here.

Creating the macOS pipeline

Alright so without further ado, let’s begin working on the Azure pipeline for a Swift project. Keep in mind this is for an application distributed OUTSIDE the App Store.

First, we need to setup the triggers for our pipeline. A trigger is something that is set up to tell the pipeline when to run. You can configure a pipeline to run upon a push to a repository, at scheduled times, or upon the completion of another build. All these actions are known as triggers.

In the following azure-pipelines.yaml file, our pipeline will run upon a push to the develop or master branches and includes pull requests to the develop branch.

Something important when working with macOS projects is that we need a macOS where we can run the corresponding tools. In YAML pipelines, if you do not specify a pool, pipelines will default to the Azure Pipelines agent pool. You simply need to specify which virtual machine image you want to use, in this case, we are using macos-latest (you can see the installed software here).

Next, we need to create some variables that we will use later in the pipeline.


A step is the smallest building block of a pipeline. For example, a pipeline might consist of build and test steps. A step can either be a script or a task. A task is simply a pre-created script offered as a convenience to you. To view the available tasks, see the Build and release tasks reference.

Step #1: Install Application Certificates

We must install the application certificates to the remote macOS virtual machine’s keychain to do the application signing. To do this, we use the InstallAppleCertificate task that makes this super easy.

This task opens the certificate file and installs it on your keychain using a certificate password.

Secret Variables

To keep your certificate and password secure, you should use secret variables and also upload the file for your pipelines to use them when needed. To create a new secret variable, you need to follow these steps:

  1. Go to the Pipelines page, select the appropriate pipeline, and then select Edit.
  2. Locate the Variables for this pipeline.
  3. Add or update the variable.
  4. Select the secret option and save.
  5. Then, to use this variable, it must be done in this format certPwd: '${secretVariable}'

Secure Files

Now, to upload files, you need to do the following:

  1. In Azure Pipelines, select the Library tab.
  2. Select the Secure files tab at the top.
  3. Upload your file.
  4. In the details view under Properties, select Authorize for use in all pipelines, and then select Save.

Step #2: Linter

Linters are a great tool to ensure your code quality, so it is a must have step in our pipeline. In our case, we used CocoaPods and installed a Pod for linter called SwiftLint directly from the command line task. (For more information visit CocoaPods)

This runs the swiftlint command stored as a Pod, the lint results are shown in the Pipeline terminal in DevOps.

Step #3: Build and Sign

Another essential step for any development pipeline is to build the solution so we know that everything is working as expected. In our case, we do not only build the solution, but we also sign it as part of the same process. For that, we use the Xcode task with the build action.

These are the variables needed to sign the app, for more information you can check this article from apple developer forums.

Step #4: Run Tests

Tests are a key part of a good development process, as it helps us to review that the solution is not only building and running, but also that is doing what is expected. Even when you run them locally it is a good practice and a very helpful one to run your tests as part of your CI pipeline. In our case, we are using the Xcode tasks executing the test action.

Step #5: Code Coverage

One thing that you should be aware is how much tested your code is. You might have multiple tests but not covering all the scenarios or even all parts of the solution. To ensure this, we are using the Slather third party tool to publish the Code Coverage reports generated by XCode to the Azure Pipeline. To use this tool, we need to install it first using the gem install slather script. After running slather, you will also need to publish those results using the PublishCodeCoverageResults task.

Step #6: Packaging

In order to distribute our app outside the store, we need to notarize it and to do that, first, we need to create a package installer from our .app. We use the productbuild command as shown here directly from the CmdLine task.

Step #7: Notarization

Here we use the xcrun altool command to upload our package for notarization.

Note: You will need an Application Specific Password to run this command, you can get one by logging in here. For more information, visit this link.

Important information: After running the notarization task, you will get a request ID that will be used to check the status of the notarization. To do so, you need to run the following command.

xcrun altool --notarization-info "REQUEST-ID" -u "your@apple.account" -p $(appSpecificPassword)

Common Notarization Issues

We fixed most of our notarization issues by adding some build variables in our project.pbxproj file. You will need to add the following to the Debug and Release build settings of your project.

Important information:CODE_SIGN_INJECT_BASE_ENTITLEMENTS set to "NO" will prevent your app from running through Xcode and will also make your test project fail. This setting must be set ONLY when you have finished testing and debugging your app. After packaging and notarization, your app will run after installing it and opening the .app file with no issues.

For more information, visit Resolving Common Notarization Issues.

Step #8: Artifact Creation and Publishing

We will need to create the artifact from our notarized package to publish it for deployment later. To do this, we need to do two steps, first to copy the files to a new folder and then call the PublishPipelineArtifact task to publish those files as an artifact which is your built app ready to be deployed.

In this case, $(Pipeline.Workspace) is a predefined pipeline variable, which indicates the workspace directory for a particular pipeline.

Pipeline artifacts are the next generation of build artifacts and are the recommended way to work with artifacts. Artifacts published using the Publish Build Artifacts task can continue to be downloaded using Download Build Artifacts but can also be downloaded using the latest Download Pipeline Artifact task.

And that is it! After publishing the artifact, all that is left to do is to create a Release pipeline to grab the artifact from the /drop folder and deploy it to the target of your choice.

Summing Up

In conclusion, we have gone through an introduction to Azure Pipelines, Continuous Integration, Continuous Delivery as well as a tutorial on how to run an Azure Pipeline for an out of store macOS project in Swift. Thank you for reading!

You can find the whole azure-pipelines.yaml file here, in case you need it!

Thanks to Mauro Krikorian

Originally published by Nicolás Bello Camilletti for SOUTHWORKS on Medium 28 December 2020