Stage failing in Azure DevOps pipeline

How to avoid global.json version conflicts in Azure DevOps

This post was most recently updated on March 13th, 2023.

4 min read.

In Azure DevOps, you sometimes have a project that’s being built with a certain .NET version. It’ll require that particular SDK version, and most typically that is defined in the global.json file.

However, if you also need to install a certain .NET tool, running dotnet tool install MyBuildTool will fail. And it’ll fail before it actually tries to install the version of the tool you wanted!

This article explains how to work around the .NET version conflicts when using Azure DevOps, your custom tooling (installed by dotnet tool install) and global.json

Problem

The error message you are running into looks like this:

A compatible SDK version for global.json version: [5.0.100] from [D:\...\global.json] was not found Did you mean to run dotnet SDK commands? Please install dotnet SDK from: https://go.microsoft.com/fwlink/?LinkID=798306&clcid=0x409

But whatever version of the tooling you need, you can’t install it because it’s incompatible with the global.json version!

So this becomes kind of a chicken-and-egg problem. If you want to use your tooling and build your project with the .NET SDK version set in the global.json, you need to either always keep the tooling in sync (which at least for us was NOT feasible), or you’ll need to find out a way to ignore the global.json file in this particular regard.

Solution

Well, of course, the solution is to ignore global.json. But how?

Luckily, I’m not the first person to run into this. This discussion on GitHub talks about the same issue: https://github.com/dotnet/sdk/issues/10311

It has this script sample:

mv global.json disabled-global.json
[Some dotnet command allowed any sdk, for example dotnet tool install]
mv disabled-global.json global.json
[Some dotnet command not allowed any sdk, for example dotnet build]

So, in short, the solution goes somewhat like this:

Time needed: 10 minutes

How to allow dotnet commands to bypass the global.json version check?

  1. Use your particular SDK version

    Set Azure DevOps pipeline to ignore the global.json for a while:

      - task: UseDotNet@2
        displayName: Use .NET Core sdk required for project $(Build.DefinitionName)
        inputs:
          version: $(SdkVersion)
          useGlobalJson: false

  2. Rename global.json

    If you rename global.json to anything else, it won’t be checked anymore! In an Azure DevOps task, you can use mv to “move” files or directories – including renaming your global.json!

  3. Run your commands

    Now you can run your dotnet commands ignoring global.json! In my case, I’d install build tooling that was “incompatible” with the global.json file.

  4. Rename the file back to global.json

    Depending on what you’re doing in your next steps, this might or might not be required – but it’s probably a good idea to do if you actually need the SDK version from the global.json to matter for the rest of your build!

  5. (OPTIONAL) You might want to then set the .NET SDK Version to be the one in global.json

    Something like this in your pipeline:

      - task: UseDotNet@2
        displayName: 'Use .Net Core sdk'
        inputs:
          useGlobalJson: true


For the TL;RD -version, just jump here: my YAML


How to use this in practice?

I adjusted the script I found on GitHub for my use like this:

# This was my initial implementation - don't use this!
- task: PowerShell@2
  displayName: Install Build Tools
  inputs:
    targetType: inline
    pwsh: true
    failOnStderr: true
    script: |
      # https://github.com/dotnet/sdk/issues/10311#issuecomment-1254912466

      try {
        try {
          mv global.json disabled-global.json
        } catch {
          Write-Host "Moving global.json failed, but we don't care"
        }

        dotnet tool install --global MyBuildTool --version $(CliVersion)

      } catch {
        Write-Host "Installing Tooling failed"
        Exit 1;
      } finally {
        try {
          mv disabled-global.json global.json
        } catch {
          Write-Host "Moving global.json back failed, but we don't care"
        }
      }

      Exit 0;

But this script sample is annoying as it uses mv – a bash command – to move stuff around. It kind of works, sure, but the try-catch-block around mv will not end up in the catch-block.

Why? Well, PowerShell try-catch works a bit differently than what you maybe expect: You’ll only end up in catch if a terminating error gets thrown in the try-block! And mv failing to move something will not throw in PowerShell.

So I wanted to turn it into a pure PowerShell solution instead. PowerShell has a Move-Item commandlet, which you can luckily use. This turned into something like the below:

My current Azure DevOps PowerShell task to run dotnet commands disregarding the .NET SDK version in global.json

# A far better implementation that actually catches the exceptions (and simply logs them, but you can change this if you need to)

# First we set the .NET SDK version we want to use
- task: UseDotNet@2
  displayName: Use .NET Core sdk required for project $(Build.DefinitionName)
  inputs:
    version: $(SdkVersion)
    useGlobalJson: false

- task: PowerShell@2
  displayName: Install Build Tools
  inputs:
    targetType: inline
    pwsh: true
    failOnStderr: false # We might get the global.json move exceptions, but we can ignore them
    script: |
      # https://github.com/dotnet/sdk/issues/10311#issuecomment-1254912466
      # https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/move-item?view=powershell-7.2

      try {
        try {
          Move-Item -Path global.json -Destination disabled-global.json
        } catch {
          Write-Host "Moving global.json failed, but we don't care"
        }

        dotnet tool install --global MyBuildTool --version $(CliVersion)

      } catch {
        Write-Host "Installing Tooling failed"
        Exit 1;
      } finally {
        try {
          Move-Item -Path disabled-global.json -Destination global.json
        } catch {
          Write-Host "Moving global.json back failed, but we don't care at this point"
        }
      }

      Exit 0;

# Now that our stuff with a particular SDK version is done, we can forget about ignoring the global.json again
- task: UseDotNet@2
  displayName: 'Use .Net Core sdk'
  inputs:
    useGlobalJson: true

And that’s that – works nicely for us, hope it’s useful to you too!

References

mm
0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
most voted
newest oldest
Inline Feedbacks
View all comments