dotnet-gha-build-test
npx skills add https://github.com/novotnyllc/dotnet-artisan --skill dotnet-gha-build-test
Agent 安装分布
Skill 文档
dotnet-gha-build-test
.NET build and test workflow patterns for GitHub Actions: actions/setup-dotnet@v4 configuration with multi-version installs and NuGet authentication, NuGet restore caching for fast CI, dotnet test with result publishing via dorny/test-reporter, code coverage upload to Codecov and Coveralls, multi-TFM matrix testing across net8.0 and net9.0, and test sharding strategies for large projects.
Version assumptions: actions/setup-dotnet@v4 for .NET 8/9/10 support. dorny/test-reporter@v1 for test result visualization. Codecov and Coveralls GitHub Apps for coverage reporting.
Scope
- setup-dotnet action configuration with multi-version installs
- NuGet restore caching for fast CI
- dotnet test with result publishing and coverage upload
- Multi-TFM matrix testing and test sharding
- NuGet authentication for private feeds in GitHub Actions
Out of scope
- Starter CI templates — see [skill:dotnet-add-ci]
- Test architecture and strategy — see [skill:dotnet-testing-strategy]
- Benchmark regression detection in CI — see [skill:dotnet-ci-benchmarking]
- Publishing and deployment — see [skill:dotnet-gha-publish] and [skill:dotnet-gha-deploy]
- Azure DevOps build/test pipelines — see [skill:dotnet-ado-build-test]
- Reusable workflow and composite action patterns — see [skill:dotnet-gha-patterns]
Cross-references: [skill:dotnet-add-ci] for starter build/test templates, [skill:dotnet-testing-strategy] for test architecture guidance, [skill:dotnet-ci-benchmarking] for benchmark CI integration, [skill:dotnet-artifacts-output] for artifact upload path adjustments when using centralized build output layout.
actions/setup-dotnet@v4 Configuration
Basic Setup
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
Multi-Version Install
Install multiple SDK versions for multi-TFM builds within a single job:
- name: Setup .NET SDKs
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
9.0.x
The first listed version becomes the default dotnet on PATH. All installed versions are available via --framework targeting.
NuGet Authentication for Private Feeds
Configure NuGet source authentication via actions/setup-dotnet@v4:
- name: Setup .NET with NuGet auth
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
source-url: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json
env:
NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
For multiple private feeds, configure additional sources after setup:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Add private NuGet feed
run: |
set -euo pipefail
dotnet nuget add source https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json \
--name AzureArtifacts \
--username az \
--password ${{ secrets.AZURE_ARTIFACTS_PAT }} \
--store-password-in-clear-text
The --store-password-in-clear-text flag is required on Linux runners where DPAPI encryption is unavailable.
Global.json SDK Version Pinning
When global.json exists in the repository root, actions/setup-dotnet@v4 can read it automatically:
- name: Setup .NET from global.json
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
This ensures CI uses the same SDK version as local development.
NuGet Restore Caching
Standard Cache Configuration
- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props') }}
restore-keys: |
nuget-${{ runner.os }}-
- name: Restore dependencies
run: dotnet restore MySolution.sln
Built-in Cache with setup-dotnet
actions/setup-dotnet@v4 has built-in caching support using packages.lock.json:
- name: Setup .NET with caching
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
cache: true
cache-dependency-path: '**/packages.lock.json'
Generate lock files locally first: dotnet restore --use-lock-file. Commit packages.lock.json files for deterministic restore.
Cache Key Strategy
| Key Component | Purpose |
|---|---|
runner.os |
Prevent cross-OS cache collisions |
hashFiles('**/*.csproj') |
Invalidate when package references change |
hashFiles('**/Directory.Packages.props') |
Invalidate when centrally managed versions change |
restore-keys prefix |
Partial match for incremental cache reuse |
Test Result Publishing
dorny/test-reporter
Publish dotnet test results as GitHub Actions check annotations with inline failure details:
- name: Test
run: |
set -euo pipefail
dotnet test MySolution.sln \
--configuration Release \
--logger "trx;LogFileName=test-results.trx" \
--results-directory ./test-results
continue-on-error: true
id: test
- name: Publish test results
uses: dorny/test-reporter@v1
if: always()
with:
name: '.NET Test Results'
path: 'test-results/**/*.trx'
reporter: dotnet-trx
fail-on-error: true
Key decisions:
continue-on-error: trueon the test step ensures the reporter step always runs, even on failuresif: always()on the reporter step publishes results regardless of test outcomefail-on-error: trueon the reporter marks the check as failed when tests fail
Alternative: EnricoMi/publish-unit-test-result-action
For richer PR comment integration with test counts:
- name: Publish test results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: 'test-results/**/*.trx'
check_name: 'Test Results'
Code Coverage Upload
Codecov
- name: Test with coverage
run: |
set -euo pipefail
dotnet test MySolution.sln \
--configuration Release \
--collect:"XPlat Code Coverage" \
--results-directory ./coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
directory: ./coverage
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}
Coveralls
- name: Test with coverage
run: |
set -euo pipefail
dotnet test MySolution.sln \
--configuration Release \
--collect:"XPlat Code Coverage" \
--results-directory ./coverage
- name: Upload coverage to Coveralls
uses: coverallsapp/github-action@v2
with:
file: coverage/**/coverage.cobertura.xml
format: cobertura
github-token: ${{ secrets.GITHUB_TOKEN }}
Coverage Report Generation with ReportGenerator
Generate human-readable HTML coverage reports alongside CI upload:
- name: Generate coverage report
run: |
set -euo pipefail
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator \
-reports:coverage/**/coverage.cobertura.xml \
-targetdir:coverage-report \
-reporttypes:HtmlInline_AzurePipelines\;Cobertura
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage-report/
retention-days: 30
Multi-TFM Matrix Testing
Matrix Strategy for TFMs
jobs:
test:
strategy:
fail-fast: false
matrix:
tfm: [net8.0, net9.0]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
9.0.x
- name: Cache NuGet
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props') }}
restore-keys: |
nuget-${{ runner.os }}-
- name: Test ${{ matrix.tfm }}
run: |
set -euo pipefail
dotnet test MySolution.sln \
--framework ${{ matrix.tfm }} \
--configuration Release \
--logger "trx;LogFileName=${{ matrix.tfm }}-results.trx" \
--results-directory ./test-results
- name: Publish test results
uses: dorny/test-reporter@v1
if: always()
with:
name: 'Tests (${{ matrix.os }} / ${{ matrix.tfm }})'
path: 'test-results/**/*.trx'
reporter: dotnet-trx
Install All Required SDKs
When running multi-TFM tests in a single job instead of a matrix, install all required SDKs upfront:
- name: Setup .NET SDKs
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
9.0.x
- name: Test all TFMs
run: dotnet test MySolution.sln --configuration Release
Without the matching SDK installed, dotnet test cannot build for that TFM and fails with NETSDK1045.
Test Sharding for Large Projects
Splitting Tests Across Parallel Jobs
For large test suites, split test projects across parallel runners to reduce total CI time:
jobs:
discover:
runs-on: ubuntu-latest
outputs:
projects: ${{ steps.find.outputs.projects }}
steps:
- uses: actions/checkout@v4
- id: find
shell: bash
run: |
set -euo pipefail
PROJECTS=$(find tests -name '*.csproj' | jq -R . | jq -sc .)
echo "projects=$PROJECTS" >> "$GITHUB_OUTPUT"
test:
needs: discover
strategy:
fail-fast: false
matrix:
project: ${{ fromJson(needs.discover.outputs.projects) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Test ${{ matrix.project }}
run: |
set -euo pipefail
dotnet test ${{ matrix.project }} \
--configuration Release \
--logger "trx;LogFileName=results.trx" \
--results-directory ./test-results
- name: Publish test results
uses: dorny/test-reporter@v1
if: always()
with:
name: 'Tests - ${{ matrix.project }}'
path: 'test-results/**/*.trx'
reporter: dotnet-trx
Sharding by Test Class Within a Project
For a single large test project, use dotnet test --filter to split by namespace:
jobs:
test:
strategy:
fail-fast: false
matrix:
shard: ['Unit', 'Integration', 'EndToEnd']
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Test ${{ matrix.shard }}
run: |
set -euo pipefail
dotnet test tests/MyApp.Tests.csproj \
--configuration Release \
--filter "FullyQualifiedName~${{ matrix.shard }}" \
--logger "trx;LogFileName=${{ matrix.shard }}-results.trx" \
--results-directory ./test-results
Agent Gotchas
- Always set
set -euo pipefailin multi-line bashrunblocks — withoutpipefail, piped commands that fail do not propagate the error, producing false-green CI. - Use
continue-on-error: trueon the test step, not on the reporter — the test step must not fail the job prematurely so the reporter can publish results, but the reporter should fail the check when tests fail. - Include
runner.osin NuGet cache keys — NuGet packages have OS-specific native assets; cross-OS cache hits cause restore failures. - Install all required SDK versions for multi-TFM —
dotnet testwithout the matching SDK producesNETSDK1045; list every required version indotnet-version. - Do not hardcode TFM strings in workflow files — use matrix variables to keep workflow files in sync with project configuration; hardcoded
net8.0in CI breaks when the project moves tonet9.0. - Coverage collection requires
--collect:"XPlat Code Coverage"— the defaultdotnet testdoes not produce coverage files; theXPlat Code Coveragecollector is built into the .NET SDK. - TRX logger path must match reporter glob — if the logger writes to
test-results/results.trx, the reporterpathmust include that directory in its glob pattern. - Never commit NuGet credentials to workflow files — use
${{ secrets.* }}references for all authentication tokens; theNUGET_AUTH_TOKENenvironment variable is the standard pattern.