Azure DevOps - Always Be Shipping!

How to find out dependency versions in your .csproj files using PowerShell?

5 min read.

This article explains our solution to a very particular use case. We needed to go through .csproj files, find certain dependencies from them, and then figure out their versions. And all of this needed to happen in an Azure DevOps pipeline.

But when would this be helpful? Well, hear me out, because boy do I have a case to describe!

Background

We wanted to have automatic versioning for our builds in Azure DevOps. We use the standard and boring semver model – i.e. Major.Minor.Patch – for any artifacts we publish. Like docker images.

Well, especially for docker images!

The versioning of our builds and the resulting images should be based on the Major and Minor versions coming from a certain subset of dependencies that our other project files would always have – them being core components that other projects extend.

And Patch should just increment on each build. The resulting image(s) will then be committed to various Azure Container Registries to be used for all kinds of purposes.

I’ve posted before about how to automatically figure out the Patch version based on Major and Minor versions – see that post here:

Fun with Azure DevOps NuGet package versioning!

With that knowledge, we already know how to get the Patch -part of the version string. Except we can’t use it as a pipeline variable, as a pipeline variable is evaluated too early in the run and we only know the Major and Minor after running some magic PowerShell to figure it out (if we want to make it dynamic, that is!)

Solution

I actually initially tried running npm list packagename or similar commands for my projects, but that requires npm install to have run – which takes a while unless I execute my step inside my build stage, which I didn’t want to do (to generalize my build templates as much as possible).

It also returns multiple lines as a response, each containing much more than just a dependency version – so I would have had to do a lot of string manipulation in any case (and God knows the regex for that would be beautiful!) so I ended up opting to investigate a simple text file – my .csproj – instead.

And anyone who’s taken a look into a .csproj file knows that while they might not be the easiest to read (especially the older formats), but they do have the dependencies in them.

In the sample below, we’ll be looking for Contoso.Core to capture Major and Minor version from.

Time needed: 15 minutes

How to read Major and Minor version from dependencies in a pipeline in Azure DevOps?

  1. Create a new PowerShell task for figuring out the variables

    In order to find the references to our dependencies, we’ll need a PowerShell task that’ll find the .csproj files from your checked-out source and go through the contents.

    So – that’s what we start with. A PowerShell task in your pipeline some time after your checkout task. In YAML, it’ll look something like below:
      - checkout: $(your-repo)

      - task: PowerShell@2
        displayName: Get new semver from files
        inputs:
          targetType: inline
          pwsh: true
          script: |
    Write-Host "Time for some PowerShell magic!"

  2. Figure out where your .csproj file is

    Now that you have your source checked out, it’ll be located in $(Build.SourcesDirectory). You could, for example, find your .csproj files by running this:
    cd $(Build.SourcesDirectory)
    $projects = Get-ChildItem *.csproj -Recurse


    In this guide I presume we don’t know which of the .csproj files has the dependencies we are interested in, so we’ll need them all for now.

  3. Read the dependency version from the .csproj file

    Since we don’t know which .csproj file has the dependencies we’re interested in, we’ll go through them until we find what we want!

    Somewhat like below:
    $projects | Foreach-Object {
      Write-Host "Path to .csproj file: " $_
      $subPath = Split-Path -Path $_

      $content = Get-Content $_

      $content | foreach { if ($_ -match "Contoso.Core") { if ($_ -match "[0-9]+\.[0-9]+\.[0-9]+") { Write-Host $matches[0]; $semverFound = $true } } }
      if ($matches.Count -eq 0) {
        continue;
      }

      $semver = $matches[0].Split(".")

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

    # At this point, we've got the variables in the local task, but not beyond that scope.
    # To make them available for the rest of the job, you can do this:
    Write-Host "##vso[task.setvariable variable=Major;]$major"
    Write-Host "##vso[task.setvariable variable=Minor;]$minor"


    # If you’ll need them in other jobs and stages, see steps 4

      break;
    }


  4. (OPTIONAL) If you’re using pipeline variables, you need a new task for them to be updated

    One important point to note is that the values of pipeline variables are not updated during the execution of the task – so if you want to verify what was set as a value to your Major and Minor pipeline variables, you’ll need a new task for it. It’ll have the updated values.

    So $(Major) and $(Minor) should be undefined for the rest of your script in step 3. But if you have a new task (in the same job), they should work just fine!

    The PowerShell can now be something as simple as:
      - task: PowerShell@2
        displayName: Log new Major and Minor versions
        name: outputVersion
        inputs:
          targetType: inline
          pwsh: true
          script: |
            Write-Host "The version is now: $(Major).$(Minor)"

  5. (OPTIONAL) Handle the complexity around multiple jobs and/or stages

    If you have a multi-job or multi-stage pipeline (as I believe most of us do), the updated values are not going to be available for the rest of the jobs or stages, as they’ll be freshly initiated on new virtual machines that are unaware of the pipeline variable value changes.

    Yeah, this adds a lot of convolutedness to the process, but what are you going to do…

    So if you don’t want to run the rest of your steps in the same job you set the new values in, what can you do?

    Well, we’ll need to set our Major and Minor to be output variables, which will be available for later stages and jobs to reference.

      - task: PowerShell@2
        displayName: Output new Major and Minor versions
        name: outputVersion
        inputs:
          targetType: inline
          pwsh: true
          script: |
            Write-Host "Exporting the Major & Minor semantic version as output to be used in later stages"
            Write-Host "##vso[task.setvariable variable=Major;isOutput=true;]$Major"
    Write-Host "##vso[task.setvariable variable=Minor;isOutput=true;]$Minor"

Is this a perfect solution? No, it’s in fact pretty hacky and not as flexible as I’d like. Anything that makes you have extra steps and/or jobs and stages is annoying.

But it DOES work really nicely for our use case and automated all of our docker container versions in one go. :)

References

mm
0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments