From fb7ba4830527b32f393bf52710341501e542becd Mon Sep 17 00:00:00 2001 From: Stuart Meeks Date: Thu, 20 Aug 2026 08:07:00 +0000 Subject: [PATCH] chore: adopt Central Package Management and collect coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit STANDARD.md sections 1.2, 1.3 and 2.6. Versions move from the three project files into Directory.Packages.props with CentralPackageVersionOverrideEnabled=false, so a stray inline Version= is a hard build error instead of being silently ignored — which is the drift the file exists to prevent. The per-TFM floors survive: CPM re-evaluates the conditional ItemGroups per target framework during the inner build, so net8.0 still floors at 8.0.2 / 8.0.1 and net10.0 at 10.0.11. No floor was raised; that is consumer-visible and belongs in its own reviewed commit. Proven behaviour-preserving rather than asserted. Packed before and after from clean obj/ at the same commit (9caf824): the .nuspec, both lib/ assemblies, both XML doc files, both symbol PDBs, README and icon are byte-identical. The only differences anywhere in either package are the .psmdcp filename, which NuGet regenerates per pack, and the _rels/.rels entry that names it — the psmdcp *content* is identical too. A first attempt appeared to show the assemblies differing. It did not: the "before" pack had reused a stale obj/, so the two builds embedded different git commit SHAs via SourceLink, which moved the MVID and the deterministic PE stamp with them. Decoding the differing bytes showed the two SHAs directly. Rebuilding both from clean obj/ removed the difference entirely — worth recording, because that failure mode looks exactly like a real packaging regression. Thirteen properties restated in every csproj move to Directory.Build.props (1.2). The demo project opts GenerateDocumentationFile back off: it is not shipped, so its types are not public surface, and demanding XML docs on sample code would fail the build under TreatWarningsAsErrors. Same reasoning as 2.7 gives the test project. Coverage (2.6) adds Microsoft.Testing.Extensions.CodeCoverage AND has CI invoke it, with a per-platform artifact upload. A referenced-but-never-invoked collector is worse than none — it reads as coverage in the dependency list while producing no data. Verified locally: one .coverage file per TFM. Also corrects a comment in dependabot.yml that pointed at "the csproj" for the floor rationale, which moved to Directory.Packages.props in this change. Verified: Release build 0 warnings, 392 tests passing across net8.0 and net10.0, coverage collected on both. Co-Authored-By: Claude Opus 5 (1M context) --- .github/dependabot.yml | 2 +- .github/workflows/ci.yml | 14 +++- CHANGELOG.md | 3 + Directory.Build.props | 19 +++++ Directory.Packages.props | 78 +++++++++++++++++++ ...tion.SpectreConsole.SelfUpdate.Demo.csproj | 10 ++- ...Iteration.SpectreConsole.SelfUpdate.csproj | 49 +++++------- ...ion.SpectreConsole.SelfUpdate.Tests.csproj | 15 ++-- 8 files changed, 152 insertions(+), 38 deletions(-) create mode 100644 Directory.Packages.props diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5180f6e..d7063f8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -23,7 +23,7 @@ updates: - dependencies - nuget # ------------------------------------------------------------------------- - # These two carry deliberate per-TFM floors (see the csproj comment): a + # These two carry deliberate per-TFM floors (see Directory.Packages.props): a # net8.0 consumer must stay on its own 8.0.x servicing line, so an # 8.x -> 10.x major PR is never mergeable and would just be weekly noise. # diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9ff46c..5c14aca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,8 +102,20 @@ jobs: # Tests run across every shipped TFM (STANDARD.md 2.3). No --no-build: this job # does not share a filesystem with `build`, and rebuilding is cheaper and less # fragile than shipping obj/ between jobs. + # `-- --coverage` passes through to Microsoft.Testing.Platform's coverage + # extension (STANDARD.md 2.6). Referencing a collector without invoking it is + # worse than none: it reads as coverage in the dependency list while producing + # no data. One .coverage file lands per TFM, under bin/*/TestResults. - name: Test - run: dotnet test --configuration Release --verbosity normal + run: dotnet test --configuration Release --verbosity normal -- --coverage + + - name: Upload coverage + if: always() + uses: actions/upload-artifact@v7 + with: + name: coverage-${{ matrix.os }} + path: '**/TestResults/*.coverage' + if-no-files-found: warn # THE required status check. Aggregates everything above so the ruleset never has to # know the matrix shape. `if: always()` is essential — without it the gate is skipped diff --git a/CHANGELOG.md b/CHANGELOG.md index 137a0e4..59594a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,8 +17,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **`.github/dependabot.yml` rewritten.** Minor and patch bumps are grouped into one PR per ecosystem; majors are deliberately left ungrouped so each arrives separately and stays open for review. The two runtime-aligned packages carrying per-TFM floors (`Microsoft.Extensions.DependencyInjection.Abstractions`, `Microsoft.Extensions.Http`) are now under `ignore` for major updates, because an 8.x → 10.x bump on the net8 floor is never mergeable and was weekly noise. - **`global.json` now pins the SDK**, not just the Microsoft.Testing.Platform runner: `10.0.100` with `rollForward: latestFeature`. An unpinned SDK means a contributor on an older one gets different analyzer results from CI, and with `TreatWarningsAsErrors` that is a build which fails for them and passes for everyone else. - **`.gitignore` and `.editorconfig` replaced with the canonical copies.** The `.editorconfig` change is one line that matters: the private-field naming rule had `applicable_kinds = field`, and a `const` *is* a field, so the rule demanded `_nonceSize` for `private const int NonceSize`. An empty `required_modifiers` scopes it to instance fields. Nothing enforces these rules at build time yet (`EnforceCodeStyleInBuild` is off), so this is a no-op for the build today and correct for when it is not. +- **Central Package Management adopted.** Package versions move out of the three project files into a root `Directory.Packages.props`, with `CentralPackageVersionOverrideEnabled=false` so a stray inline `Version=` is a hard build error rather than being silently ignored. The per-TFM floors survive intact — CPM re-evaluates the conditional `ItemGroup`s per target framework during the inner build, so `net8.0` still floors at 8.0.2 / 8.0.1 and `net10.0` at 10.0.11. **Verified behaviour-preserving:** packed before and after from clean `obj/` at the same commit, the `.nuspec`, both `lib/` assemblies, both XML documentation files, both symbol PDBs, the README and the icon are byte-identical; the only differences in either package are the `.psmdcp` filename, which NuGet regenerates per pack, and the `_rels/.rels` entry that names it. No dependency floor was raised — that is consumer-visible and belongs in its own reviewed commit. +- **Properties every project restated now live in `Directory.Build.props`.** `ImplicitUsings`, `SatelliteResourceLanguages`, `AnalysisLevel`, `GenerateDocumentationFile`, `EnablePackageValidation`, `IncludeSymbols`, `SymbolPackageFormat`, `DebugType`, `PublishRepositoryUrl`, `EmbedUntrackedSources`, `ContinuousIntegrationBuild`, `PackageLicenseExpression` and `Copyright`. Duplicated settings are how these repos drifted apart in the first place; each csproj now carries only what is genuinely specific to it. The demo project opts `GenerateDocumentationFile` back off — it is not shipped, so its types are not public surface and demanding XML docs on sample code would fail the build under `TreatWarningsAsErrors`. ### Added +- **Code coverage is now collected.** Adds `Microsoft.Testing.Extensions.CodeCoverage` and — the part that actually matters — has CI invoke it (`dotnet test … -- --coverage`) and upload the result per platform. A collector that is referenced but never invoked is worse than none: it reads as coverage in the dependency list while producing no data. One `.coverage` file lands per target framework, verified locally on both. - **`SECURITY.md`**, with a scope section specific to this library: SHA-256 verification establishes integrity but not authenticity (the expected hash ships from the same release as the asset, and there is no signature checking); `AllowInsecureManifestSource` and `UseDefaultSha256Verifier = false` are documented opt-outs that defeat verification by design; archive path-traversal defence is the framework's `ZipFile`/`TarFile` guard rather than this library's; and `GhCliReleaseSource` trusts whatever `gh` is on `PATH`. Stating the boundary is the point — a report that only restates a documented limitation is not a vulnerability. - **`CONTRIBUTING.md`, `.github/PULL_REQUEST_TEMPLATE.md` and `CLAUDE.md`.** `CLAUDE.md` records the constraints an agent would otherwise violate here — why the three-platform matrix is load-bearing, why one install-lock test returns early on Windows by design, and that `PackageValidationBaselineVersion` tracks the last shipped release. diff --git a/Directory.Build.props b/Directory.Build.props index 08993bb..65a7154 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,25 @@ + enable + enable + en + latest + true + true + true + snupkg + portable + true + true + true + MIT + © Stuart Meeks true Stuart Meeks Next Iteration diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..568116f --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,78 @@ + + + + true + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo/NextIteration.SpectreConsole.SelfUpdate.Demo/NextIteration.SpectreConsole.SelfUpdate.Demo.csproj b/demo/NextIteration.SpectreConsole.SelfUpdate.Demo/NextIteration.SpectreConsole.SelfUpdate.Demo.csproj index ada0496..25222a7 100644 --- a/demo/NextIteration.SpectreConsole.SelfUpdate.Demo/NextIteration.SpectreConsole.SelfUpdate.Demo.csproj +++ b/demo/NextIteration.SpectreConsole.SelfUpdate.Demo/NextIteration.SpectreConsole.SelfUpdate.Demo.csproj @@ -3,9 +3,15 @@ Exe net10.0 - enable - enable false + + false diff --git a/src/NextIteration.SpectreConsole.SelfUpdate/NextIteration.SpectreConsole.SelfUpdate.csproj b/src/NextIteration.SpectreConsole.SelfUpdate/NextIteration.SpectreConsole.SelfUpdate.csproj index fe53967..201b3fb 100644 --- a/src/NextIteration.SpectreConsole.SelfUpdate/NextIteration.SpectreConsole.SelfUpdate.csproj +++ b/src/NextIteration.SpectreConsole.SelfUpdate/NextIteration.SpectreConsole.SelfUpdate.csproj @@ -1,72 +1,63 @@ + net8.0;net10.0 - enable - enable - en - true - latest NextIteration.SpectreConsole.SelfUpdate 0.3.1 - Stuart Meeks Self-update for Spectre.Console CLIs: pluggable update sources (GitHub Releases over HTTP, GitHub Releases via gh CLI for private repos, generic HTTPS manifest, custom), SHA-256 verification, atomic file swap, and a drop-in `update` command. true $(MSBuildThisFileDirectory)..\..\artifacts\packages true - MIT README.md https://github.com/StuartMeeks/NextIteration.SpectreConsole.SelfUpdate https://github.com/StuartMeeks/NextIteration.SpectreConsole.SelfUpdate.git git spectre;cli;self-update;auto-update;github-releases icon.png - © Stuart Meeks - true 0.3.0 - true - true - true - snupkg - portable - true + + + + + + + + + - - + + - - - - - - - - - - + + diff --git a/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/NextIteration.SpectreConsole.SelfUpdate.Tests.csproj b/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/NextIteration.SpectreConsole.SelfUpdate.Tests.csproj index 1ad62f6..e0263f8 100644 --- a/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/NextIteration.SpectreConsole.SelfUpdate.Tests.csproj +++ b/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/NextIteration.SpectreConsole.SelfUpdate.Tests.csproj @@ -3,13 +3,11 @@ net8.0;net10.0 Exe - enable - enable false true false @@ -35,8 +33,15 @@ is pointed at MTP by the `test.runner` setting in global.json. --> - - + + + +