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..ef9a5416 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,11 +2,22 @@ - - + + diff --git a/PlexCleanerTests/PlexCleanerTests.csproj b/PlexCleanerTests/PlexCleanerTests.csproj index 6ff395ba..3269f250 100644 --- a/PlexCleanerTests/PlexCleanerTests.csproj +++ b/PlexCleanerTests/PlexCleanerTests.csproj @@ -10,14 +10,16 @@ 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