Xamarin.Forms Android App Bundle (aab) DevOps YAML Pipeline
In my previous post I went through the process of creating a DevOps CD pipeline for Xamarin.Forms Android applications. I've received some feedback that even after following the blog post to the letter, build pipeline wasn't working for some of you. That got me thinking...
YAML
It would be good to share the exact setup of my build and release pipelines for better reference. I wondered if that's doable with the classic build and release pipelines. I couldn't find an easy way...
Fortunately enough, I've recently jumped on the YAML hype wagon and slowly started becoming a YAML ninja (sic!). These rather freshly acquired skills gave me enough confidence to re-write my existing build and release pipeline into two stage YAML pipeline! Woohoo!
Here's a GitHub Gist in case the below is not displaying properly.
trigger:
branches:
include:
# Your CI branch that the build should be triggered for
- vNext
stages:
- stage: Build
pool:
vmImage: 'windows-latest'
jobs:
- job: GenerateAab
variables:
# Your major and minor version numbers
appVersion: '1.2'
buildConfiguration: 'Release'
androidNdkPath: 'C:\Microsoft\AndroidNDK64\android-ndk-r16b'
steps:
# Code "stolen" from James, from his DevOps tasks -> https://github.com/jamesmontemagno/vsts-mobile-tasks
- task: PowerShell@2
displayName: 'Updating Version Code and Name in Android Manifest'
inputs:
targetType: 'inline'
script: |
[string] $sourcePath = "$(System.DefaultWorkingDirectory)\Path\To\Your\Android\Project\Properties\AndroidManifest.xml"
[string] $appVersionName = "$(AppVersion).$(Build.BuildId)"
# I would suggest to use Build.BuildId variable for the version code but I've started using date and can't go back to lower numbers
[string] $appVersionCode = Get-Date -Format "yyMMddHH"
# Load the Android Manifest file
[xml] $androidManifestXml = Get-Content -Path $sourcePath
Write-Host "Original Manifest:"
Get-Content $sourcePath | Write-Host
# Get the version name
$VersionName= Select-Xml -xml $androidManifestXml -Xpath "/manifest/@android:versionName" -namespace @{android = "http://schemas.android.com/apk/res/android" }
$oldVersionName= $VersionName.Node.Value;
Write-Host " (i) Original Version Name: $oldVersionName"
$VersionName.Node.Value = $appVersionName
Write-Host " (i) New Package Name: $appVersionName"
# Get the version code
$VersionCode= Select-Xml -xml $androidManifestXml -Xpath "/manifest/@android:versionCode" -namespace @{android = "http://schemas.android.com/apk/res/android" }
$oldVersionCode = $VersionCode.Node.Value;
Write-Host " (i) Old Version Code: $oldVersionCode"
$VersionCode.Node.Value = $appVersionCode
Write-Host " (i) New App Name: $appVersionCode "
$androidManifestXml.Save($sourcePath)
Write-Host "Final Manifest:"
Get-Content $sourcePath | Write-Host
- task: NuGetToolInstaller@1
displayName: 'Installing Nuget'
- task: NuGetCommand@2
displayName: 'Restoring Nugets'
inputs:
restoreSolution: '**/*.sln'
- task: DownloadSecureFile@1
displayName: 'Download keystore'
# This file's name will be used in the build task as a value for parameter AndroidSigningKeyStore
name: keystore
inputs:
# This file needs to be stored in the DevOps' Library
secureFile: 'NameOfYourKeystoreFile.keystore'
# KeystorePassword is a secure variable that has been set using GUI in the DevOps portal
- task: XamarinAndroid@1
displayName: 'Build aab'
inputs:
projectFile: Path/To/Your/Android/Project/NameOfYourDroidHeadProject.csproj
outputDirectory: '$(Build.BinariesDirectory)'
configuration: '$(BuildConfiguration)'
clean: true
msbuildVersionOption: latest
msbuildArguments: ' /p:JavaSdkDirectory="$(JAVA_HOME)/" /t:SignAndroidPackage /p:AndroidNdkDirectory="$(androidNdkPath)" /p:AndroidKeyStore="True" /p:AndroidSigningKeyStore="$(keystore.secureFilePath)" /p:AndroidSigningKeyPass="$(KeystorePassword)" /p:AndroidSigningKeyAlias="YourKeystoreAlias" /p:AndroidSigningStorePass="$(KeystorePassword)"'
- task: PublishPipelineArtifact@1
displayName: 'Publishing aab artifacts'
inputs:
targetPath: '$(Build.BinariesDirectory)'
artifact: AndroidAabPackage
publishLocation: 'pipeline'
- stage: Release
pool:
vmImage: 'macOS-latest'
dependsOn: Build
# The condition checks if the Build stage has succeeded and if it has been triggered from the vNext branch
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/vNext'))
jobs:
- job: GooglePlayBetaRelease
steps:
# Another file that needs to be stored in the DevOps' Library (see my previous post on how to generate it)
- task: DownloadSecureFile@1
displayName: 'Download Google Play Console JSON key auth file'
name: serviceAccAuthJson
inputs:
secureFile: 'Google_Play_Store_Dev_Ops_Service_Acc_Auth.json'
- task: DownloadPipelineArtifact@2
displayName: 'Downloading aab package'
inputs:
buildType: 'current'
artifactName: 'AndroidAabPackage'
itemPattern: '**/com.yourcompanyname.yourappname-Signed.aab'
targetPath: '$(Build.SourcesDirectory)\Artifacts'
- task: Bash@3
displayName: 'Fastlane - Release aab to Beta'
inputs:
workingDirectory: '$(Build.SourcesDirectory)\Artifacts'
targetType: 'inline'
script: |
fastlane supply --aab com.yourcompanyname.yourappname-Signed.aab --json_key $(serviceAccAuthJson.secureFilePath) --track beta --rollout 1.0 --package_name com.yourcompanyname.yourappname --skip_upload_apk true
Conclusion
I was quite skeptical about learning YAML at first, as I'm rather a GUI person and I really like the classic way of working with DevOps pipelines. I must say though, that I was pleasantly surprised how easy it is to get YAML to work. Definitely give it a go!
Another selling point for YAML for me was the ability to create Pull Requests with changes made to the pipeline. It's soooo much better to have history of changes in commits and even more importantly having others review your code, which gives much more control over what's going into the pipelines, as opposed to the classic way when changes are usually completely unnoticed.