C#
.NET
MAUI
Azure
4 minute read | April 18, 2024

Building a MAUI iOS app with Azure DevOps Pipelines

Learn how to build and sign your MAUI iOS app with Azure DevOps Pipelines and ouput an ipa file.

Setup

First off create yourself a new blank .yaml file if you don't already have one.

Triggers

You can trigger your pipeline based on changes to a branch if you like. For this example we'll trigger it manually.

Yaml
trigger: none

Build stage

Now lets add our build stage and configure it to use the lastest macOS image (at the time of writing macOS-13). You can also use 'macOS-latest' but confusingly it might not be the latest version.

Yaml
trigger: none stages: - stage: Build displayName: Build stage jobs: - job: Build displayName: Build pool: vmImage: 'macOS-13'

Configure MAUI

We need to install the MAUI workload first before we build so lets add a task for it.

Yaml
trigger: none stages: - stage: Build displayName: Build stage jobs: - job: Build displayName: Build pool: vmImage: 'macOS-13' steps: - task: CmdLine@2 displayName: 'Install Latest .NET MAUI Workload' inputs: script: 'dotnet workload install maui'

(Optional) Select xcode version

If you want to build with a specific version of xcode you can use xcode-select. In this example I'm selecting xcode 15.2

Yaml
- task: CmdLine@2 displayName: 'Use Xcode 15.2' inputs: script: 'sudo xcode-select -s /Applications/Xcode_15.2.app'

Install certificates and provisioning profiles

In order to sign your app you'll need an apple developer certificate and a provisioning profile. Upload your .p12 and .mobileprovision to Pipelines > Library > Secure files on DevOps.

Install the certificate

I've used pipeline variables for the filename and password but the values can be hardcoded instead

Yaml
- task: InstallAppleCertificate@2 displayName: 'Install apple certificate' inputs: certSecureFile: '$(certificateFilename)' certPwd: '$(certificatePassword)' keychain: 'temp'

Install the provisioning profile

I've used a pipeline variable for the filename but the value can be hardcoded instead

Yaml
- task: InstallAppleProvisioningProfile@1 displayName: 'Install app store provisioning profile' inputs: provisioningProfileLocation: 'secureFiles' provProfileSecureFile: '$(appStoreProvisioningProfileFilename)'

Build the project

The next step is to build and sign your project. We'll use the dotnet publish command to achieve that. Don't change the values of CodesignKey CodesignProvision as they are output by the previous two steps. I've also set working directory as I'm using a multi-project solution, if you are using single project them you shouldn't need to.

Yaml
- task: CmdLine@2 displayName: 'Build project' inputs: script: 'dotnet publish -f net8.0-ios -c Release -p:ArchiveOnBuild=true -p:RuntimeIdentifier=ios-arm64 -p:CodesignKey="$(APPLE_CERTIFICATE_SIGNING_IDENTITY)" -p:CodesignProvision="$(APPLE_PROV_PROFILE_NAME)"' workingDirectory: './Source/MyProject.iOS/'

Publish build output

Finally the last thing to do is publish the ipa file that is output by the build. Note that the source folder will differ depending on your project name but dotnet publish always outputs to 'bin/Release/net8.0-ios/{RuntimeIdentifier}/publish'

Yaml
- task: CopyFiles@2 displayName: 'Copy build output to staging' inputs: SourceFolder: '$(Agent.BuildDirectory)/s/Source/MyProject.iOS/bin/Release/net8.0-ios/ios-arm64/publish' Contents: '**/*.ipa' TargetFolder: '$(ArtifactStagingDirectory)' flattenFolders: true - publish: $(ArtifactStagingDirectory) artifact: drop