DEV Community

Victor Hugo Garcia
Victor Hugo Garcia

Posted on • Edited on

6

Azure DevOps for .NET MAUI using YML

NET Multi-platform App UI (.NET MAUI) is a cross-platform framework for creating native mobile and desktop apps with C# and XAML. Using .NET MAUI, you can develop apps that can run on Android, iOS, macOS, and Windows from a single shared code-base. In these post series, I will show you how to implement Continuous Integration in your .NET MAUI app using the Microsoft Azure DevOps with YML in a multi-environment.


Disclaimer

The steps below represent an approach I follow for my projects due to they accommodate for my requirements. You are free to update the logic and YML files to fit your needs on each project. Also, in an effort to demo the multi-environment, I am using Firebase Analytics services.


Getting Started

Shared

  • In the Azure Pipelines -> Library upload the Android KeyStore file, the Apple .p12 certificate & provisioning profile files and the Google firebase config files if you are using this service.

Make sure the Google firebase files are renamed using the following sample:
Image description

  • Then create a variable group called: "App Configuration" within the Library, this variable group will contain the configuration keys for the Android and iOS build and we use them because it is the securest way to store the sensitive data.
Variable Value
app-build-configuration Release
app-check-dependencies true
app-id-android com.company.appname
app-id-ios com.company.appname
app-path-contents PROJECT_PATH/bin
app-path-project PROJECT_PATH/project.csproj
app-path-root PROJECT_PATH
app-target-framework-android .net7.0-android
app-target-framework-ios .net7.0-ios
apple-certificate-name CERTIFICATE_NAME.p12
apple-certificate-password ******
apple-provisioning-profile-name PROFILE_NAME.mobileprovision
dotnet-version 7.0.x
android-signing-key-password ******
android-keystore-alias ALIAS
android-keystore-name FILE.keystore
android-keystore-password ******

iOS

  • Create a new file azure-pipelines-ios.yml in your code repository at any path you prefer.
# Documentation
#
# Set the vX.X.X-X tag (vMAJOR.MINOR.PATCH-BUILD_NUMBER)
# Install the Mobile Versioning in your Azure workspace https://marketplace.visualstudio.com/items?itemName=DamienAicheh.mobile-versioning-task
#
parameters:
- name: environment
displayName: Select an Environment
type: string
default: 'Staging'
values:
- Production
- Staging
variables:
- group: 'App Configuration'
- ${{ if eq(parameters.environment, 'Staging') }}:
- group: 'Firebase Configuration Staging'
- ${{ if eq(parameters.environment, 'Production') }}:
- group: 'Firebase Configuration Production'
- name: ENVIRONMENT
value: ${{ parameters.environment }}
trigger:
branches:
exclude:
- '*'
tags:
include:
- v2.*.*-*
- v3.*.*-*
- v4.*.*-*
- v5.*.*-*
paths:
exclude:
- README.md
stages:
# Stage for MAUI iOS
- stage: build_maui_ios
jobs:
- job: build_maui_ios_app
displayName: Build App for iOS
pool:
vmImage: macos-latest
steps:
- task: InstallAppleCertificate@2
inputs:
certSecureFile: '$(apple-certificate-name)'
certPwd: '$(apple-certificate-password)'
keychain: 'temp'
- task: InstallAppleProvisioningProfile@1
inputs:
provisioningProfileLocation: 'secureFiles'
provProfileSecureFile: '$(apple-provisioning-profile-name)'
# Extract the version of the last tag you pushed to your Git repository, from the branch you selected to build your pipeline.
- task: ExtractVersionFromTag@1
displayName: 'Get git tag version'
inputs:
projectFolderPath: '$(Build.SourcesDirectory)'
# Download the Google Firebase Configuration from secured files
- task: DownloadSecureFile@1
name: firebase
displayName: Download Firebase Configuration
inputs:
secureFile: '$(google-ios-config-file)'
- script: |
echo Downloaded $(firebase.secureFilePath)
mv $(firebase.secureFilePath) $(System.DefaultWorkingDirectory)/$(app-path-root)/GoogleService-Info.plist
displayName: Move Firebase configuration to Working Directory
# Install .NET SDKs
- task: UseDotNet@2
displayName: Install .NET SDK
inputs:
packageType: 'sdk'
version: '$(dotnet-version)'
includePreviewVersions: false
# Install all workloads your solution is supported
- powershell: dotnet workload install maui-android maui-ios
displayName: Install .NET MAUI Workload
# build project
- task: CmdLine@2
displayName: 'Build project'
inputs:
script: |
dotnet publish $(app-path-project) -f $(app-target-framework-ios) -c Release /p:ApplicationId=$(app-id-ios) /p:ApplicationDisplayVersion=$(MAJOR).$(MINOR).$(PATCH) /p:ApplicationVersion=$(PRE_RELEASE) /p:ArchiveOnBuild=true /p:EnableAssemblyILStripping=false
- task: CopyFiles@2
displayName: 'Copy files to: $(build.artifactstagingdirectory)'
inputs:
SourceFolder: '$(Agent.BuildDirectory)'
Contents: '**/*.ipa'
TargetFolder: '$(Build.ArtifactStagingDirectory)'
CleanTargetFolder: true
flattenFolders: true
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifacts'
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop_maui_ios'
publishLocation: 'Container'

Android

  • Create a new file azure-pipelines-android.yml in your code repository at any path you prefer.
# Documentation
#
# Set the vX.X.X-X tag (vMAJOR.MINOR.PATCH-BUILD_NUMBER) to trigger the pipeline
# Install the Mobile Versioning in your Azure workspace https://marketplace.visualstudio.com/items?itemName=DamienAicheh.mobile-versioning-task
#
parameters:
- name: environment
displayName: Select an Environment
type: string
default: 'Staging'
values:
- Production
- Staging
variables:
- group: 'App Configuration'
- ${{ if eq(parameters.environment, 'Staging') }}:
- group: 'Firebase Configuration Staging'
- ${{ if eq(parameters.environment, 'Production') }}:
- group: 'Firebase Configuration Production'
- name: ENVIRONMENT
value: ${{ parameters.environment }}
# Setting the trigger when a tag has been added to any branch.
trigger:
branches:
exclude:
- '*'
tags:
include:
- v2.*.*-*
- v3.*.*-*
- v4.*.*-*
- v5.*.*-*
paths:
exclude:
- README.md
stages:
# Stage for MAUI Android
- stage: build_maui_android
jobs:
- job: build_maui_android_app
displayName: Build App for Android
pool:
vmImage: 'windows-latest'
demands:
- MSBuild
steps:
# Extract the version of the last tag you pushed to your Git repository, from the branch you selected to build your pipeline.
- task: ExtractVersionFromTag@1
displayName: 'Get git tag version'
inputs:
projectFolderPath: '$(Build.SourcesDirectory)'
# Download the keystore from secured files
- task: DownloadSecureFile@1
name: keystore
displayName: Download keystore
inputs:
secureFile: '$(keystore-filename)'
- script: |
echo Downloaded $(keystore.secureFilePath)
echo Environment $(ENVIRONMENT)
echo Working Directory $(System.DefaultWorkingDirectory)\\$(keystore-filename)
mv $(keystore.secureFilePath) $(System.DefaultWorkingDirectory)
displayName: Move Keystore to Working Directory
# Download the Google Firebase Configuration from secured files
- task: DownloadSecureFile@1
name: firebase
displayName: Download Firebase Configuration
inputs:
secureFile: '$(google-android-config-file)'
- script: |
echo Downloaded $(firebase.secureFilePath)
mv $(firebase.secureFilePath) $(System.DefaultWorkingDirectory)/$(app-path-root)/google-services.json
displayName: Move Firebase configuration to Working Directory
# Install .NET SDKs
- task: UseDotNet@2
displayName: Install .NET SDK
inputs:
packageType: 'sdk'
version: '$(dotnet-version)'
includePreviewVersions: false
# Install Java SDK for Android
- task: JavaToolInstaller@0
displayName: Install Java SDK
inputs:
versionSpec: '11'
jdkArchitectureOption: 'x64'
jdkSourceOption: 'PreInstalled'
# Install all workloads your solution is supported
- powershell: dotnet workload install maui-android maui-ios
displayName: Install .NET MAUI Workload
# build project
- task: CmdLine@2
displayName: 'Build project'
inputs:
script: |
dotnet publish $(app-path-project) -f $(app-target-framework-android) -c Release /p:ApplicationId=$(app-id-android) /p:ApplicationDisplayVersion=$(MAJOR).$(MINOR).$(PATCH).$(PRE_RELEASE) /p:ApplicationVersion=$(MAJOR)$(MINOR)$(PATCH)$(PRE_RELEASE) /p:AndroidSigningKeyPass=$(key-password) /p:AndroidSigningStorePass=$(keystore-password) /p:AndroidSigningKeyStore=$(System.DefaultWorkingDirectory)\\$(keystore-filename) /p:AndroidSigningKeyAlias=$(keystore-alias) /p:AndroidKeyStore=true
# Copy files to artifact directory Temporary just to make sure the AAB is generated and signed correctly
- task: CopyFiles@2
displayName: 'Copy files to: $(build.artifactstagingdirectory)'
inputs:
SourceFolder: '$(system.defaultworkingdirectory)'
Contents: '$(app-path-contents)/$(app-build-configuration)/$(app-target-framework-android)/publish/**'
TargetFolder: '$(build.artifactstagingdirectory)'
CleanTargetFolder: true
# Publish artifacts
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifacts'
inputs:
PathtoPublish: '$(build.artifactstagingdirectory)'
ArtifactName: 'drop_maui_android'
publishLocation: 'Container'

Basically, the pipelines will always trigger automatically pointing to the staging environment when a versioning tag is added to a branch, however, you can run the pipeline manually and set the environment pointing to production, so it generates a new version of the app. Just, don't forget to set the version tag before trigger the pipeline manually.

What About Distribution?

I'm working in a post to share how to distribute the application through different channels. I hope to have it ready soon.

Conclusion

There are tons of combinations and customizations for the YML and each project may have its own set of rules for CI. Take this just as an example or base and improve it, please!

I hope this helps others to create their own pipelines for .NET MAUI.

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more

Top comments (6)

Collapse
 
lomeshnparmar profile image
lomeshnparmar • Edited

@vhugogarcia do you have any updates for how to release (CD) for generated .apk or .ipa? Also want to know how can we release to VS App Center?

Collapse
 
vhugogarcia profile image
Victor Hugo Garcia

Hello @lomeshnparmar ,

Thanks for looking into. I have been very busy, however, you can certainly refer to these 2 good guides from our friend Damien:

I followed those guides to successfully deploy my apps to the correct channels.

About VS App Center, I don't have experience, however, I will research about it.

Collapse
 
bcr profile image
B.R.

There is an AppCenter CLI, this would probably be the most useful. There are also "classic" tasks for AppCenter as well.

Collapse
 
andrewgene profile image
Andrew Goodwin

Thank you so much for the download of the keystore file. We missed that step and COULD NOT get it to work.

Collapse
 
rajkumarkvvsn profile image
Rajkumar

@vhugogarcia

Thanks for the great post. one question, this yml schema support .Net Android and .Net iOS?

Collapse
 
vhugogarcia profile image
Victor Hugo Garcia

Hello @rajkumarkvvsn ,

This yml only supports .NET MAUI apps.

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more