DEV Community

Frits Becker
Frits Becker

Posted on

Auto update version

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This is how it looks when manually starting a run
manually running a pipeline

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
Enter fullscreen mode Exit fullscreen mode

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'))
Enter fullscreen mode Exit fullscreen mode

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'))
Enter fullscreen mode Exit fullscreen mode

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.

regex 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'))
Enter fullscreen mode Exit fullscreen mode

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'))
Enter fullscreen mode Exit fullscreen mode

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'))
Enter fullscreen mode Exit fullscreen mode

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)