From 5416622fa4f5955ded5bcc859866f338943b03c8 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 30 Aug 2026 14:16:12 -0700 Subject: [PATCH 1/2] Switch the coverage engine to Microsoft.Testing.Extensions.CodeCoverage ptr727/ProjectTemplate#1088 resolved its open decision to `Microsoft.Testing.Extensions.CodeCoverage` rather than `coverlet.MTP`, and hub `WORKFLOW.md` D1.6 now binds an MTP-based repo, in practice any repo on xunit.v3 4.0.0 or later, to that package at 18.9.0 or later in place of `coverlet.collector`. PlexCleaner became MTP-based in #940, so the "migration still owed rather than drift" exemption D1.6 grants a repo that is not yet MTP-based no longer covers it. #940 chose coverlet.MTP on the strength of the issue's original recommendation, before that decision was recorded. - `Microsoft.Testing.Extensions.CodeCoverage` 18.10.0 replaces `coverlet.MTP`, central-managed as an exact pin rather than a range. - The CI step becomes `dotnet test --coverage --coverage-output-format cobertura --results-directory ./coverage`, byte-identical to the hub validator's own, so the two gates cannot drift on coverage. - `--coverage-output` stays unset, since pinning one filename gives every test project in the solution the same path and the last to finish overwrites the rest. The `.cobertura.xml` default it writes instead is a name `codecov-cli`'s file finder does not match, its patterns being `*coverage*.*` and an exact `cobertura.xml`, so each report is prefixed to `coverage-.cobertura.xml` rather than renamed, keeping the guid. - `.gitignore` gains `*.cobertura.xml` and `TestResults/`, which D1.6 names. A run that passes no results directory writes `./TestResults/.cobertura.xml`. The 18.9.0 floor takes two reasons and the comments now carry both, because the first alone would justify lowering it to 18.1.0. Below 18.1.0 the extension is built against Microsoft.Testing.Platform 1.x and an 18.0.x resolution throws a `TypeLoadException` against the 2.x platform xunit.v3 4.0.0 carries, runs zero tests, and still writes a well-formed Cobertura file reporting full coverage, so only the non-zero exit says the run reported nothing. 18.9.0 is then the first release on Microsoft.Testing.Platform 2.3.x, where every test project writes into the one shared results directory the invocation names. Verified on the .NET 10.0.400 SDK: the graph resolves 18.10.0 over Microsoft.Testing.Platform 2.3.3, 239 of 239 tests pass at exit 0, the report carries real data (`line-rate="0.501"`) rather than the full-coverage artifact the floor warns about, the raw name is `.cobertura.xml` and the prefixed name matches codecov's finder, and the Docker builder's own `dotnet test` still passes 239 of 239. Also corrects four sites, and one comment this change added, that said the hub still ran the VSTest invocation and that adopting its task would be a deletion. ptr727/ProjectTemplate#1107 migrated the hub's step, so that is no longer the blocker. What blocks adoption now is that the hub's task declares no `ref` input and checks out the caller's default ref, where `publish-release.yml`'s `validate` job passes `github.sha` so the publish gate validates the exact commit being published, per D4.6. --- .github/workflows/publish-release.yml | 6 +++-- .github/workflows/test-pull-request.yml | 4 +-- .github/workflows/validate-task.yml | 16 +++++++++--- .gitignore | 3 +++ ARCHITECTURE.md | 2 +- Directory.Packages.props | 14 ++++++++--- PlexCleanerTests/PlexCleanerTests.csproj | 17 +++++++------ WORKFLOW.md | 32 ++++++++++++++++++------ 8 files changed, 68 insertions(+), 26 deletions(-) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index ad4b8d1d..b4fd177f 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -42,8 +42,10 @@ jobs: ref_name: ${{ github.ref_name }} # The same gate the PR runs, on the branch tip, running only when a publish will happen. Still this repo's - # own validate-task.yml rather than the hub's: the hub's unit-test step is the VSTest invocation that - # Microsoft.Testing.Platform rejects on the .NET 10 SDK (ptr727/ProjectTemplate#1088). + # own validate-task.yml rather than the hub's. The unit-test step matches the hub's now that + # ptr727/ProjectTemplate#1107 migrated it, but the hub's task declares no ref input and checks out the + # caller's default ref, where ref: github.sha below is what makes the publish gate validate the exact commit + # being published, per D4.6. Adopting it as-is would validate a different tree. validate: name: Validate job needs: [plan] diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 3a2b59ff..39b389f7 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -25,8 +25,8 @@ jobs: # `!github.event.deleted` skips a branch-deletion push (github.sha is all-zeros, so checkout/build fail). # The same unit-test + lint gate the publisher runs, so the PR gate and the publish gate are identical. - # Still this repo's own validate-task.yml rather than the hub's: the hub's unit-test step is the VSTest - # invocation that Microsoft.Testing.Platform rejects on the .NET 10 SDK (ptr727/ProjectTemplate#1088). + # Still this repo's own validate-task.yml rather than the hub's. The unit-test step matches the hub's now, + # but the hub's task declares no ref input, which publish-release.yml's own validate job needs (see there). validate: name: Validate job if: ${{ !github.event.deleted }} diff --git a/.github/workflows/validate-task.yml b/.github/workflows/validate-task.yml index 0d7d3ec1..d0abaf40 100644 --- a/.github/workflows/validate-task.yml +++ b/.github/workflows/validate-task.yml @@ -32,10 +32,20 @@ jobs: # Builds with TreatWarningsAsErrors, so analyzer and code-style warnings fail here. # global.json opts dotnet test into Microsoft.Testing.Platform (MTP), the .NET 10 SDK's replacement for - # the VSTest runner; --coverlet drives coverlet.MTP (coverlet.collector's VSTest data collector is silently - # ignored under MTP) to emit Cobertura XML into ./coverage/. + # the VSTest runner, and Microsoft.Testing.Extensions.CodeCoverage emits the Cobertura XML. + # --coverage-output stays unset, since pinning one filename gives every test project in the solution the + # same path and the last to finish overwrites the rest. The default .cobertura.xml it writes instead + # is a name codecov-cli's own file finder does not match, its patterns being *coverage*.* and an exact + # cobertura.xml, so each report is prefixed rather than renamed, keeping the guid that makes it unique. + # This step is byte-identical to the hub validator's own, so the two gates cannot drift on coverage. - name: Run unit tests step - run: dotnet test --coverlet --coverlet-output-format cobertura --results-directory ./coverage + run: | + set -Eeuo pipefail + dotnet test --coverage --coverage-output-format cobertura --results-directory ./coverage + for report in ./coverage/*.cobertura.xml; do + [ -e "$report" ] || continue + mv "$report" "./coverage/coverage-$(basename "$report")" + done # Report-only: fail_ci_if_error is false so a Codecov hiccup or an absent token never fails the gate. - name: Upload coverage to Codecov step diff --git a/.gitignore b/.gitignore index 9ef45ac0..97a824ab 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ .artifacts .codex coverage/ +# MTP writes .cobertura.xml, into ./TestResults when a run names no --results-directory. +*.cobertura.xml +TestResults/ .DS_Store *.log diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 42a5243c..783ad2f9 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -381,7 +381,7 @@ Two entry-point workflows, whose build and publish jobs are thin callers of the - **test-pull-request.yml**: CI on every push. `validate` (this repo's own `validate-task.yml`, unit tests plus the full lint set) and `smoke-build` (the hub's `build-release-task.yml` with `smoke: true`, which builds both targets and hard-disables every publish path) feed the `Check pull request workflow status` aggregator, whose name is the ruleset-bound required check. - **publish-release.yml**: the sole publisher (weekly `schedule` plus `workflow_dispatch`). `plan` (the hub's `publish-plan-task.yml`) makes the release-gate decision once, `validate` re-runs the gate on the branch tip, `publish` (the hub's `build-release-task.yml`) versions once and builds the executable 7z and the multi-arch Docker image before cutting the GitHub release, and `publish-docker-readme` (the hub's `publish-docker-readme-task.yml`) pushes the Docker Hub overview on `main`. -- **validate-task.yml** is the one task still carried here rather than reached at the hub, because the hub's own unit-test step is the VSTest invocation that Microsoft.Testing.Platform rejects on the .NET 10 SDK (ptr727/ProjectTemplate#1088). It converges once that lands. +- **validate-task.yml** is the one task still carried here rather than reached at the hub. Its unit-test step now matches the hub's, which ptr727/ProjectTemplate#1107 migrated to Microsoft.Testing.Platform, so that is no longer the blocker. The hub's task declares no `ref` input and checks out the caller's default ref, while the `validate` job in `publish-release.yml` passes `github.sha` so the publish gate validates the exact commit being published (D4.6), so adopting it as-is would validate a different tree. - The executable and Docker builds run the hub's default hooks rather than repo-specific ones: `dotnet_publish_project` names `./PlexCleaner/PlexCleaner.csproj`, which the default publishes across the 7-runtime matrix and archives as `PlexCleaner.7z`, and `docker_image` names `ptr727/plexcleaner`, which the default builds from `./Docker/Dockerfile` for `linux/amd64,linux/arm64` on a `main` publish. - Version info: `version.json` with Nerdbank.GitVersioning format. The hub's `get-version-task.yml`, reached from inside `build-release-task.yml`, surfaces `SemVer2`, the assembly versions, and `GitCommitId` (used to pin the release `target_commitish`). - Branches: `main` (stable releases, `latest`), `develop` (pre-releases, `develop`). diff --git a/Directory.Packages.props b/Directory.Packages.props index e74154e3..892f0a58 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,11 +2,19 @@ - - + + diff --git a/PlexCleanerTests/PlexCleanerTests.csproj b/PlexCleanerTests/PlexCleanerTests.csproj index 6ff395ba..9f9072ee 100644 --- a/PlexCleanerTests/PlexCleanerTests.csproj +++ b/PlexCleanerTests/PlexCleanerTests.csproj @@ -10,14 +10,15 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/WORKFLOW.md b/WORKFLOW.md index 24e7fc32..b96cda1b 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -30,9 +30,12 @@ Dependabot pull requests merge themselves once their checks pass. The **build, publish, plan, merge-bot and Docker-Hub-overview jobs are thin callers of hub-hosted reusable tasks** in `ptr727/ProjectTemplate`, reached by a commit-SHA `uses:` pin that Dependabot bumps, per that repo's `docs/reusable-workflows.md`. This repo carries no copy of them. The one task still carried here is -`validate-task.yml`, because the hub's own unit-test step is the VSTest invocation that -Microsoft.Testing.Platform rejects on the .NET 10 SDK (ptr727/ProjectTemplate#1088); it converges once that -lands. +`validate-task.yml`. Its unit-test step is now byte-identical to the hub's, which +ptr727/ProjectTemplate#1107 migrated to Microsoft.Testing.Platform, so that is no longer what blocks +adoption. What blocks it is that the hub's task declares no `ref` input and checks out the caller's default +ref, where the `validate` job in `publish-release.yml` passes `github.sha` so the publish gate validates the +exact commit being published, which is D4.6. Adopting the hub task without a `ref` input there would +silently validate a different tree. ### Glossary @@ -333,8 +336,20 @@ Each is a **MUST**, stated as input -> output plus the failure it prevents. - **D1.2 Unit tests always run.** Output: `validate-task`'s `unit-test` job runs `dotnet test` (build with `TreatWarningsAsErrors`, so analyzer/style warnings fail here). [`global.json`](./global.json) opts the run into Microsoft.Testing.Platform, which the .NET 10 SDK requires of a test project carrying - `Microsoft.Testing.Platform.MSBuild`, and `--coverlet --coverlet-output-format cobertura` drives - `coverlet.MTP` to emit the Cobertura XML the Codecov upload reads. + `Microsoft.Testing.Platform.MSBuild`, and `--coverage --coverage-output-format cobertura` drives + `Microsoft.Testing.Extensions.CodeCoverage` to emit the Cobertura XML the Codecov upload reads. Two details + of that invocation are load-bearing and neither fails the job on its own, so both are asserted in 5A. + `--coverage-output` stays unset, because pinning one filename gives every test project in the solution the + same path and the last to finish overwrites the rest. The `.cobertura.xml` default it writes instead is + a name `codecov-cli`'s own file finder does not match (its patterns are `*coverage*.*` and an exact + `cobertura.xml`), so the step prefixes each report to `coverage-.cobertura.xml`, keeping the guid that + makes it unique. The extension is pinned at or above **18.9.0**, for two reasons rather than one. Below + 18.1.0 it is built against Microsoft.Testing.Platform 1.x, so an 18.0.x resolution throws a + `TypeLoadException` against the 2.x platform xunit.v3 4.0.0 carries, runs zero tests, and still writes a + well-formed Cobertura file reporting full coverage, leaving only the non-zero exit to say the run reported + nothing. 18.9.0 is then the first release on Microsoft.Testing.Platform 2.3.x, where every test project + writes into the one shared `--results-directory` the invocation names rather than resolving that relative + path per project, which is what the rename loop's glob depends on. - **D1.3 Lint enforces the editor checks in CI.** Output: `validate-task`'s `lint` job runs CSharpier check, `dotnet format style --verify-no-changes`, `markdownlint-cli2`, `cspell` on the user-facing docs (README, HISTORY), `ruff` and `mypy` over the `RegressionTests` Python tooling, `actionlint` (which shellchecks every @@ -507,8 +522,11 @@ Read the workflow files plus `version.json` and assert the fact behind each appl exists under `.github/workflows/`. - **D1:** CI runs on `push` with no paths filter; `validate` + `smoke-build` (both targets, `smoke: true`) run; every build `upload-artifact` is gated `!smoke`; `global.json` declares - `test.runner = Microsoft.Testing.Platform` and the unit-test step passes `--coverlet - --coverlet-output-format cobertura`; `lint` runs CSharpier, `dotnet format style`, markdownlint, cspell on + `test.runner = Microsoft.Testing.Platform`, the unit-test step passes `--coverage + --coverage-output-format cobertura` with no `--coverage-output`, and prefixes each report to + `coverage-.cobertura.xml` before the upload reads the directory; + `Directory.Packages.props` pins `Microsoft.Testing.Extensions.CodeCoverage` at 18.9.0 or above; + `lint` runs CSharpier, `dotnet format style`, markdownlint, cspell on README/HISTORY, ruff, mypy, actionlint, editorconfig-checker; the aggregator `needs:` both and blocks on non-success. - **D2:** the hub task's `validate-release` job runs before the build jobs and they `needs:` it; it checks From 150840aefef48fa51bbbf5b5599aad377da9286c Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 30 Aug 2026 14:24:37 -0700 Subject: [PATCH 2/2] Name the package this change actually replaces Both comments described the extension as replacing `coverlet.collector`, which is D1.2's own wording for the fleet baseline but not what happens here: this repository was on `coverlet.MTP`, chosen in #940 before ptr727/ProjectTemplate#1088 resolved the fleet on this package. A reader of these files sees a replacement that did not occur. Both now name `coverlet.MTP` as the thing displaced and keep `coverlet.collector` as the baseline further back, so the comment matches the diff and still connects to D1.2's wording. The test project's comment also drops the sentence about `xunit.runner.visualstudio`, which #940 removed and this change does not touch, and records instead that the compile-time closure requirement is enforced by a build failure, since the generated `SelfRegisteredExtensions.cs` calls into the package directly. --- Directory.Packages.props | 23 +++++++++++++---------- PlexCleanerTests/PlexCleanerTests.csproj | 17 +++++++++-------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 892f0a58..ef9a5416 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,16 +4,19 @@ - + diff --git a/PlexCleanerTests/PlexCleanerTests.csproj b/PlexCleanerTests/PlexCleanerTests.csproj index 9f9072ee..3269f250 100644 --- a/PlexCleanerTests/PlexCleanerTests.csproj +++ b/PlexCleanerTests/PlexCleanerTests.csproj @@ -10,14 +10,15 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - +