Always ship Azure DevOps.

Automatically get version number from project dependencies in Azure DevOps

5 min read.

This article expands on my earlier article on automatically figuring out versioning in an Azure DevOps Pipeline. In the other article, you’d add Major and Minor versions as variables, and Patch (the last part of an x.y.z version scheme) would be incremented automatically.

In this one, I’m describing how to get Major and Minor from the dependencies of your project – this will be incredibly convenient, if you’re, for example, publishing Docker images, NPM or NuGet packages as an extension or auxiliary library to some other projects or products.

Background

Let’s say you want to keep up with the versioning of a dependency you have in your project. This could be .NET versioning if you know your project will have a dependency on Microsoft.Extensions.Configuration.Abstractions, for example. Or this could be your proprietary library versions like, say, a NuGet package called Contoso.Libraries.Core.

And let’s also say you want to keep your project’s deliverables (such as packages, DLL libraries, and Docker container images…) all in sync with the major and minor versions of the dependencies, to make it really straightforward to track compatibility, for example.

I’m not saying that’s exactly what I’m solving at work with this… But it’s pretty close. Just another way to make my colleagues lives a bit easier – since it might not be me who needs to figure out which container versions to deploy to Azure Kubernetes Service or some such. :)

Problem

As far as I know, unless your dependencies are built in the same Azure DevOps Team Project, you can’t “share version numbers” between pipelines. Variable Groups (under “Library”) are great if you have your pipelines in just one Team Project, but are not easily used between pipelines in different Team Projects or Organizations. And by “not easily” I mean you can still call the API to fetch them, but that’s hardly easy.

But since in a case like this you probably share dependencies (or some projects depend on your other projects), you can “sync” the versions with some PowerShell magic.

Let’s get on it, then.

Solution

In the short guide below, I’ll go through the steps on a high level. Practical code samples are given further down on the page – I’ll link to them from the steps, where relevant.

Time needed: 30 minutes

How to read the semantic version from your project’s dependencies in Azure DevOps?

  1. Define dependencies based on which you want to get the semver

    This is the basis of automatically figuring out the Major and Minor versions of your project/package. Which dependencies should you look at? In my example, I’m taking Contoso.Libraries, but yours might be Microsoft.Extensions or something else.

  2. Go through your dependencies

    You could go through your .csproj (to find NuGet dependencies), package.json (to find your NPM packages) or some other file – my example below goes through your .csproj file, and finds the dependency you’ve defined in step 1.

  3. Split the version number in parts

    Next we find the version number string from the line and split the version into Major, Minor and Patch. We know both NuGet and NPM packages follow semver-compatible versioning (Major.Minor.Patch), so we can just split the string with “.” as the delimiter.

    This is done in the same sample I linked above.

  4. Store the Major and Minor versions in variables

    We can discard Patch (as that should increment per our build and we’ll calculate it in the next step), but let’s store Major and Minor for our later use, like I’m doing here.

  5. Use counter() to calculate Patch

    counter() is a built-in Azure DevOps function that’ll take a seed – such as your Major and Minor versions – and return the next increment, giving you a unique, incremental Patch value (click for a longer sample).

    A short sample of how to do this:
    variables:
    - name: Patch
    value: $[counter(format('{0}.{1}', $(Major), $(Minor)), 0)]

  6. Form all of the version parts together

    And finally, we’ll combine everything to create a semver-compatible version number string and store it in a variable :)
    variables:
    - name: CurrentVersion
    value: $(Major).$(Minor).$(Patch)

… and if those snippets are a bit unclear, maybe the longer ones below will offer some clarity! Or just confuse everyone even more – that’s also a real possibility. If the latter happens, do let me know in the comments -section below!

Steps in more detail

Below, we’ll go through the steps with any PowerShell or YAML in further detail. Note, that in my case the values are being passed between stages, which affects the implementation.

Find major and minor versions from the .csproject files

This sample goes through all .csproj(ect) files which are probably not what you actually want to do – you’d either want to break when you’ve found what you want or (for example) pick the largest value of all matches. But since I don’t know your specifics, I won’t try to tell you what to do :)

The sample below should work nicely if your $projectFiles is an array of absolute or relative paths to .csproj files in your solution.

$dependencyName is the pattern against which you want to check for your dependencies’ versions. It could be Microsoft.Extensions – or if you have some internal packages, like I do, your internal namespace.

$dependencyName = "Contoso.Libraries."

$projectFiles | Foreach-Object { 
  Write-Host "Path to .csproj file: " $_.Path
  $subPath = Split-Path -Path $_.Path

  try {
    $content = Get-Content $_.Path
    $content | foreach { if ($_ -match $dependencyName) { if ($_ -match "[0-9]+\.[0-9]+\.[0-9]+") { Write-Host $matches[0]; $semverFound = $true } } }

    if ($matches.Count -eq 0) {
      continue;
    }

    $semver = $matches[0].Split(".") # just take the first :)

    $major = $semver[0]
    $minor = $semver[1]

    # Now we overwrite the pipeline/variable group minor & major variables with what we have here
    Write-Host "##vso[task.setvariable variable=Major;]$major"
    Write-Host "##vso[task.setvariable variable=Minor;]$minor"

    Write-Host "Stored major: $major and minor: $minor !" -ForegroundColor Blue

    Write-Host "GREAT SUCCESS!! Now go get some coffee." -ForegroundColor Green

    Exit 0;
  }
  catch {
    Write-Host "Error: $($_.Exception.Message)"
  }

After having run this, we now have Major and Minor variables set with values matching your dependencies! You can use it in the next steps – and if you set them as output variables (see below), you can even use them in the following jobs and stages (as long as you map them in the job or stage definition – shown even further below):

Passing variable values between jobs and stages

# Setting your variables to be output variables (that you can then
# access in later jobs and stages, and not just steps in the same job)
Write-Host "##vso[task.setvariable variable=Major;isOutput=true;]$major"

I guess you could even combine Major & Minor and calculate Patch at this point based on them… But for whatever reason, I ended up passing the 2 between jobs and stages instead.

Calculate your “Patch” and combine values

Anyway – then we’ll want to combine the logic to automatically count the Patch for your version number – like in the sample below (I’m setting them as variables for the next stage):

variables:
# Get the Major output variable from previous stage
- name: Major
  value: stageDependencies.YourStage.YourJob.outputs['outputVersion.Major']
# Get the Minor output variable from previous stage
- name: Minor
  value: stageDependencies.YourStage.YourJob.outputs['outputVersion.Minor']
# Calculate Patch variable (can't use Major and Minor as they're
# evaluated after counter has run)
- name: Patch
  value: $[counter(format('{0}.{1}', stageDependencies.YourStage.YourJob.outputs['outputVersion.Major'], stageDependencies.YourStage.YourJob.outputs['outputVersion.Minor']), 0)]
# Finally, combine Major, Minor and Patch
- name: CurrentVersion
  value: $(Major).$(Minor).$(Patch)

And now you can use the $(CurrentVersion) variable in your jobs like this:

- task: NuGetCommand@2
  inputs:
    command: pack
    packagesToPack: '**/[projectname].csproj.nuspec'
    versioningScheme: byEnvVar
    versionEnvVar: CurrentVersion
    packDestination: '$(Build.ArtifactStagingDirectory)\nugetpkg'

If you were running everything in just one job, you wouldn’t necessarily need the complicated references to output variables. But I suspect few people can fit their whole pipeline in just one job :)

Anyway – as usual, feedback is welcome. Hope this is useful for others!

Reference

mm
5 2 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments