diff --git a/.github/workflows/example-release.yml b/.github/workflows/example-release.yml
new file mode 100644
index 0000000..4ef7379
--- /dev/null
+++ b/.github/workflows/example-release.yml
@@ -0,0 +1,53 @@
+name: example-pr
+
+on:
+ push:
+ branches:
+ - main
+ # would normally filter to only paths that actually want to release for
+ # paths:
+ # - "src/**"
+ workflow_dispatch: # ๐ manual trigger here to release
+ inputs:
+ force-release:
+ description: "Force a release regardless of PR labels or branch"
+ type: boolean
+ required: false
+ default: false
+
+permissions:
+ contents: write
+ packages: write
+ pull-requests: read
+
+jobs:
+ dotnet-package:
+ permissions:
+ contents: write
+ packages: write
+ pull-requests: read
+ uses: ./.github/workflows/dotnet-package-release.yml
+ with:
+ working-directory: "./examples/NugetPackages"
+ os: ubuntu-latest
+ dotnet-version: 10.0.x
+ codecov-name: "example-dotnet-package"
+ is-release-branch: false
+ # is-release-branch: ${{ github.ref == 'refs/heads/main' }}
+ force-release: ${{ github.event.inputs.force-release == 'true' }}
+ secrets: inherit
+
+ # TODO: docker / aws lambda / other release build step
+ # dotnet-minimal-web-api:
+ # permissions:
+ # actions: read
+ # contents: read
+ # pull-requests: write
+ # security-events: write
+ # uses: ./.github/workflows/dotnet-pr.yml
+ # with:
+ # working-directory: "./examples/MinimalWebApi"
+ # os: "ubuntu-latest"
+ # dotnet-version: "10.0.x"
+ # codecov-name: "example-minimal-web-api"
+ # secrets: inherit
diff --git a/docs/dotnet-testing-setup.md b/docs/dotnet-testing-setup.md
new file mode 100644
index 0000000..1431462
--- /dev/null
+++ b/docs/dotnet-testing-setup.md
@@ -0,0 +1,232 @@
+# Setting up a .NET 10 solution for unit testing
+
+This describes how `examples/MinimalWebApi` and `examples/NugetPackages` are wired for
+testing, coverage, and CI, so you can replicate the pattern in a other solutions.
+
+## 1. Commonly used testing packages
+
+Both examples centralise the same core test packages via `Directory.Packages.props`
+(see [ยง3](#3-directorybuildprops-directorypackagesprops-and-globaljson)):
+
+| Package | Purpose |
+|---|---|
+| `AwesomeAssertions` | Fluent assertions library |
+| `GitHubActionsTestLogger` | Emits test results as GitHub Actions log annotations/summary |
+| `Microsoft.NET.Test.Sdk` | Base .NET test SDK |
+| `Microsoft.Testing.Platform` | The modern "MTP" test engine (replaces classic VSTest) |
+| `Microsoft.Testing.Extensions.CodeCoverage` | Native MTP code-coverage collector (powers `dotnet test --coverage`) |
+| `xunit.v3.mtp-v2` | xUnit v3, run natively via MTP |
+| `xunit.runner.visualstudio` | VSTest-compatible xUnit adapter (IDE test explorer support) |
+
+**WebApi Test projects**
+
+Additionally use `Microsoft.AspNetCore.Mvc.Testing` in its
+integration test project, for spinning up the API in-memory (`WebApplicationFactory`).
+
+## 2. Test project (`.csproj`) config
+
+A unit test project needs almost no explicit configuration โ everything else is
+inherited from `Directory.Build.props` / `Directory.Packages.props`:
+
+```xml
+
+
+
+ true
+
+
+
+
+
+
+
+```
+
+`true` is technically redundant if the project name
+contains `.Tests.` or ends in `.Tests` (see ยง3 โ `Directory.Build.props` detects this
+automatically), but can set it explicitly for clarity.
+
+An integration-test project that needs extra packages or config just adds them on top,
+e.g. `examples/MinimalWebApi/tests/MinimalWebApi.Tests.Integration.Api`:
+
+```xml
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+```
+
+> **Don't** redeclare a `PackageReference` that's already added centrally in
+> `Directory.Packages.props`'s test-project `ItemGroup` (ยง3) โ e.g. adding
+> `GitHubActionsTestLogger` again here โ NuGet restore fails with `NU1504: Duplicate
+> 'PackageReference' items found`, because `TreatWarningsAsErrors` is on.
+
+## 3. Solution config files
+
+### `./global.json`
+
+```json
+{
+ "test": {
+ "runner": "Microsoft.Testing.Platform"
+ }
+}
+```
+
+This opts `dotnet test` into the modern Microsoft.Testing.Platform (MTP) engine for the
+whole solution, instead of the legacy VSTest-based runner. It's required for the native
+`dotnet test --coverage --coverage-output-format cobertura --results-directory ...` flags
+used by `_dotnet-build-and-test.yml` on .NET 10 (see ยง5 for a related gotcha).
+
+
+### `./Directory.Packages.props`
+
+Central Package Management (CPM): every package version is declared once here, and
+individual `.csproj` files reference packages **without** a version:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+```
+
+Because the conditioned `ItemGroup` applies to **every** project where
+`IsTestProject == 'true'`, you never add these `PackageReference`s to individual test
+`.csproj` files โ that's what causes the `NU1504` duplicate error mentioned in ยง2.
+
+After adding/changing a package version here, run `dotnet restore --use-lock-file` from
+the solution folder and commit the updated `packages.lock.json` files โ CI's "Verify ALL
+lock files are up-to-date" step will fail the build otherwise.
+
+
+### `Directory.Build.props`
+
+Sets solution-wide defaults and auto-detects test projects by name:
+
+```xml
+
+
+
+ net10.0
+ enable
+ enable
+ true
+ true
+ true
+ true
+
+
+
+ true
+ true
+ false
+ false
+ false
+
+
+
+ CS1591;CS8602;CS8625;CS8618
+
+
+
+
+
+
+
+
+```
+
+Key points:
+- `RestorePackagesWithLockFile` + `ManagePackageVersionsCentrally` โ every project restores
+ from a committed `packages.lock.json`, with versions pinned centrally (see below).
+- A project is automatically treated as a test project if its name contains `.Tests.`
+ (e.g. `NugetPackages.Tests.Alpha`) or ends in `.Tests`. This also makes it non-packable
+ and relaxes a handful of nullable/doc-comment warnings that are noisy in test code.
+- Test projects get global `using AwesomeAssertions;` and `using Xunit;` for free โ no
+ need to repeat these usings in every test file.
+
+## 4. Setting up Codecov
+
+1. Sign in to [codecov.io](https://codecov.io) with your GitHub account and add the
+ repository (or org) you want coverage for.
+2. Copy the **Repository Upload Token** from the repo's Codecov settings page.
+3. Add it as a GitHub Actions secret named `CODECOV_TOKEN` (see ยง5 โ repo or org level).
+4. No `codecov.yml` is required for the default setup used here (none of this repo's
+ examples have one) โ add one only if you want to configure coverage thresholds, PR
+ comment behaviour, or ignore patterns. See the
+ [Codecov YAML reference](https://docs.codecov.com/docs/codecov-yaml).
+5. Pass `codecov-name` (and optionally `codecov-flag`) into `_dotnet-build-and-test.yml`
+ (via `dotnet-pr.yml` / `dotnet-package-pr.yml`, see ยง5) โ coverage upload is skipped
+ entirely when `codecov-name` is empty.
+
+### Known gotchas (already handled in `_dotnet-build-and-test.yml`, but good to know)
+
+- **MTP coverage filenames aren't auto-discovered by Codecov.** `dotnet test --coverage`
+ writes files named `.cobertura.xml`. Codecov's built-in file search only
+ recognises a file named exactly `cobertura.xml`, or one whose name *contains* the
+ substring `coverage`. The workflow renames each output file to
+ `coverage..cobertura.xml` after the test run to work around this โ don't remove
+ that step, and don't rely on `--coverage-output ` instead, since a fixed
+ filename is unsafe when a solution has more than one test project (parallel test-host
+ processes will race to write the same file).
+- **Harden Runner needs `storage.googleapis.com` allow-listed.** The Codecov CLI fetches
+ a signed upload URL from `ingest.codecov.io`, then uploads the actual report body
+ directly to Google Cloud Storage (`storage.googleapis.com`). If that domain isn't in
+ the job's `allowed-endpoints` list, the upload silently fails after several retries
+ with `Request failed after too many retries`.
+
+## 5. GitHub repository configuration
+
+### Secrets
+
+| Secret | Required when | Notes |
+|---|---|---|
+| `CODECOV_TOKEN` | `codecov-name` is set on `dotnet-pr.yml` / `dotnet-package-pr.yml` | From Codecov repo settings (ยง4) |
+
+Add these under **Settings โ Secrets and variables โ Actions**, at the repo or org level.
+
+### Parameters for GitHub workflow actions
+
+Include `secrets: inherit`, to ensure that the build steps have access to the `CODECOV_TOKEN`. eg
+
+```yaml
+dotnet-web-api-pr:
+ uses: saan800/github/.github/workflows/dotnet-pr.yml
+ with:
+ os: "ubuntu-latest"
+ dotnet-version: "10.0.x"
+ codecov-name: "my-project-name"
+ secrets: inherit
+```