.cursor/skills/dotnet-gha-build-test/SKILL.md
Configuring .NET build/test in GitHub Actions. setup-dotnet, NuGet caching, test reporting.
npx skillsauth add AGIBuild/Fulora dotnet-gha-build-testInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
.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 boundary: This skill owns .NET build and test pipeline configuration for GitHub Actions. Starter CI templates (basic build/test/pack) are owned by [skill:dotnet-add-ci]. Composable workflow patterns (reusable workflows, matrix strategies, caching) are in [skill:dotnet-gha-patterns]. Testing strategy guidance (what to test, test architecture, quality gates) is owned by [skill:dotnet-testing-strategy]. Benchmark CI workflows are owned by [skill:dotnet-ci-benchmarking].
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].
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 Configurationsteps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
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.
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.
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.
- 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
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.
| 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 |
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: true on 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: true on the reporter marks the check as failed when tests failFor 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'
- 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 }}
- 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 }}
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
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
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.
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
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
set -euo pipefail in multi-line bash run blocks -- without pipefail, piped commands that fail do not propagate the error, producing false-green CI.continue-on-error: true on 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.runner.os in NuGet cache keys -- NuGet packages have OS-specific native assets; cross-OS cache hits cause restore failures.dotnet test without the matching SDK produces NETSDK1045; list every required version in dotnet-version.net8.0 in CI breaks when the project moves to net9.0.--collect:"XPlat Code Coverage" -- the default dotnet test does not produce coverage files; the XPlat Code Coverage collector is built into the .NET SDK.test-results/results.trx, the reporter path must include that directory in its glob pattern.${{ secrets.* }} references for all authentication tokens; the NUGET_AUTH_TOKEN environment variable is the standard pattern.tools
Captures learnings, errors, and corrections to enable continuous improvement. Use when: (1) A command or operation fails unexpectedly, (2) User corrects Claude ('No, that's wrong...', 'Actually...'), (3) User requests a capability that doesn't exist, (4) An external API or tool fails, (5) Claude realizes its knowledge is outdated or incorrect, (6) A better approach is discovered for a recurring task. Also review learnings before major tasks.
testing
Security headers configuration and best practices for ASP.NET Core Razor Pages applications. Covers CSP, HSTS, X-Frame-Options, and comprehensive security middleware setup. Use when configuring security headers in ASP.NET Core applications, implementing Content Security Policy (CSP), or setting up HSTS and other security-related HTTP headers.
development
Reviews designs and business goals for security vulnerabilities, data protection (in transit/at rest), authorization, and compliance alignment. Use when the user asks for a security review, threat modeling, attack surface analysis, data leakage prevention, or compliance/security assessment.
development
Best practices for building production-grade ASP.NET Core Razor Pages applications. Focuses on structure, lifecycle, binding, validation, security, and maintainability in web apps using Razor Pages as the primary UI framework. Use when building Razor Pages applications, designing PageModels and handlers, implementing model binding and validation, or securing Razor Pages with authentication and authorization.