Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/benchmarks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ permissions:
# Serialize benchmark publishes so two pushes to main close together can't race
# on the gh-pages branch. cancel-in-progress: false — every merge represents a
# meaningful data point, so we queue rather than discard.
#
# NOTE: docfx.yaml and release.yaml currently have no `concurrency:` block,
# so cross-workflow serialization is only partial today. Adding the same
# `gh-pages` group to those workflows is tracked as a fleet-wide follow-up.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: gh-pages
cancel-in-progress: false

jobs:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/codeql.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ jobs:

- name: Initialize CodeQL
if: steps.check-csharp.outputs.has-csharp == 'true'
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# security-extended adds the broader security query pack on top of the
Expand Down Expand Up @@ -159,7 +159,7 @@ jobs:
- name: Perform CodeQL Analysis
id: perform-codeql-analysis
if: steps.check-csharp.outputs.has-csharp == 'true'
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"

Expand Down
153 changes: 89 additions & 64 deletions .github/workflows/stryker.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
# Stryker mutation testing
#
# Runs the Stryker.NET mutation tester against the repo's test projects to
# measure mutation score. Mutation runs are slow — triggered manually
# (workflow_dispatch) and on a weekly schedule, not on every PR.
# Mutation testing is canonical for every repo created from this template.
# The workflow runs Stryker.NET against every test project under tests/ on
# a Windows runner so it can compile every TFM the repo targets - including
# .NET Framework 4.6.2-4.8.1, which a Linux runner cannot build.
#
# The workflow looks for a stryker-config.json at the repo root or under
# tests/**/. If none is present the run is a no-op (Stryker setup is a
# per-repo follow-up; this file is the canonical infrastructure).
# Triggers:
# - workflow_dispatch: manual ad-hoc runs (e.g. while iterating on tests)
# - schedule (weekly Sunday 06:00 UTC): catch quality regressions between
# releases without burning CI time on every PR (mutation testing is slow)
#
# Stryker discovers each test project's mutation targets via the standard
# project-reference graph. Two configuration modes are supported:
# - Root stryker-config.json (umbrella): if present, runs Stryker once with
# the umbrella config and skips per-project runs (avoids duplicate work).
# - Per-project stryker-config.json (or none): if no root config exists, runs
# Stryker once per test project; per-project configs override Stryker's
# defaults when present.
name: Stryker (mutation testing)

on:
Expand All @@ -20,88 +30,103 @@ permissions:
jobs:
stryker:
name: Run Stryker
runs-on: ubuntu-latest
timeout-minutes: 60
# Windows runner so Stryker can compile every TFM the repo targets,
# including .NET Framework 4.6.2-4.8.1. Linux runners cannot build
# net4x, so they would silently mutate only the non-Framework TFMs
# and miss bugs that only reproduce on Framework.
runs-on: windows-latest
# Skip on the template repo itself - it has no test projects, so the
# workflow would just no-op every Sunday at 06:00 UTC, wasting a
# Windows runner allocation. Matches the same-pattern guard used by
# the .NET test-stage jobs in pr.yaml.
if: github.repository != 'Chris-Wolfgang/repo-template'
timeout-minutes: 120
steps:
- name: Check out repo
uses: actions/checkout@v6
with:
persist-credentials: false

- name: Detect stryker-config.json
id: check
shell: bash
run: |
# Explicit existence checks rather than globbing — `nullglob` only
# drops words that look like patterns (contain *, ?, [). The bare
# literal `stryker-config.json` has no glob characters, so it would
# be preserved as a literal even when the file doesn't exist, and
# the workflow would mistakenly think a config was present.
shopt -s globstar nullglob
configs=()
[ -f stryker-config.json ] && configs+=(stryker-config.json)
for cfg in tests/**/stryker-config.json; do
[ -f "$cfg" ] && configs+=("$cfg")
done
if (( ${#configs[@]} )); then
printf 'found=true\n' >> "$GITHUB_OUTPUT"
# NOTE: previously also wrote a configs<<EOF multi-line output here,
# but (a) the downstream Run Stryker step re-globs from scratch and
# didn't consume it, and (b) ${configs[*]} joins with spaces so the
# value was effectively a single space-separated line — not actually
# multi-line. Dropped the dead/broken output.
else
echo "::notice::No stryker-config.json found — skipping Stryker run. Add one at repo root or under tests/<project>/ to enable mutation testing."
printf 'found=false\n' >> "$GITHUB_OUTPUT"
fi

- name: Setup .NET
if: steps.check.outputs.found == 'true'
# Install every SDK in the canonical test matrix (.NET Core 3.1
# through .NET 10.0). Stryker has to be able to build whichever
# TFM each test project targets; pinning to a subset would
# silently fail repos that target older runtimes.
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
3.1.x
5.0.x
6.0.x
7.0.x
8.0.x
9.0.x
10.0.x

- name: Install dotnet-stryker
if: steps.check.outputs.found == 'true'
run: dotnet tool update -g dotnet-stryker || dotnet tool install -g dotnet-stryker
shell: pwsh
run: |
dotnet tool update -g dotnet-stryker
if ($LASTEXITCODE -ne 0) {
dotnet tool install -g dotnet-stryker
}

- name: Run Stryker
if: steps.check.outputs.found == 'true'
shell: bash
shell: pwsh
run: |
set -e
shopt -s globstar nullglob
# Run BOTH a root stryker-config.json (if present) AND any
# tests/**/stryker-config.json suites. The Detect step collects
# both shapes; running only one leaves per-test suites unscanned
# in repos that have both an umbrella and per-suite configs.
ran=0
if [ -f stryker-config.json ]; then
echo "::group::Stryker with root stryker-config.json"
# Configuration mode resolution:
# 1. If a root stryker-config.json exists, it is the umbrella -
# run Stryker once with it and stop (per-project runs would
# duplicate the same work the umbrella already covers).
# 2. Otherwise, run Stryker against every test project under
# tests/. A per-project stryker-config.json next to a test
# project overrides Stryker's defaults when present.

$failed = 0

if (Test-Path 'stryker-config.json') {
Write-Host "::group::Stryker with root umbrella stryker-config.json"
dotnet stryker --config-file stryker-config.json
echo "::endgroup::"
ran=1
fi
for cfg in tests/**/stryker-config.json; do
dir=$(dirname "$cfg")
echo "::group::Stryker in $dir"
(cd "$dir" && dotnet stryker)
echo "::endgroup::"
ran=1
done
if [ "$ran" -eq 0 ]; then
echo "::error::Detect step said stryker-config.json was present, but Run found neither root nor tests/**/stryker-config.json. Bailing."
if ($LASTEXITCODE -ne 0) { $failed++ }
Write-Host "::endgroup::"
}
else {
$tests = @(Get-ChildItem -Path tests -Recurse -Filter '*Test*.csproj' -ErrorAction SilentlyContinue)

if ($tests.Count -eq 0) {
Write-Host "::notice::No test projects found under tests/ - skipping Stryker run."
exit 0
}

Write-Host "Found $($tests.Count) test project(s):"
$tests | ForEach-Object { Write-Host " $($_.FullName)" }
Write-Host ""

foreach ($proj in $tests) {
$dir = $proj.DirectoryName
Write-Host "::group::Stryker in $dir"
Push-Location $dir
try {
dotnet stryker
if ($LASTEXITCODE -ne 0) { $failed++ }
} finally {
Pop-Location
}
Write-Host "::endgroup::"
}
}

if ($failed -gt 0) {
Write-Host "::error::$failed Stryker run(s) failed"
exit 1
fi
}

- name: Upload Stryker report
if: always() && steps.check.outputs.found == 'true'
if: always()
uses: actions/upload-artifact@v7
with:
name: stryker-report-${{ github.run_id }}
path: |
**/StrykerOutput/**
if-no-files-found: ignore
retention-days: 30
retention-days: 30
10 changes: 8 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,13 @@ CoverageReport/
package-smoke-test/
nuget-packages/

# DocFX generated files
docfx_project/_site/
# DocFX generated files (any depth)
_site/
docfx_project/obj/
# Auto-generated API metadata: `docfx metadata` writes <Type>.yml and toc.yml
# under docfx_project/api/ for every public type extracted from src/. Keep
# these out of the repo so a careless `git add -A` after running docfx
# metadata locally doesn't commit generated files. The hand-written
# index.md and README.md in api/ are intentionally tracked.
docfx_project/api/*.yml

6 changes: 3 additions & 3 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@
</PackageReference>

<!-- Meziantou - Comprehensive code quality -->
<PackageReference Include="Meziantou.Analyzer" Version="3.0.85">
<PackageReference Include="Meziantou.Analyzer" Version="3.0.98">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

<!-- SonarAnalyzer - Industry-standard analysis -->
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.25.0.139117">
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.27.0.140913">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down Expand Up @@ -104,7 +104,7 @@
<ItemGroup>
<!-- SourceLink: embed source provenance into PDBs so debuggers can step
into NuGet package code from GitHub. -->
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.300">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
Expand Down