The .NET Build & Test workflow is a comprehensive, reusable GitHub Actions workflow for building, testing, and deploying .NET applications. It supports web APIs, libraries, Blazor WebAssembly, microservices, and console applications with extensive configuration options.
- ✅ Multi-Platform Support: Linux, Windows, macOS
- ✅ .NET 8+ LTS: Optimized for .NET 8 (LTS) with support for 6.0+
- ✅ Project Types: Web APIs, Class Libraries, Blazor, Microservices, Console Apps
- ✅ Matrix Builds: Parallel builds across OS, .NET versions, configurations
- ✅ Docker Support: Build and push container images
- ✅ NuGet Packages: Create and publish NuGet packages
- ✅ Code Coverage: Multiple coverage formats with thresholds
- ✅ Code Analysis: Static analysis and quality checks
- 🐳 Container Optimization: Multi-stage builds, Alpine support
- 📦 Package Management: Symbol packages, source linking
- 🔍 Testing: Unit, integration, E2E test support
- 📊 Reporting: Test results, coverage reports, code metrics
- 🚀 CI/CD Ready: Artifact management, deployment outputs
- ⚡ Performance: Dependency caching, parallel execution
name: Build My Project
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
uses: your-org/automation-templates/.github/workflows/dotnet-build.yml@main
with:
dotnet-version: '8.0.x'
project-path: 'src/MyProject.csproj'
run-tests: true
upload-artifacts: truejobs:
build:
uses: your-org/automation-templates/.github/workflows/dotnet-build.yml@main
with:
# .NET Configuration
dotnet-version: '8.0.x'
configuration: 'Release'
# Project Settings
project-path: 'src/MyApi.csproj'
working-directory: '.'
# Build Options
treat-warnings-as-errors: true
verbosity: 'minimal'
# Testing
run-tests: true
collect-coverage: true
coverage-threshold: 80
# Code Analysis
run-code-analysis: true
analysis-level: 'recommended'
# Publishing
publish: true
self-contained: false
# Docker
build-docker: true
dockerfile-path: './Dockerfile'
# NuGet Package
create-package: true
push-to-nuget: true
secrets:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USER }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASS }}| Input | Description | Default | Options |
|---|---|---|---|
dotnet-version |
.NET SDK version(s) | 10.0.x |
8.0.x, 9.0.x, 10.0.x |
global-json-file |
Path to global.json | '' |
File path |
dotnet-quality |
SDK quality level | '' |
daily, signed, validated, preview, ga |
| Input | Description | Default |
|---|---|---|
project-path |
Path to project/solution | . |
working-directory |
Working directory | . |
configuration |
Build configuration | Release |
| Input | Description | Default |
|---|---|---|
build-args |
Additional build arguments | '' |
restore-args |
Additional restore arguments | '' |
verbosity |
Logging verbosity | normal |
treat-warnings-as-errors |
Treat warnings as errors | false |
| Input | Description | Default |
|---|---|---|
snk-file-path |
Path where SNK key should be created (relative to working-directory) | '' |
The snk-file-path must match the AssemblyOriginatorKeyFile setting in your .csproj or Directory.Build.props. The workflow decodes DOTNET_SIGNKEY_BASE64 secret and writes it to this path before building.
PR Builds: When
snk-file-pathis configured but theDOTNET_SIGNKEY_BASE64secret is not available (e.g. pull requests from forks), the workflow automatically disables assembly signing via MSBuild overrides (-p:SignAssembly=false). A warning annotation is emitted so reviewers can see that signing was skipped. The build will succeed but assemblies will not be strong-name signed.To suppress the warning entirely, only pass
snk-file-pathon non-PR events:snk-file-path: ${{ github.event_name != 'pull_request' && 'build/MyLibrary.snk' || '' }}
| Input | Description | Default | Examples |
|---|---|---|---|
runtime |
Target runtime | '' |
linux-x64, win-x64, osx-x64 |
self-contained |
Self-contained deployment | false |
true, false |
| Input | Description | Default |
|---|---|---|
run-tests |
Run unit tests | true |
test-filter |
Test filter expression | '' |
test-args |
Additional test arguments | '' |
test-logger |
Test logger format | trx;LogFileName=test-results.trx |
| Input | Description | Default | Options |
|---|---|---|---|
collect-coverage |
Collect coverage | false |
true, false |
coverage-type |
Coverage format | cobertura |
cobertura, opencover, coverlet |
coverage-threshold |
Minimum coverage % | 0 |
0-100 |
coverage-exclude |
Exclusions | '' |
Comma-separated |
| Input | Description | Default | Options |
|---|---|---|---|
run-code-analysis |
Run analysis | false |
true, false |
analysis-level |
Analysis level | recommended |
none, default, minimum, recommended, all |
| Input | Description | Default |
|---|---|---|
publish |
Publish application | false |
publish-args |
Additional publish arguments | '' |
output-directory |
Output directory | ./publish |
| Input | Description | Default |
|---|---|---|
create-package |
Create NuGet package | false |
package-version |
Package version | '' |
include-symbols |
Include symbols | true |
include-source |
Include source | false |
push-to-nuget |
Push to NuGet | false |
nuget-source |
NuGet source URL | https://api.nuget.org/v3/index.json |
| Input | Description | Default |
|---|---|---|
build-docker |
Build Docker image | false |
dockerfile-path |
Dockerfile path | ./Dockerfile |
docker-image-name |
Image name | '' |
docker-registry |
Registry URL | '' |
| Input | Description | Default |
|---|---|---|
upload-artifacts |
Upload artifacts | true |
artifact-name |
Artifact name | dotnet-build |
artifact-path |
Artifact path pattern | '' |
artifact-retention-days |
Retention days | 30 |
| Input | Description | Default | Options |
|---|---|---|---|
runs-on |
Runner to use | ubuntu-latest |
String or JSON array (see below) |
timeout-minutes |
Job timeout | 30 |
Minutes |
The runs-on parameter supports both GitHub-hosted and self-hosted runners:
# GitHub-hosted (string)
runs-on: 'ubuntu-latest'
runs-on: 'windows-latest'
# Self-hosted (JSON array)
runs-on: '["self-hosted", "linux"]'
runs-on: '["self-hosted", "linux", "docker"]'
runs-on: '["self-hosted", "Windows", "vs2022"]'See Self-Hosted Runner Documentation for details.
| Input | Description | Default |
|---|---|---|
enable-matrix |
Enable matrix builds | false |
matrix-os |
OS matrix (JSON) | ["ubuntu-latest"] |
matrix-dotnet |
.NET matrix (JSON) | ["8.0.x"] |
| Secret | Description | Required |
|---|---|---|
NUGET_API_KEY |
NuGet API key | When pushing packages |
DOCKER_USERNAME |
Docker username | When pushing images |
DOCKER_PASSWORD |
Docker password | When pushing images |
CODECOV_TOKEN |
Codecov token | For coverage upload |
SONAR_TOKEN |
SonarCloud token | For code analysis |
DOTNET_SIGNKEY_BASE64 |
Base64-encoded SNK key for assembly signing | When snk-file-path is set |
| Output | Description | Example |
|---|---|---|
version |
Application/package version | 1.2.3 |
test-results |
Path to test results | /tmp/test-results |
coverage-report |
Path to coverage report | /tmp/coverage.xml |
package-path |
Path to NuGet package | /tmp/packages/lib.1.2.3.nupkg |
docker-image |
Docker image tag | myapp:1.2.3 |
jobs:
build-api:
uses: your-org/automation-templates/.github/workflows/dotnet-build.yml@main
with:
project-path: 'src/WebApi.csproj'
build-docker: true
dockerfile-path: './Dockerfile'
docker-registry: 'ghcr.io'
docker-image-name: '${{ github.repository }}'
secrets:
DOCKER_USERNAME: ${{ github.actor }}
DOCKER_PASSWORD: ${{ secrets.GITHUB_TOKEN }}jobs:
build-library:
uses: your-org/automation-templates/.github/workflows/dotnet-build.yml@main
with:
project-path: 'src/MyLibrary.csproj'
create-package: true
include-symbols: true
push-to-nuget: true
secrets:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}For projects with SignAssembly=true in .csproj or Directory.Build.props:
jobs:
build-signed:
uses: your-org/automation-templates/.github/workflows/dotnet-build.yml@main
with:
project-path: 'MyLibrary.sln'
# Path must match AssemblyOriginatorKeyFile in your project
snk-file-path: 'build/MyLibrary.snk'
run-tests: true
secrets: inheritThe workflow decodes DOTNET_SIGNKEY_BASE64 and creates the SNK file at the specified path before building. If the secret is not available (e.g. in PR builds), signing is automatically skipped with a warning annotation. Your project configuration should look like:
<!-- Directory.Build.props -->
<PropertyGroup>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)build\MyLibrary.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>jobs:
build-blazor:
uses: your-org/automation-templates/.github/workflows/dotnet-build.yml@main
with:
project-path: 'src/BlazorApp.csproj'
publish: true
output-directory: './dist'
build-args: '-p:BlazorEnableCompression=true'
publish-args: '-p:BlazorWebAssemblyEnableLinking=true'jobs:
build-microservice:
uses: your-org/automation-templates/.github/workflows/dotnet-build.yml@main
with:
project-path: 'src/Microservice.csproj'
runtime: 'linux-musl-x64'
self-contained: true
build-docker: true
run-code-analysis: truejobs:
matrix-build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
dotnet: ['6.0.x', '8.0.x']
configuration: [Debug, Release]
uses: your-org/automation-templates/.github/workflows/dotnet-build.yml@main
with:
dotnet-version: ${{ matrix.dotnet }}
configuration: ${{ matrix.configuration }}
runs-on: ${{ matrix.os }}
project-path: 'src/MyProject.csproj'Location: .github/config/dotnet-build/default.yml
dotnet:
version: '8.0.x'
build:
configuration: Release
verbosity: normal
testing:
enabled: true
coverage:
enabled: false
threshold: 0- Web API:
.github/config/dotnet-build/web-api.yml - Class Library:
.github/config/dotnet-build/class-library.yml - Blazor WebAssembly:
.github/config/dotnet-build/blazor-wasm.yml - Microservice:
.github/config/dotnet-build/microservice.yml
All example workflows are located in github/workflows/examples/dotnet-build/:
simple-library.yml- Simple class library buildweb-api-docker.yml- Web API with Docker deploymentnuget-package-publish.yml- NuGet package publishingblazor-wasm-deploy.yml- Blazor WebAssembly deploymentmatrix-cross-platform.yml- Cross-platform matrix buildsmicroservice-k8s.yml- Microservice with Kubernetes
package-version: ${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || format('1.0.0-preview.{0}', github.run_number) }}publish: ${{ github.ref == 'refs/heads/main' }}
push-to-nuget: ${{ startsWith(github.ref, 'refs/tags/v') }}configuration: ${{ github.ref == 'refs/heads/main' && 'Release' || 'Debug' }}coverage-threshold: ${{ github.event_name == 'pull_request' && 80 || 0 }}docker-image-name: '${{ github.repository }}:${{ github.sha }}'# Build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["MyApp.csproj", "."]
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish
# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS runtime
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyApp.dll"]- GitHub Container Registry:
ghcr.io - Docker Hub:
docker.io - Azure Container Registry:
*.azurecr.io - AWS ECR:
*.dkr.ecr.*.amazonaws.com
test-filter: 'Category!=LongRunning & Category!=Integration'test-args: '--parallel --max-parallel-threads 4'- TRX: Visual Studio test results
- HTML: Human-readable reports
- JUnit: CI/CD integration
-
Package restore fails
- Check network connectivity
- Verify NuGet sources
- Clear NuGet cache
-
Tests not discovered
- Verify test project references
- Check test framework packages
- Review filter expressions
-
Docker build fails
- Verify Dockerfile path
- Check base image availability
- Review build context
-
Coverage below threshold
- Exclude generated code
- Add more test cases
- Review threshold settings
-
"No test report files were found"
-
This warning appears when no
.trxfiles are generated -
Cause: No test projects exist or
run-tests: trueis set but the solution has no tests -
Solution A: Set
run-tests: falseif your project has no tests:uses: bauer-group/automation-templates/.github/workflows/dotnet-build.yml@main with: run-tests: false
-
Solution B: Add a test project with
Microsoft.NET.Test.Sdkpackage reference -
The workflow now gracefully skips test reporting when no test projects are found
-
-
"Input required and not supplied: path" (Upload artifact)
- This error occurs when test results path is empty
- Cause: Tests didn't run or no test projects were found
- Solution: Same as issue #5 above - set
run-tests: falseor add test projects - The workflow now includes
if-no-files-found: ignoreto prevent this error
If your repository uses this reusable workflow and you see test-related warnings:
# Option 1: Disable tests if your project has none
jobs:
build:
uses: bauer-group/automation-templates/.github/workflows/dotnet-build.yml@main
with:
project-path: 'src/MyLibrary.csproj'
run-tests: false # No test projects in this repo
# Option 2: Ensure test projects are properly configured
# Your test project .csproj must include:
# <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*" />
# <PackageReference Include="xunit" Version="2.*" /> (or NUnit/MSTest)
# <PackageReference Include="xunit.runner.visualstudio" Version="2.*" />Enable verbose logging:
verbosity: 'diagnostic'- Cache dependencies: Enabled by default
- Parallel builds: Use matrix strategy
- Selective testing: Use test filters
- Incremental builds: Leverage build cache
- Container optimization: Multi-stage builds
- Use secrets for sensitive data
- Enable code analysis for security checks
- Scan Docker images for vulnerabilities
- Sign NuGet packages when publishing
- Use dependabot for dependency updates
- Convert pipeline YAML syntax
- Map tasks to workflow inputs
- Update variable references
- Configure service connections as secrets
- Convert pipeline script to YAML
- Map plugins to actions
- Update credential management
- Configure webhooks
- 📚 Documentation
- 🐛 Issues
- 💬 Discussions
- 📧 Contact
This workflow is part of the Automation Templates repository and follows the same license terms.