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:
- Create a temporary GIT branch on the pipeline agent
- Identify and loop through all CSProj files in the solution directories
- Set version information on all identified CSProj files (.NET Core+)
- Add the updates to the GIT repo for the temp branch
- Set the Username and Email for the commit details to reflect the Azure Agent
- Commit changes to the temp branch
- 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...