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.