From 9e9da599d8080a81bcc93350f50ac0e75110e33c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Gro=C3=9F?= Date: Sun, 16 Aug 2026 15:47:57 +0200 Subject: [PATCH] test: guard PrivateAssets=all per shipping project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SBOM filter test flattens all three src/ projects into one set of package ids, so it only asks whether a name is somewhere private. Drop PrivateAssets=all from a single csproj and the other two keep the id in that set: every check stays green while that one nuspec starts declaring the dependency and its consumers restore the whole subtree behind it. For Microsoft.EntityFrameworkCore.Design that subtree is ~45 MSBuild/Roslyn components carrying System.Security.Cryptography.Xml 9.0.0 and its eight high advisories. "Consumers never receive it" is the premise NU1903 staying a warning rests on (Directory.Build.props) and the reason dependabot.yml ignores that package under src/ — and nothing checked it per project. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 10 +++++ .../PackagingConventionTests.cs | 45 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 31dd8d3..fafff60 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -66,6 +66,16 @@ declaring one, handing consumers vulnerability alerts for code they never receiv SBOM that misdescribes the package is worse than none. Publishing one is a courtesy: the CRA's SBOM duty falls on commercial consumers for their own products, not on this package. +That filter test flattens the three projects into one set of ids, so it only ever asks whether a +name is *somewhere* private; a second guard asserts a reference marked `PrivateAssets=all` in one +project is marked so in **every** project. Dropping it from a single csproj is invisible to every +other check — the package still builds, the SBOM test still passes — while that one nuspec starts +declaring the dependency and its consumers restore the whole subtree behind it. That is what makes +"consumers never receive it" true, and it is the premise `NU1903` staying a warning rests on +(`Directory.Build.props`), along with the `System.Security.Cryptography.Xml` ignore under `src/` in +`.github/dependabot.yml`. Confirmed by removing the metadata from one satellite: a consumer of the +resulting package inherits all eight advisories. + `.github/workflows/release.yml` publishes on a `v*` tag. `Directory.Build.props` stays the single source of truth: the tag is verified against it rather than driving it, packages are discovered from the pack output (so a future satellite needs no workflow edit), and pushes use `--skip-duplicate` so diff --git a/test/EFCore.ComplexIndexes.Tests/PackagingConventionTests.cs b/test/EFCore.ComplexIndexes.Tests/PackagingConventionTests.cs index 429fb16..36cad91 100644 --- a/test/EFCore.ComplexIndexes.Tests/PackagingConventionTests.cs +++ b/test/EFCore.ComplexIndexes.Tests/PackagingConventionTests.cs @@ -131,6 +131,51 @@ public void Sbom_filter_covers_every_private_reference() + "consumers never receive."); } + [TestMethod(DisplayName = "A reference marked build-only in one project is build-only in every project")] + public void Build_only_references_are_private_in_every_project() + { + // The test above flattens all three projects into one set of ids, so it only ever asks + // whether a name is *somewhere* private. Drop PrivateAssets=all from a single csproj and the + // other two keep that name in the set: the SBOM test stays green while that one package's + // nuspec starts declaring the dependency, and its consumers restore the whole subtree behind + // it. For Microsoft.EntityFrameworkCore.Design that is ~45 MSBuild/Roslyn components — and + // "consumers never receive it" is the stated reason NU1903 stays a warning + // (Directory.Build.props) and the reason .github/dependabot.yml ignores + // System.Security.Cryptography.Xml under src/. Nothing else checks it per project. + var references = Projects + .Select(project => ( + project, + refs: XDocument.Load(project.ProjectFile) + .Descendants("PackageReference") + .Where(reference => reference.Attribute("Include") is not null) + .Select(reference => (Id: reference.Attribute("Include")!.Value, + IsBuildOnly: IsPrivate(reference))) + .ToList())) + .ToList(); + + var buildOnly = references.SelectMany(entry => entry.refs) + .Where(reference => reference.IsBuildOnly) + .Select(reference => reference.Id) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + Assert.IsNotEmpty(buildOnly, "Expected at least one PrivateAssets=all reference — has the packaging changed?"); + + var shipped = references + .SelectMany(entry => entry.refs + .Where(reference => !reference.IsBuildOnly && buildOnly.Contains(reference.Id)) + .Select(reference => $"{entry.project} -> {reference.Id}")) + .OrderBy(entry => entry, StringComparer.Ordinal) + .ToList(); + + Assert.IsEmpty( + shipped, + $"{string.Join("; ", shipped)} — marked PrivateAssets=all elsewhere under src/ but not here, " + + "so this package's nuspec declares it and its consumers restore that dependency's entire " + + "subtree. Mark it PrivateAssets=all here too; if a consumer genuinely needs it at runtime, " + + "make it public in every project and drop it from the SBOM --exclude-filter, because it is " + + "then a dependency they really do take on."); + } + /// /// Package validation compares each pack against PackageValidationBaselineVersion. A /// baseline left behind stops seeing API added since it — a member introduced in 5.1 and