As developers, we do not want to waste time on releases, and one of the challenges with an automatic release is updating the version number.
Conventional Commits
At multiple companies I have introduced the conventional commits website. This allows us to easily add the type of change in our commit title.
If every developer uses these conventions, you can automate versioning in the pipeline, and that is what I did about two years ago. I will show you how.
First of all, I have to admit that I am not a PowerShell guru, so feel free to improve the scripts if this post helped you.
Now lets start with all the steps we need to take and what better way then using an example pipeline.
Patch number logic
We set the default name of the pipeline to $(date:yyyyMMdd)$(rev:rr), so we can use this as the patch number. This gives us a couple of advantages:
- No need for +1 or reset to 0 logic
- We always know when a version is released
Now there are some restrictions to this, at least if you create a NuGet package:
- Version parts in NuGet are integers, so the max version is 2147483647
- Limit is 99 releases a day
- This logic will fail from the year 2148; this will be the new Y2K bug!
# 2147483647
# yyyyMMddrr
name: $(date:yyyyMMdd)$(rev:rr)
variables:
system_accesstoken: $(System.AccessToken)
projectName: 'name'
isMaster: $[eq(variables['Build.SourceBranch'], 'refs/heads/master')]
trigger:
- master
Reuse scripts
After writing and testing the scripts (details later in the blog) we put the scripts in another repository, so they are reusable
resources:
repositories:
- repository: repoName
type: git
name: TeamProjectName/repoName
Nobody is perfect
Sometimes a (new) developer forgets to add the convention. So we added two extra parameters to set the type of release when we run a pipeline manually.
parameters:
- name: buildConfiguration
type: string
default: 'Release'
version
- name: releaseNewVersion
type: boolean
default: false
- name: newVersionType
type: string
default: 'patch'
values:
- patch
- minor
- major
This is how it looks when manually starting a run
verion tag in git
Our scripts will write the new version as a tag to Git. This means the pipeline needs permission to push to the repository.
steps:
- checkout: self
displayName: "Git checkout with allow scripts to access the system token for pushing"
persistCredentials: true
clean: true
fetchDepth: 0
flow of scripts
Here you can see all the scripts we built and how we call them in the pipeline.
- template: scriptsFolderName/set-latest-version-in-variables.yml@repoName
- template: scriptsFolderName/update-version-variables.yml@repoName
parameters:
overruleNewVersion: ${{ parameters.ReleaseNewVersion }}
newVersionType: ${{ parameters.newVersionType }}
- template: scriptsFolderName/create-git-tag.yml@repoName
- template: scriptsFolderName/update-version-in-project-file.yml@repoName
parameters:
buildConfiguration: ${{ parameters.buildConfiguration }}
- template: scriptsFolderName/default-build.yml@repoName
parameters:
buildConfiguration: ${{ parameters.buildConfiguration }}
- template: scriptsFolderName/push-new-version.yml@repoName
parameters:
buildConfiguration: ${{ parameters.buildConfiguration }}
- powershell: |
$newVersion = "$($(Major)).$($(Minor)).$($(Patch))"
Write-Host "##vso[build.updatebuildnumber]$newVersion"
displayName: 'Rename pipeline to new version'
condition: and(succeeded(), eq(variables.isRelease, 'true'))
Setting the Latest Version
To set the latest version, we get the latest tag. When no tag is available, we use version 0.0.0.
When you implement this script but want to start with a higher version, just create that tag if it is not already available.
After setting the full version, we split it up into major, minor, and patch variables that we can update later in the pipeline.
#set-latest-version-in-variables.yml
steps:
- powershell: |
$latestVersion = git describe --tags --abbrev=0 2>$null
if ($latestVersion) {
echo "Current version: $($latestVersion)"
} else {
$latestVersion = "0.0.0"
echo "No tag was found. Using version 0.0.0 as a base."
}
$versionArray = $latestVersion -split "\."
$major = [int]$versionArray[0]
$minor = [int]$versionArray[1]
$patch = [int]$versionArray[2]
Write-Host "##vso[task.setvariable variable=Major]$major"
Write-Host "##vso[task.setvariable variable=Minor]$minor"
Write-Host "##vso[task.setvariable variable=Patch]$patch"
displayName: 'Get latest version and set variables'
condition: and(succeeded(), eq(variables.isMaster, 'true'))
Incrementing the Version Number
The normal flow will be that the run was triggered automatically and the convention was used, which means that the commit title starts with:
- fix = patch
- feat = minor
- anyType! = major
We do this with regex and also get other parts of the commit message. Who knows, maybe we can use them in the future. Some examples of how the regex will return the results.
After this, we just check the type of change and update the corresponding version type, where the patch will always be updated.
We added some extra checks for the manual run part that overrules the default logic.
#update-version-variables.yml
parameters:
overruleNewVersion: false
newVersionType: ''
steps:
- powershell: |
$latestCommitTitle = git log -n 1 --pretty=format:"%s"
#Normalize title
$pattern = "^merged pr \d+:\s*(.*)"
$normalizedTitle = $latestCommitTitle.ToLower()
if($latestCommitTitle.ToLower() -match $pattern) {
$normalizedTitle = $matches[1]
}
$pattern = "(?<type>\w+)(?<scope>(?:\([^()\r\n]*\)|\()?(?<breaking>!)?)(?<subject>:.*)?"
$normalizedTitle -match $pattern
echo "type: $($matches["type"])"
echo "scope: $($matches["scope"])"
echo "breaking: $($matches["breaking"])"
echo "subject: $($matches["subject"])"
$major = [int]$(Major)
$minor = [int]$(Minor)
$patch = $(Patch)
$changed = $false
if('${{ parameters.overruleNewVersion }}' -eq "true") {
echo '${{ parameters.newVersionType }}'
$changed = $true
if('${{ parameters.newVersionType }}' -eq "major") {
echo "breaking change"
$major += 1
$minor = 0
}
elseif('${{ parameters.newVersionType }}' -eq "minor"){
echo "minor change"
$minor += 1
}
else{
echo "patch change"
}
}
else {
if($matches["breaking"] -ne $null) {
echo "breaking change"
$major += 1
$minor = 0
$changed = $true
}
elseif($matches["type"] -eq "feat"){
echo "minor change"
$minor += 1
$changed = $true
}
elseif($matches["type"] -eq "fix"){
echo "patch change"
$changed = $true
}
}
echo "Changed: $($changed)"
if($changed) {
Write-Host "##vso[task.setvariable variable=Major]$major"
Write-Host "##vso[task.setvariable variable=Minor]$minor"
Write-Host "##vso[task.setvariable variable=Patch]$(Build.BuildNumber)"
Write-Host "##vso[task.setvariable variable=IsRelease]true"
}
displayName: 'Check change type and update version'
condition: and(succeeded(), eq(variables.isMaster, 'true'))
Creating the Git Tag
When there was a change that needed a release, the isRelease variable will be set to true in the last script, and when it is, we just create a new Git tag.
This will only work if you set the Git permission in the pipeline with persistCredentials: true
#create-git-tag.yml
steps:
- powershell: |
git tag "$($(Major)).$($(Minor)).$($(Patch))"
displayName: 'Create git tag'
condition: and(succeeded(), eq(variables.IsRelease, 'true'))
Optional: Updating project file
We develop libraries/NuGet packages in .NET, so the last step for us will be an update to the .csproj file. This will ensure that when we build, the version of the NuGet package will be correct.
As I have written earlier, NuGet uses an integer per version type. But if we look deeper, we find something interesting. The type is different per tag:
- Packageversion: int32
- version: int16 = 32.767
#update-version-in-project-file.yml
parameters:
projectName: ''
steps:
- powershell: |
$newVersionTag = git describe --tags --abbrev=0
echo "New version tag: $($newVersionTag)"
$projectName = '$(projectName)'
$pattern = '^(.*?(<PackageVersion>|<Version>).*?)([0-9].[0-9]{1,2}.)(([0-9]{1,2})(-rc([0-9]{2}))?)(.*?)$'
$path = if (Test-Path -Path $projectName) {'{0}\*' -f $projectName } else { '*' }
$ProjectFileName = '{0}.csproj' -f $projectName
$ProjectFiles = Get-ChildItem -Path $path -Include $ProjectFileName -Recurse
foreach ($file in $ProjectFiles)
{
echo "file: $($file)"
echo "file path $($file.PSPath)"
(Get-Content $file.PSPath) | ForEach-Object{
if($_ -match $pattern){
'{0}{1}{2}' -f $matches[1],$newVersionTag,$matches[8]
} else {
# Output line as is
$_
}
} | Set-Content $file.PSPath
}
displayName: 'Update version in project file'
condition: and(succeeded(), eq(variables.IsRelease, 'true'))
I hope this helps you guys with automating your versioning process and streamlining your releases. Feel free to reach out if you have any questions or need further assistance. Happy coding! 😊
Top comments (0)