Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8637b88
Update mcr.microsoft.com/dotnet/sdk:10.0.401 Docker digest to 35d4030…
renovate[bot] Sep 22, 2026
d59c238
Avoid re-installing .NET SDK or runtimes
AArnott Sep 22, 2026
086eeab
Merge pull request #576 from AArnott/Install-Sdk-Enhancements
AArnott Sep 22, 2026
13d25ee
Update dependency docfx to v2.81.0 (577)
renovate[bot] Sep 25, 2026
95dd30b
Update becheran/mlc action to v1.2.2 (578)
renovate[bot] Sep 29, 2026
d477832
Make docs workflow recoverable via re-run or manual dispatch
AArnott Sep 30, 2026
57fc2b2
Pin Ubuntu workflows to 26.04
AArnott Oct 1, 2026
52ff92c
Merge pull request #581 from AArnott/agents/pin-ubuntu-26-04
AArnott Oct 1, 2026
3f29bf7
Migrate from xUnit to TUnit
AArnott Sep 30, 2026
302ccbe
Build and test NativeAOT-safe libraries
AArnott Oct 1, 2026
a3c19c0
Add opt-out instructions for the NativeAOT support
AArnott Oct 2, 2026
5e9fca2
Improve release notes further
AArnott Oct 2, 2026
3eebddd
Build and publish in one step in Azure Pipelines
AArnott Oct 2, 2026
bab81d0
Merge pull request #580 from AArnott/nativeaot
AArnott Oct 2, 2026
8b0c0d0
Update mcr.microsoft.com/dotnet/sdk:10.0.401 Docker digest to 83e0db9…
renovate[bot] Oct 2, 2026
84e1db7
Update mcr.microsoft.com/dotnet/sdk:10.0.401 Docker digest to e70cdb7…
renovate[bot] Oct 2, 2026
84911e4
Clarify API xml doc requirements
AArnott Oct 3, 2026
580ec8a
Fix version.json schema URL
AArnott Oct 4, 2026
d9061ce
Initial plan
Copilot Oct 5, 2026
ee412cb
Merge the main branch from https://github.com/aarnott/Library.Template
Copilot Oct 5, 2026
6b8242d
chore: re-trigger CI after Copilot merge
AArnott Oct 5, 2026
b8820ef
fix: exclude Mac tests from traversal build on non-macOS agents
Copilot Oct 5, 2026
18f7ada
fix: restore valid version.json publicReleaseRefSpec escapes
Copilot Oct 5, 2026
4e091f9
fix: restore repo-specific content overwritten by Library.Template merge
Copilot Oct 5, 2026
ccabde7
fix: build net8.0-windows test TFMs only on Windows
Copilot Oct 5, 2026
05c2761
AOT.props: don't overwrite a test project's InvariantGlobalization/Sa…
AArnott Oct 6, 2026
d8cb86e
Merge the main branch from https://github.com/aarnott/Library.Template
invalid-email-address Oct 6, 2026
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
64 changes: 64 additions & 0 deletions .agents/skills/update-library-template/template-release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,67 @@ dotnet solution EXISTING.sln migrate
This will create an EXISTING.slnx file. `git add` that file, then `git rm` the old `.sln` file.
Sometimes a repo will reference the sln filename in a script or doc somewhere.
Search the repo for such references and update them to the slnx file.

## Migrating from xUnit to TUnit

The template now uses TUnit with Microsoft.Testing.Platform instead of the xUnit test runner.
When merging this update into an existing repository, migrate all test projects and repo-specific test automation.
Follow the [TUnit xUnit migration guide](https://tunit.dev/docs/migration/xunit), including its automated migration code fixes.

* Replace xUnit runner/framework package references with `TUnit.Engine` and set `<OutputType>Exe</OutputType>` in each test project.
Remove obsolete runner dependencies such as `xunit.runner.visualstudio`, `Microsoft.NET.Test.Sdk`, and `coverlet.collector`, along with `xunit.runner.json`.
Reconcile central package versions with the template's `Directory.Packages.props`.
* Existing xUnit assertions can remain: the template retains `xunit.v3.assert`, using `xunit.v3.assert.aot` for .NET 9 and later.
Keep `Xunit` imports for assertions, but use TUnit for test attributes and lifecycle APIs.
* Convert `[Fact]` and `[Theory]` to `[Test]`, and `[InlineData]` to `[Arguments]`.
Migrate member/class data sources, traits, fixtures, collection behavior, setup/teardown, and test output according to the migration guide.
Review tests that depend on xUnit's execution ordering or parallelization rules; do not assume those rules carry over.
* Merge the `global.json` Microsoft.Testing.Platform runner setting, test directory build files, traversal projects, and updated PowerShell/YAML automation.
Keep every test project in the repository's solution for managed test runs.
The traversals discover `.csproj` files under `src` and `test`; adjust discovery if your projects live elsewhere or use another project language.
* Update custom test commands and filters.
VSTest `--filter` expressions do not work with this runner; pass TUnit filters after `--`, for example `-- --treenode-filter "/*/*/ClassName/MethodName"`.
Replace any xUnit-specific CI result or coverage handling with the template's Microsoft.Testing.Platform integration.
* Validate managed tests and coverage, then run `dotnet publish tools\dirs.proj -c Release` and `.\tools\dotnet-test-cloud.ps1 -Configuration Release -IncludeNativeAOT`.
Test projects targeting .NET 8 or later are eligible for NativeAOT publication by default.
If a project cannot support NativeAOT, set `<PublishNativeAOTTests>false</PublishNativeAOTTests>` in its project file; it will still run as managed tests.

## NativeAOT compatibility validation

Shipping projects now set `IsAotCompatible` for target frameworks compatible with .NET 8.
Apply the same conditional property to each shipping project that is intended to support NativeAOT, then resolve all trim and AOT analyzer warnings rather than suppressing them broadly.

The new `test/AotCompatibilityTest` project catches NativeAOT compiler warnings that library analyzers may not report.
Keep it in the solution, add every NativeAOT-compatible shipping assembly as a `TrimmerRootAssembly`, and retain its publishable entry in `test/dirs.proj`.
CI validates this project through the existing traversal publish on Windows, Linux, and macOS; it is intentionally marked `IsTestProject=false` because publishing successfully is the test.

## NativeAOT opt-out and AI merge guidance

Not every repo wants to take on NativeAOT readiness or NativeAOT-published test validation.
If a receiving repo does not intend to support NativeAOT, opt out explicitly instead of inheriting the template's defaults.

For test projects that should remain managed-only:

* Set `<PublishNativeAOTTests>false</PublishNativeAOTTests>` in the project file or in a repo-level `Directory.Build.props`.
* Leave the managed test project in the solution so usual IL-based testing continues to work.
* Keep `dotnet test` and the managed coverage flow unchanged; only the NativeAOT publish step is disabled.

For shipping libraries that do not want NativeAOT compatibility analysis:

* Remove `IsAotCompatible` from the project, or set it only for specific target frameworks that truly support NativeAOT.
* Do not add new AOT compatibility warnings to the build if the library is not intended to be AOT-compatible.
* If the repo does not target .NET 8 or later, the property should be omitted entirely rather than applied unconditionally.

For the compatibility project itself:

* Remove the `test/AotCompatibilityTest` project from the solution if NativeAOT validation is out of scope.
* Remove its entry from `test/dirs.proj` so the traversal publish no longer publishes it.
* Keep the project only when the repo intends to validate shipping assemblies under NativeAOT in CI.

When merging this template into a downstream repo, the AI assistant should ask whether NativeAOT is a relevant target for that repo before applying these changes if `IsAotCompatible` is not already set by any of the projects.
If the answer is no, it should apply the opt-out steps above instead of blindly keeping the NativeAOT defaults.
This is especially important for repos that are not shipping a NativeAOT-ready library or that cannot support the extra publish-time validation in CI.
If the repo owner is unsure about NativeAOT support, you might offer to test the repo for NativeAOT readiness by attempting a NativeAOT publish of the relevant projects and observing whether any compatibility warnings or errors are produced.

In the event the user elects for NativeAOT targeting, make efforts to get tests to work under NativeAOT as well so the shipping code can confidently advertise its NativeAOT compatibility.
If aspects of the library or tests make successful builds, publish or test runs problematic, discuss this with the repo owner before making significant changes or giving up on NativeAOT support.
2 changes: 1 addition & 1 deletion .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"rollForward": false
},
"docfx": {
"version": "2.80.1",
"version": "2.81.0",
"commands": [
"docfx"
],
Expand Down
2 changes: 1 addition & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Refer to https://hub.docker.com/_/microsoft-dotnet-sdk for available versions
FROM mcr.microsoft.com/dotnet/sdk:10.0.401@sha256:2fa828c68761b1b8c23d7662dc134421b9d3b59fe1425fdbc80804e390cdb24d
FROM mcr.microsoft.com/dotnet/sdk:10.0.401@sha256:e70cdb7f80b0348f5cb85f19a8f670fca061f033d57eed12fa003d58b0e06317

# Installing mono makes `dotnet test` work without errors even for net472.
# But installing it takes a long time, so it's excluded by default.
Expand Down
5 changes: 5 additions & 0 deletions .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
"matchUpdateTypes": ["minor", "patch"],
"enabled": false
},
{
"matchPackageNames": ["tunit*"],
"groupName": "tunit",
"schedule": ["on the first day of the month"]
},
{
"matchPackageNames": ["Microsoft.Testing.Extensions.*"],
"groupName": "Microsoft Testing Platform"
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
fail-fast: false
matrix:
os:
- ubuntu-24.04
- ubuntu-26.04
- macOS-15
- windows-2025

Expand All @@ -45,7 +45,7 @@ jobs:
run: tools/variables/_define.ps1
shell: pwsh
- name: 🛠 build
run: dotnet build -t:build,pack --no-restore -c ${{ env.BUILDCONFIGURATION }} -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904 /bl:"${{ runner.temp }}/_artifacts/build_logs/build.binlog"
run: dotnet build tools/dirs.proj -t:build,pack,publish --no-restore -c ${{ env.BUILDCONFIGURATION }} -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904 /bl:"${{ runner.temp }}/_artifacts/build_logs/build.binlog"
- name: 🧪 test
run: tools/dotnet-test-cloud.ps1 -Configuration ${{ env.BUILDCONFIGURATION }} -Agent ${{ runner.os }}
shell: pwsh
Expand Down Expand Up @@ -76,10 +76,10 @@ jobs:

docs:
name: 📃 Docs
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: 🔗 Markup Link Checker (mlc)
uses: becheran/mlc@7ec24825cefe0c9c8c6bac48430e1f69e3ec356e # v1.2.0
uses: becheran/mlc@b0d0d353e04fcbcfd1d86716103d78a61e5b25dd # v1.2.2
with:
args: --do-not-warn-for-redirect-to https://learn.microsoft.com*,https://dotnet.microsoft.com/*,https://dev.azure.com/*,https://app.codecov.io/* -p docfx -i https://www.npmjs.com/package/*,https://get.dot.net/
2 changes: 1 addition & 1 deletion .github/workflows/copilot-setup-steps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ on:
jobs:
# The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot.
copilot-setup-steps:
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
# Set the permissions to the lowest permissions possible needed for your steps.
# Copilot will be given its own token for its operations.
permissions:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/libtemplate-update.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ on:

jobs:
merge:
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
permissions:
contents: write
pull-requests: write
Expand Down
18 changes: 9 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,43 +28,43 @@ dotnet test --no-build -c Release

**Run tests for a specific test project**:
```bash
dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release
dotnet test --project test/Xunit.StaFact.Tests/Xunit.StaFact.Tests.csproj --no-build -c Release
```

**Run a single test method**:
```bash
dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --filter-method ClassName.MethodName
dotnet test --project test/Xunit.StaFact.Tests/Xunit.StaFact.Tests.csproj --no-build -c Release -- --filter-method ClassName.MethodName
```

**Run all tests in a test class**:
```bash
dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --filter-class ClassName
dotnet test --project test/Xunit.StaFact.Tests/Xunit.StaFact.Tests.csproj --no-build -c Release -- --filter-class ClassName
```

**Run tests with wildcard matching** (supports wildcards at beginning and/or end):
```bash
dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --filter-method "*Pattern*"
dotnet test --project test/Xunit.StaFact.Tests/Xunit.StaFact.Tests.csproj --no-build -c Release -- --filter-method "*Pattern*"
```

**Run tests with a specific trait** (equivalent to category filtering):
```bash
dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --filter-trait "TraitName=value"
dotnet test --project test/Xunit.StaFact.Tests/Xunit.StaFact.Tests.csproj --no-build -c Release -- --filter-trait "TraitName=value"
```

**Exclude tests with a specific trait** (skip unstable tests):
```bash
dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release -- --filter-not-trait "TestCategory=FailsInCloudTest"
dotnet test --project test/Xunit.StaFact.Tests/Xunit.StaFact.Tests.csproj --no-build -c Release -- --filter-not-trait "TestCategory=FailsInCloudTest"
```

**Run tests for a specific framework only**:
```bash
dotnet test --project test/Library.Tests/Library.Tests.csproj --no-build -c Release --framework net9.0
dotnet test --project test/Xunit.StaFact.Tests/Xunit.StaFact.Tests.csproj --no-build -c Release --framework net8.0
```

**List all available tests without running them**:
```bash
cd test/Library.Tests
dotnet run --no-build -c Release --framework net9.0 -- --list-tests
cd test/Xunit.StaFact.Tests
dotnet run --no-build -c Release --framework net8.0 -- --list-tests
```

**Key points about test filtering with MTP v2 / xunit v3**:
Expand Down
19 changes: 19 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,25 @@ You can use `dotnet test` to build and/or test the repo.

There may be tests that are known to be unstable or have special requirements. These can be avoided by running tests using the [dotnet-test-cloud.ps1](tools/dotnet-test-cloud.ps1) script *after* running `dotnet build`.

To build both managed and NativeAOT tests, run `dotnet publish tools/dirs.proj -c Release`.
Then run `./tools/dotnet-test-cloud.ps1 -Configuration Release -IncludeNativeAOT`.
The traversal projects discover projects under `src` and `test`, and publish each eligible test framework targeting .NET 8 or later.
Keep test projects in the solution as well: managed test runs still use the solution, while NativeAOT runs use the traversal's evaluated executable paths.
Test projects can opt out of NativeAOT publishing with `<PublishNativeAOTTests>false</PublishNativeAOTTests>`.
One restore includes all test target frameworks, runtime identifiers, and NativeAOT compiler dependencies.
Managed builds are RID-neutral by default; NativeAOT builds use the SDK's runtime-specific output directories.
For a specified RID, managed execution and native publishing can share the same build outputs.
Test builds keep dynamic code, startup hooks, and event tracing enabled for managed code coverage.
The `ConfigureNativeAOTTestFeatures` target disables those features only in the native compiler's publish-time inputs, without rewriting the managed runtime configuration.
It preserves all other runtime feature options, including invariant globalization, so native compilation and linking use consistent settings.
For an existing RID-specific build, `dotnet publish test/Library.Tests/Library.Tests.csproj -f net8.0 -r <RID> -p:NativeAOT=true --no-build` publishes native tests from the managed build.
Use the same configuration, framework, and RID for the preceding build and the publish.
Test builds use invariant globalization and retain only English satellite resources; RID-specific builds are self-contained.
Shipping libraries targeting .NET 8 or later opt into NativeAOT compatibility analysis with `IsAotCompatible`.
The `test/AotCompatibilityTest` project complements those analyzers by rooting the shipping assembly and passing it through the NativeAOT compiler during every traversal publish.
Add each shipping assembly that must be validated as a `TrimmerRootAssembly`, and keep this project publishable in `test/dirs.proj`.
Root `Directory.Build.props` supplies project-reference defaults for both traversal and SDK projects that remove the `_IsPublishing` global property for managed dependencies, avoiding duplicate project instances that write to the same outputs during parallel publishing.

## Releases

Use `nbgv tag` to create a tag for a particular commit that you mean to release.
Expand Down
17 changes: 17 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,28 @@
<LangVersion Condition="'$(MSBuildProjectExtension)'=='.vbproj'">16.9</LangVersion>
</PropertyGroup>

<PropertyGroup>
<RidOsPrefix Condition="$([MSBuild]::IsOsPlatform('Windows'))">win</RidOsPrefix>
<RidOsPrefix Condition="$([MSBuild]::IsOsPlatform('Linux'))">linux</RidOsPrefix>
<RidOsPrefix Condition="$([MSBuild]::IsOsPlatform('OSX'))">osx</RidOsPrefix>

<RidOsArchitecture Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'x64'">x64</RidOsArchitecture>
<RidOsArchitecture Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'ARM64'">arm64</RidOsArchitecture>

<DefaultRuntimeIdentifier>$(RidOsPrefix)-$(RidOsArchitecture)</DefaultRuntimeIdentifier>
<AvailableRuntimeIdentifiers>$(DefaultRuntimeIdentifier)</AvailableRuntimeIdentifiers>
</PropertyGroup>

<ItemGroup>
<AdditionalFiles Include="$(MSBuildThisFileDirectory)stylecop.json" Link="stylecop.json" />
</ItemGroup>

<ItemDefinitionGroup>
<ProjectReference>
<!-- Keep referenced builds identical to the traversal's managed builds. -->
<GlobalPropertiesToRemove>_IsPublishing</GlobalPropertiesToRemove>
<UndefineProperties Condition="'$(UsingMicrosoftTraversalSdk)' == 'true'">_IsPublishing</UndefineProperties>
</ProjectReference>
<!-- We always want MSBuild properties generated that point at the restored location of each package. -->
<PackageReference GeneratePathProperty="true" />
</ItemDefinitionGroup>
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<PackageVersion Include="Microsoft.Testing.Extensions.Telemetry" Version="$(MicrosoftTestingPlatformVersion)" />
<PackageVersion Include="Microsoft.Testing.Extensions.TrxReport" Version="$(MicrosoftTestingPlatformVersion)" />
<PackageVersion Include="xunit.v3.mtp-v2" Version="$(XunitV3LibraryVersion)" />
<PackageVersion Include="xunit.v3.assert" Version="4.0.1" />
</ItemGroup>
<ItemGroup>
<!-- Put repo-specific GlobalPackageReference items in this group. -->
Expand Down
3 changes: 3 additions & 0 deletions global.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@
},
"test": {
"runner": "Microsoft.Testing.Platform"
},
"msbuild-sdks": {
"Microsoft.Build.Traversal": "4.1.82"
}
}
1 change: 1 addition & 0 deletions init.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ try {
if ($lastexitcode -ne 0) {
throw "Failure while restoring packages."
}

}

if (!$NoToolRestore -and $PSCmdlet.ShouldProcess("dotnet tool", "restore")) {
Expand Down
1 change: 1 addition & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
</ItemGroup>

<Import Project="$([MSBuild]::GetPathOfFileAbove($(MSBuildThisFile), $(MSBuildThisFileDirectory)..))" />

</Project>
5 changes: 5 additions & 0 deletions src/dirs.proj
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.Build.Traversal">
<ItemGroup>
<ProjectReference Include="**\*.csproj" Publish="false" />
</ItemGroup>
</Project>
7 changes: 7 additions & 0 deletions test/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@
<Import Project="$([MSBuild]::GetPathOfFileAbove($(MSBuildThisFile), $(MSBuildThisFileDirectory)..))" />

<PropertyGroup>
<IsTestProject>true</IsTestProject>
<PublishNativeAOTTests>false</PublishNativeAOTTests>
<IsPackable>false</IsPackable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
<UseAppHost Condition="'$(RuntimeIdentifier)' == '' and '$(NativeAOT)' != 'true'">false</UseAppHost>
</PropertyGroup>

<PropertyGroup Condition="'$(NativeAOT)' == 'true' and '$(PublishNativeAOTTests)' == 'true'">
<RuntimeIdentifier>$(DefaultRuntimeIdentifier)</RuntimeIdentifier>
</PropertyGroup>

</Project>
Loading
Loading