MAUI iOS Azure DevOps pipelines – Build & publish for App Store review

Lately, I was working on a .NET MAUI project, where I had to build different Azure DevOps pipelines for build and publish. There are some blogposts which are pretty helpful, but I ended up with a slightly different solution after knowledge gathering from the following posts:

I ended up with setting up 2 pipelines. One for building the application and publishing the artifact (which is an IPA file) for testing to able to easily install on a QA device. On the other hand I wanted to automate the publishing for a review to the App Store.

Prerequisites for my pipelines

File structure

├─ 📂 src
│ └─ ♾️ MyApplication.Mobile.sln
├─ 🚀 azure-pipelines-build.yml
├─ 🚀 azure-pipelines-publish.yml
├─ 📄 MyApplication.mobileprovision
└─ 📄 MyApplicationDistribution.mobileprovision

The development.p12 & distribution.p12 file

These files are required to able to build a signed IPA file. You can download these certs from the Apple Developer center and install to the Keychain and then export the .p12 files – for this step you need a macOS. You need two different certifications, one for development and another for App Store distribution.

development.p12
distribution.p12

Once, these certs are generated, those should be uploaded to Azure DevOps library as secret files. The pipelines below, are using this library to retrieve the certs and upload to the Keychain of the virtual agent machine.

Azure DevOps Library

Visual Studio project file

The .csproj of the project needs to be also slightly edited to able to use the pipelines below, here are the relevant parts:

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- 'Test' OR 'Prod', 'Test' by default -->
<Environment>Test</Environment>
<!-- Use distribution provision profile or development, false by default -->
<Distribute>false</Distribute>
<DefineConstants>ENV_$(Environment)</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)'=='net6.0-ios'">
<ProvisioningType>automatic</ProvisioningType>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)'=='net6.0-ios' and '$(Distribute)' == true">
<ProvisioningType>manual</ProvisioningType>
<CodesignKey>iPhone Distribution: Account Name (Account ID)</CodesignKey>
<CodesignProvision>MyApplicationDistribution</CodesignProvision>
</PropertyGroup>
</Project>

Build pipeline

trigger:
- main
variables:
DotNetVersion: 6.0.x
stages:
# iOS
- stage: BuildiOS
dependsOn: ''
displayName: Build iOS
jobs:
- job: BuildMAUIApps
displayName: Build app
pool:
vmImage: 'macos-latest'
demands: 'xcode'
steps:
- task: UseDotNet@2
displayName: Use .NET version
inputs:
packageType: 'sdk'
version: '$(DotNetVersion)'
- task: CmdLine@2
displayName: Install MAUI
inputs:
script: 'dotnet workload install maui'
- task: InstallAppleCertificate@2
displayName: Install Apple Certificate
inputs:
certSecureFile: 'development.p12'
certPwd: ''
keychain: 'temp'
- task: InstallAppleProvisioningProfile@1
displayName: Install Apple Provisioning Profile
inputs:
provisioningProfileLocation: 'sourceRepository'
provProfileSourceRepository: 'MyApplication.mobileprovision'
- task: Bash@3
displayName: Restore nuget
inputs:
targetType: 'inline'
script: |
cd src
dotnet restore MyApplication.Mobile.sln
- task: Bash@3
displayName: Publish iOS app
inputs:
targetType: 'inline'
script: |
cd src
dotnet publish -f net6.0-ios -c Release -p:ArchiveOnBuild=true -p:EnableAssemblyILStripping=false -p:Environment=$ENVIRONMENT
env:
ENVIRONMENT: $(Environment)
- task: CopyFiles@2
displayName: Copy artifacts
inputs:
SourceFolder: '$(agent.builddirectory)'
Contents: |
**/*.ipa
TargetFolder: '$(build.artifactstagingdirectory)'
flattenFolders: true
- task: PublishBuildArtifacts@1
displayName: Publish artifacts
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop-ios'
publishLocation: 'Container'

Publishing pipeline

trigger: none
variables:
DotNetVersion: 6.0.x
stages:
# PROD iOS build and upload
- stage: BuildiOS
dependsOn: ''
displayName: PROD - Build iOS and upload for review
jobs:
- job: BuildMAUIApps
displayName: Build app
pool:
vmImage: 'macos-latest'
demands: 'xcode'
steps:
- task: UseDotNet@2
displayName: Use .NET version
inputs:
packageType: 'sdk'
version: '$(DotNetVersion)'
- task: CmdLine@2
displayName: Install MAUI
inputs:
script: 'dotnet workload install maui'
- task: InstallAppleCertificate@2
displayName: Install Apple Certificate
inputs:
certSecureFile: 'distribution.p12'
certPwd: ''
keychain: 'temp'
- task: InstallAppleProvisioningProfile@1
displayName: Install Apple Provisioning Profile
inputs:
provisioningProfileLocation: 'sourceRepository'
provProfileSourceRepository: 'MyApplicationDistribution.mobileprovision'
- task: Bash@3
displayName: Restore nuget
inputs:
targetType: 'inline'
script: |
cd src
dotnet restore MyApplication.Mobile.sln
- task: Bash@3
displayName: Publish iOS app
inputs:
targetType: 'inline'
script: |
cd src
dotnet publish -f net6.0-ios -c Release -v d -p:ArchiveOnBuild=true -p:Environment='Prod' -p:Distribute=true
- task: CopyFiles@2
displayName: Copy artifacts
inputs:
SourceFolder: '$(agent.builddirectory)'
Contents: |
**/*.ipa
TargetFolder: '$(build.artifactstagingdirectory)'
flattenFolders: true
- task: PublishBuildArtifacts@1
displayName: Publish artifacts
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop-ios'
publishLocation: 'Container'
- task: Bash@3
displayName: Validate
inputs:
targetType: 'inline'
script: |
cd $BUILDDIR
xcrun altool --validate-app -f MyApplication.Mobile.ipa -t ios -u $APPLEACCOUNTUSER -p $APPLEACCOUNTPASSWORD --output-format json --verbose
env:
APPLEACCOUNTUSER: $(AppleAccountUser)
APPLEACCOUNTPASSWORD: $(AppleAccountPassword)
BUILDDIR: $(Build.ArtifactStagingDirectory)
- task: Bash@3
displayName: Upload to App Store
inputs:
targetType: 'inline'
script: |
cd $BUILDDIR
xcrun altool --upload-app -f MyApplication.Mobile.ipa -t ios -u $APPLEACCOUNTUSER -p $APPLEACCOUNTPASSWORD --output-format json --verbose
env:
APPLEACCOUNTUSER: $(AppleAccountUser)
APPLEACCOUNTPASSWORD: $(AppleAccountPassword)
BUILDDIR: $(Build.ArtifactStagingDirectory)

2 thoughts on “MAUI iOS Azure DevOps pipelines – Build & publish for App Store review

  1. Great post. I had to make a few changes:

    I’m building locally using on premise MacOS build agent and DevOps Server, so I changed:
    pool:
    vmImage: ‘macos-latest’
    demands: ‘xcode’
    To:
    pool:
    name: {poolname}

    I had to use InstallAppleCertificate-sxs@2 (available in DevOps extensions) on macOS 13.x to get around an OpenSSL SHA1->SHA256 issue.

    I added -r ios-arm64 to the dotnet publish task.

    I also had to alter SourceFolder: ‘$(agent.builddirectory)’ on the CopyFiles@2 tasks to be a lower level directory. The task is a bit buggy with certain pathing on MacOS it sounds like.

    Have you attempted doing an android build and publish to the Play Store using MacOS? That is up next for me and I’m debating between using windows or MacOS build agent.

    Like

    1. Thanks for sharing the findings on an on-prem MacOS build agent and the coffees 🙌 I have done the Android build on a Windows agent but not the publish to Play Store (I skipped that for now, it was just easier to upload manually). If the mentioned articles does not help for you, I am happy to share my Android build pipeline.

      Like

Leave a comment