Pages

Wednesday, December 28, 2022

Updating Project Versions in Azure DevOps

We have a couple projects in DevOps that specify an incrementing build version as part of the release pipeline. The approach we have is working well and is deploying our projects with the proper version to our hosted environments. This approach, however, was causing some issues for local development and we needed a way to keep our local versions in sync with the releases. After looking into the behaviour I discovered we weren't the only ones. A quick search revealed many discussions trying to deal with this situation, and after some trial and error I was able to come up with a solution.

DevOps and PowerShell provide enough flexibility

The existing version update happens via a PowerShell script that runs as part of the release pipeline. First, a variable is set to the build version to be used. Then, the build command is called with that variable to specify the version. Because that process was working and in place prior to my involvement on the pipeline I didn't want to change it. Instead, I added a new pipeline step after the existing versioning step, and called a new custom PowerShell script. 

[pre class="brush:yml;" title="Sample Pipeline YML"]
trigger:
- develop

pool:
  vmImage: 'windows-latest'

variables:
  # My project variables here
 
steps:
  # This is important to allow scripts access to auth token
- checkout: self
  persistCredentials: true

  # Set Build Number to variable
- task: PowerShell@2
  displayName: 'Set the Build Number'
  inputs:
     targetType: inline
     script:
       Write-Host "Setting Build Number"       
       Write-Host "###vso[task.setvariable variable=buildNumber]$(VersionNumber).$(VersionRevision).$(($(Get-Date).ToUniversalTime()-[datetime]"01/01/2022 00:00").Days.ToString()).$([System.Math]::Floor($(Get-Date).TimeOfDay.TotalMinutes).ToString())"

  # Extra step for debugging version
- task: PowerShell@2
  displayName: 'Confirm Build Number'
  inputs:
    targetType: inline
    script: Write-Host "Build Number $(buildNumber)"

  # Update version in code in repo
- task: PowerShell@2
  displayName: 'Update Project Version'
  inputs:
    targetType: filePath
    filePath: 'dev\_SolutionItems\Powershell Scripts\UpdateProjectVersions.ps1'
    arguments: '$(Build.SourceBranch) $(Build.SourcesDirectory) $(buildNumber)'

  # Extra steps for various build and configuration tasks go here

  # Build command specifying version previously set
- task: DotNetCoreCLI@2
  displayName: 'Build Code'
  inputs:
    command: build
    projects: '$(projects)'
    arguments: '--configuration $(buildConfiguration) --framework $(coreFramework) -p:Version=$(buildNumber)'

  # Code to publish and deploy package
[/pre]

 

Pay attention to the first two lines under steps declaring a checkout step with the "persistCredentials" option set to "true". This ensures that scripts executing can access the system token. This is needed for proper permissions for the GIT operations in the PowerShell script.

The custom PowerShell script updates the version information in the code base. 

The important thing about updating the version in the codebase is it needs to make that update to the code in the repository. The script below is the solution to perform that task. It takes in 3 required parameters to specify the solution root directory, the version number to use, and the calling branch from our GIT repo. It then performs the following tasks:

  1. Create a temporary GIT branch on the pipeline agent
  2. Identify and loop through all CSProj files in the solution directories
  3. Set version information on all identified CSProj files (.NET Core+)
  4. Add the updates to the GIT repo for the temp branch
  5. Set the Username and Email for the commit details to reflect the Azure Agent
  6. Commit changes to the temp branch
  7. Merge and push the changes into the source branch

[pre class="brush:ps" title="Update Version PowerShell"]
param
  (
    [Parameter(Position=0, Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string]$sourceBranch,
    [Parameter(Position=1, Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string]$rootDir,
    [Parameter(Position=2, Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string]$version
  )
  if([string]::IsNullOrWhiteSpace($sourceBranch)){
      throw "Source branch must be specified."
  }
  if([string]::IsNullOrWhiteSpace($rootDir)){
    throw "Please specify the root path containing the CSProj files to update."
  }
  if([string]::IsNullOrWhiteSpace($version)){
    throw "Please specify the version number to be used."
  }

$sourceBranch = $sourceBranch.Replace("refs/heads/","")
$tempBranch = "temp/UpdateVersion_$($version)"
# create a new branch for hosting update
git branch $tempBranch --quiet
# switch to new branch for performing updates
git checkout $tempBranch --quiet

# function to update or add and update XML nodes in project group
Function SetNodeValue($xmlRootNode, [string]$nodeName, [string]$nodeValue) {
    $propGroup = $xmlRootNode.Node.SelectSingleNode("PropertyGroup")
    $node = $propGroup.SelectSingleNode("./$($nodeName)")
    
    if ($node -eq $null) {
        Write-Host "Adding node for $($nodeName)"
        $node = $propGroup.OwnerDocument.CreateElement($nodeName)
        $nodeAdded = $propGroup.AppendChild($node)    # do this to avoid console output
    }
    else {
        Write-Host "Existing node found for $($nodeName)"
    }
    $node.InnerText = $nodeValue   
    Write-Host "Set value $($nodeValue) for node $($nodeName)"
}   
# loop all CSProj files so they are all on same version
Get-ChildItem -Path $rootDir -Filter "*.csproj" -Recurse -File |
  ForEach-Object {
      Write-Host "Found project file $($_.FullName)"
      $projPath = $_.FullName
      $projXml = Select-Xml $projPath -XPath "//Project"
     
      SetNodeValue $projXml "AssemblyVersion" $version
      SetNodeValue $projXml "FileVersion" $version
      SetNodeValue $projXml "Version" $version
     
      $doc = $projXml.Node.OwnerDocument     
      $doc.PreserveWhitespace = $true
      $doc.save($projPath)
     
    # add the updated CSProj to GIT
    git add $projPath     
  }

# update email and username to identify repo updates by automated pipe - not a real address
git config user.email "azure.agent@mydomain.com"
git config user.name "Azure Agent" --replace-all
# commit changes with a comment identifying pipeline mod
git commit -m "[auto] Update Version: Set project versions to $($version)"
# switch back to the original branch that triggered this
git checkout $sourceBranch --quiet
# pull latest from git
git pull --quiet
# merge the proj updates back into source branch, with comment to indicate operation
git merge $tempBranch -m "[auto] Update Version: Complete version to $($version) for source $($sourceBranch)" --quiet
# push changes back into source branch
git push -u origin --quiet
[/pre]

 

This process updates the version information in our repo

With this script in place, each time a merge happens to our pipeline branch, the develop branch in this case, the version will be updated in each project file for our solution. After this, any pull of that branch will include the latest version number in it. This does mean that any local branches in progress when this happens need to pull down and merge the latest version before committing to include the latest changes without conflict. But that is a small price to pay for the automation this provides.

No comments:

Post a Comment

Share your thoughts or ask questions...