Skip to content
compiler.dev

.NET GitHub Actions Workflow Template

Last updated

This is a working .github/workflows/ci.yml for a .NET solution. It installs the SDK, caches the NuGet package folder, restores once, builds in Release mode and runs the tests without rebuilding.

The workflow

name: CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

concurrency:
  group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
  DOTNET_NOLOGO: "true"
  DOTNET_CLI_TELEMETRY_OPTOUT: "true"
  DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "true"

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: 9.0.x

      - uses: actions/cache@v4
        with:
          path: ~/.nuget/packages
          key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props', '**/packages.lock.json') }}
          restore-keys: |
            nuget-${{ runner.os }}-

      - run: dotnet restore
      - run: dotnet build --no-restore --configuration Release
      - run: dotnet test --no-build --configuration Release --verbosity normal

What the non-obvious parts do

  • dotnet-version: 9.0.x takes the latest 9.0 patch. Use the major your project targets, or global-json-file: global.json to follow the SDK pinned in your repo.
  • The NuGet cache step stores ~/.nuget/packages. setup-dotnet has a cache: true option, but it requires a packages.lock.json lock file, so an explicit actions/cache step is more broadly usable. Key it on project files so adding a package makes a new cache.
  • restore, then --no-restore, then --no-build. Each step does one job. Without the flags, build and test would each repeat the earlier steps, wasting time.
  • Environment variables. The three DOTNET_ settings remove the welcome banner, telemetry and first-run work that clutters logs.
  • Release configuration. Tests run against what you ship. Use Debug only if tests rely on debug symbols.
  • Linux runner. .NET runs cross-platform, and Linux minutes are the cheapest.

Variants

SQL Server service container

    services:
      mssql:
        image: mcr.microsoft.com/mssql/server:2022-latest
        env:
          ACCEPT_EULA: "Y"
          MSSQL_SA_PASSWORD: "Ci_Passw0rd!"
        ports:
          - 1433:1433
        options: >-
          --health-cmd "/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P Ci_Passw0rd! -Q 'SELECT 1'"
          --health-interval 10s
          --health-retries 10

Then set the connection string through an environment variable. The SQL Server image needs more memory and time to start than Postgres. A Postgres service, as in the Django template, uses the same structure.

Multiple target frameworks

Add a matrix with dotnet-version: [8.0.x, 9.0.x]. See matrix builds.

Code coverage

Add --collect:"XPlat Code Coverage" to dotnet test and upload the results as an artifact.

Publish and deploy

Add a second job with needs: build on main that runs dotnet publish -c Release -o out, and deploy using OIDC.

Windows or macOS

Add them to a matrix for desktop or cross-platform libraries. Windows costs more per minute, so use it where needed.

Run it on faster machines

Change one line:

    runs-on: [self-hosted, Linux, compiler-ci]

The job runs on a fresh 8 vCPU machine from compiler.dev, with automatic fallback to your original runner. compiler.dev's benchmark of React with ASP.NET Core and SQL Server went from 3m34s to 1m41s; see benchmarks. More in speed up .NET on GitHub Actions, and prices on pricing.

FAQ

Why not use setup-dotnet's cache option?

It only works when you use NuGet lock files. If you do, cache: true is simpler.

Do I need to restore separately?

Not strictly, but a separate restore makes the cache effect visible in the logs and lets you use --no-restore afterward.

How do I run tests in parallel?

xUnit and NUnit parallelise within a project. For more, use test sharding by project.

What about the SQL Server password rules?

The sa password must meet complexity rules or the container exits. The example password does.