dotnetup: A new way to manage .NET in your GitHub Actions workflows

Microsoft just announced a public preview of dotnetup. It is a toolchain manager for .NET. You can read the official announcement from the .NET blog.
The announcement is mostly written from a developer point of view. I work a lot with CI/CD pipelines so I wanted to look at it from the DevOps side. Can this tool make our CI/CD workflows better?
What is dotnetup?
As stated in the devblogs, if you have used rustup or nvm, you already know the idea. Dotnetup installs and updates .NET SDKs and runtimes. Everything goes to the user folder, so you don’t need admin rights or sudo.
Here are the main features:
- Same commands on every platform
- Side-by-side SDKs, runtimes, previews and daily builds
- Tracking of channels like
10.0orlatest - Support for the
global.jsonfile
Today most of us use the actions/setup-dotnet action in GitHub Actions. It works well. It uses the dotnet-install scripts under the hood. The .NET team has said that they want to bring dotnetup into GitHub Actions and Azure DevOps. That could replace parts of those implementations.
Dotnetup is still a preview and the official integration is not ready yet. You can already use it today by calling it directly in your workflow. Microsoft even has a GitHub Actions example in the documentation.
For the DevOps!
There are a few things I like about this approach:
One source of truth. Dotnetup can read the SDK version from your global.json. Developers and the build agent use the same version. No more “works on my machine” because of different SDK feature bands.
Daily and preview builds. Do you want to know if your app still builds with the next .NET version? You can add a nightly job that installs the daily SDK. It takes one command.
Self-hosted runners. On a shared self-hosted runner you often can’t touch the system-wide installation. Dotnetup lives in the user folder, so every job can have what it needs.
Verification. The install scripts don’t manage what you have installed. Dotnetup validates signatures and checksums of the downloads. It also tracks what is installed and cleans up the old stuff.
Sample 1: Build with the SDK from global.json
Let’s start with a simple build workflow. The SDK version comes from the global.json file in the repository:
{
"sdk": {
"version": "10.0.103",
"rollForward": "latestPatch"
}
}
With this file dotnetup tracks the 10.0.1xx feature band. It installs the latest patch version of that band.
And here is the workflow:
name: Build
on:
push:
branches: [ main ]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dotnetup
run: |
curl -fsSL https://aka.ms/dotnetup/get-dotnetup.sh | bash
echo "$HOME/.dotnetup" >> "$GITHUB_PATH"
- name: Install SDK from global.json
run: dotnetup sdk install --no-progress --interactive false
- name: Build
run: dotnetup dotnet build --configuration Release
- name: Test
run: dotnetup dotnet test --configuration Release --no-build
The install script puts dotnetup into the ~/.dotnetup folder. It does not change the PATH. That is why the workflow adds the folder to GITHUB_PATH. Without this line the next steps can’t find the dotnetup command.
I use the dotnetup dotnet command to run the build. It sets PATH and DOTNET_ROOT only for that command. Be careful with this. If another step calls plain dotnet, it will not use the SDK installed by dotnetup.
The --no-progress and --interactive false options keep the logs clean and make sure that nothing waits for user input. This is the same pattern as in the official automation guide.
Sample 2: Test against the new .NET version
This nightly build check allows you to check how your app builds against the latest .NET version. Run a scheduled workflow which builds your project with the daily SDK. When Microsoft ships a breaking change, you find it easily.
name: Nightly .NET daily build check
on:
schedule:
- cron: "0 3 * * *"
workflow_dispatch:
jobs:
daily:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dotnetup
run: |
curl -fsSL https://aka.ms/dotnetup/get-dotnetup.sh | bash
echo "$HOME/.dotnetup" >> "$GITHUB_PATH"
- name: Install daily SDK
run: dotnetup sdk install daily --no-progress --interactive false
- name: Show installed versions
run: dotnetup list
- name: Build and test
run: |
dotnetup dotnet --version
dotnetup dotnet build
dotnetup dotnet test
You can also use dotnetup sdk install preview if you only want the preview releases. Daily builds can be unstable. That is why I recommend to run this as a separate workflow and not as a part of the main pipeline.
Sample 3: Matrix build with many versions
Do you ship a library that supports many .NET versions? A matrix makes this easy:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, macos-latest ]
channel: [ "9.0", "10.0", "preview" ]
steps:
- uses: actions/checkout@v4
- name: Install dotnetup
run: |
curl -fsSL https://aka.ms/dotnetup/get-dotnetup.sh | bash
echo "$HOME/.dotnetup" >> "$GITHUB_PATH"
- name: Install SDK
run: dotnetup sdk install ${{ matrix.channel }} --no-progress --interactive false
- name: Test
run: dotnetup dotnet test
The same dotnetup commands work in both operating systems. Windows runners just needs the PowerShell bootstrap script instead of the bash one:
irm https://aka.ms/dotnetup/get-dotnetup.ps1 | iex
Daily builds are not code-signed. Use them only for short-lived testing like this.
Keeping the global.json up to date
Dotnetup has one more trick. You can ask it to write the new version back to the global.json file:
dotnetup sdk update --update-global-json
Think about a scheduled workflow that runs this command and opens a pull request if the file changed. Then your monthly (bi-monthly, quarter update… anyway) servicing update is just a PR review.
Things to remember
- Dotnetup is a public preview. Commands and behavior can change.
- The bootstrap script installs the latest preview build of dotnetup by default. Use
--quality dailyif you want the daily build. - Don’t run several install or update commands against the same manifest at the same time. Use a separate
--manifest-pathfor parallel jobs on the same machine. - For production workflows the actions/setup-dotnet action is still the safe choice.
- Windows system-level installs are not supported yet.
Summary
Dotnetup is not going to replace your current setup today. But it gives a very nice way to handle .NET versions in CI. The daily build check alone is worth trying. When the official GitHub Actions and Azure DevOps integration arrives, I expect it to be a really easy switch.