Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 72 additions & 2 deletions .github/scripts/inspect-packed-nupkgs.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Asserts the packed .nupkg contents for all seven publishable Compono packages
# Asserts the packed .nupkg contents for every publishable Compono package
# match ADR-0031's package-readiness bar (PLAN-0008 Phase 0's package-contents-
# inspection CI job): the .nupkg's file listing matches the expected shape
# exactly (an allowlist, not a denylist - nothing unexpected snuck in, not just
Expand Down Expand Up @@ -168,6 +168,65 @@ assert_dependency_range() {
fi
}

# Compono.Logging's Microsoft.Extensions.Logging.Abstractions dependency is the one exception to
# every other integration package's single, TFM-uniform third-party range: Directory.Packages.props
# conditions it per $(TargetFramework) (net8.0/net9.0/net10.0 each track a different BCL logging-
# abstractions version), and net11.0 carries no such dependency at all in the packed .nuspec - the
# type is satisfied by net11.0's own shared framework, so an explicit PackageReference produces no
# packed dependency entry for that TFM (confirmed against a real local pack, not assumed).
# assert_dependency_range's single authoritative_json blob (evaluated with no $(TargetFramework) set)
# can't see this per-TFM branching, so this sibling function re-evaluates Directory.Packages.props
# once per TFM instead of once per package.
assert_dependency_range_per_tfm() {
local nuspec="$1"
local pkg_name="$2"
local dep_id="$3"
local packages_props="$4"
local tfm

for tfm in net8.0 net9.0 net10.0; do
local expected_range
expected_range=$(dotnet msbuild "$packages_props" -nologo -getItem:PackageVersion -p:TargetFramework="$tfm" 2>/dev/null \
| jq -r --arg id "$dep_id" '.Items.PackageVersion[]? | select(.Identity == $id) | .Version' | head -1)
if [ -z "$expected_range" ]; then
echo "FAIL: could not determine authoritative PackageVersion for $dep_id ($tfm) in Directory.Packages.props" >&2
fail=1
continue
fi

local actual_range
actual_range=$(awk -v tfm="$tfm" -v dep="$dep_id" '
$0 ~ "<group targetFramework=\"" tfm "\"" { in_group=1 }
in_group && $0 ~ "</group>" { in_group=0 }
in_group && $0 ~ "id=\"" dep "\"" { print; exit }
' "$nuspec" | sed -E "s/.*id=\"${dep_id}\" version=\"([^\"]*)\".*/\1/")

if [ "$actual_range" = "$expected_range" ]; then
echo "OK: $pkg_name's .nuspec constrains $dep_id to the intended tested range $actual_range for $tfm (matches Directory.Packages.props)"
else
echo "FAIL: $pkg_name's .nuspec dependency on $dep_id for $tfm is '${actual_range:-<absent>}', expected the intended tested range '$expected_range' (from Directory.Packages.props)" >&2
fail=1
fi
done

# net11.0: the BCL's own shared framework satisfies this dependency for that TFM - no packed
# <dependency> entry should exist at all. Asserted explicitly (not just left unchecked) so a
# regression in either direction - the framework un-bundling it, or a future change accidentally
# reintroducing an explicit dependency - fails loudly instead of silently.
local net11_entry
net11_entry=$(awk -v dep="$dep_id" '
$0 ~ "<group targetFramework=\"net11.0\"" { in_group=1 }
in_group && $0 ~ "</group>" { in_group=0 }
in_group && $0 ~ "id=\"" dep "\"" { print; exit }
' "$nuspec")
if [ -z "$net11_entry" ]; then
echo "OK: $pkg_name's .nuspec has no $dep_id dependency for net11.0 (satisfied by net11.0's own shared framework)"
else
echo "FAIL: $pkg_name's .nuspec unexpectedly declares a $dep_id dependency for net11.0: $net11_entry" >&2
fail=1
fi
}

main() {
local pack_output="${1:?usage: inspect-packed-nupkgs.sh <pack-output-dir>}"
local script_dir
Expand All @@ -186,7 +245,7 @@ main() {
}

local pkg nupkg extract_dir extra_paths nuspec
for pkg in Compono Compono.XunitV3 Compono.NSubstitute Compono.Bogus Compono.TUnit Compono.TestDoubles Compono.DependencyInjection Compono.Http Compono.MSTest Compono.NUnit; do
for pkg in Compono Compono.XunitV3 Compono.NSubstitute Compono.Bogus Compono.TUnit Compono.TestDoubles Compono.DependencyInjection Compono.Http Compono.Logging Compono.MSTest Compono.NUnit; do
nupkg=$(find "$pack_output" -maxdepth 1 -iname "${pkg}.[0-9]*.nupkg" | head -1)
if [ -z "$nupkg" ]; then
echo "FAIL: no .nupkg found for $pkg in $pack_output" >&2
Expand All @@ -203,6 +262,12 @@ main() {
# ComponoGeneratedTestDoubles (ADR-0043 Amendment 4, Finding F) - without it,
# AnalyzerConfigOptionsProvider can never see a consumer's MSBuild setting for the opt-in.
extra_paths=$'analyzers/dotnet/cs/Compono.Generators.dll\nbuild/Compono.props\nbuildTransitive/Compono.props'
elif [ "$pkg" = "Compono.Logging" ]; then
# build/ + buildTransitive/ Compono.Logging.props: defaults ComponoGeneratedLogging to true
# (ADR-0055 Amendment 3) - no analyzers/ entry here, unlike Compono above: logging activation
# generation lives inside the existing Compono.Generators (packed only into Compono.nupkg),
# reached transitively through this package's Compono dependency, not a second analyzer DLL.
extra_paths=$'build/Compono.Logging.props\nbuildTransitive/Compono.Logging.props'
fi
assert_exact_file_listing "$nupkg" "$pkg" "$extra_paths"

Expand Down Expand Up @@ -259,6 +324,11 @@ main() {
# over System.Net.Http (BCL) - nothing else to range-assert here (ADR-0051 "Minimal
# dependency graph").
;;
Compono.Logging)
assert_manifest_field "$nuspec" "$pkg" "title" "Compono — Microsoft.Extensions.Logging Testing Support"
assert_exact_pin_dependency "$nuspec" "$pkg" "Compono"
assert_dependency_range_per_tfm "$nuspec" "$pkg" "Microsoft.Extensions.Logging.Abstractions" "$packages_props"
;;
Compono.MSTest)
assert_manifest_field "$nuspec" "$pkg" "title" "Compono — MSTest Integration"
assert_exact_pin_dependency "$nuspec" "$pkg" "Compono"
Expand Down
65 changes: 65 additions & 0 deletions .github/scripts/inspect-packed-nupkgs.tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,71 @@ else
tests_failed=1
fi

# 6a. assert_dependency_range_per_tfm: Compono.Logging's real shape - per-TFM ranges for net8/9/10,
# absent for net11.0 (satisfied by net11.0's own shared framework). Uses the real repository
# Directory.Packages.props (via dotnet msbuild -p:TargetFramework=X per TFM, the same mechanism the
# real function uses) rather than a synthetic fixture, since the whole point of this coverage is
# proving the function reads the *actual* per-TFM authoritative values correctly - a synthetic props
# file would just test that the function echoes back whatever synthetic value it was given.
make_multi_tfm_nuspec() {
local path="$1"
local dep_id="$2"
local net8_range="$3"
local net9_range="$4"
local net10_range="$5"
local net11_has_entry="$6" # "yes" or "no"
local net11_line=""
if [ "$net11_has_entry" = "yes" ]; then
net11_line=" <dependency id=\"$dep_id\" version=\"[99.0.0, 100.0.0)\" exclude=\"Build,Analyzers\" />"
fi
cat >"$path" <<EOF
<?xml version="1.0"?>
<package>
<metadata>
<dependencies>
<group targetFramework="net8.0">
<dependency id="$dep_id" version="$net8_range" exclude="Build,Analyzers" />
</group>
<group targetFramework="net9.0">
<dependency id="$dep_id" version="$net9_range" exclude="Build,Analyzers" />
</group>
<group targetFramework="net10.0">
<dependency id="$dep_id" version="$net10_range" exclude="Build,Analyzers" />
</group>
<group targetFramework="net11.0">
$net11_line
</group>
</dependencies>
</metadata>
</package>
EOF
}

repo_root="$script_dir/../.."
real_props="$repo_root/Directory.Packages.props"
real_net8=$(dotnet msbuild "$real_props" -nologo -getItem:PackageVersion -p:TargetFramework=net8.0 2>/dev/null | jq -r '.Items.PackageVersion[]? | select(.Identity == "Microsoft.Extensions.Logging.Abstractions") | .Version')
real_net9=$(dotnet msbuild "$real_props" -nologo -getItem:PackageVersion -p:TargetFramework=net9.0 2>/dev/null | jq -r '.Items.PackageVersion[]? | select(.Identity == "Microsoft.Extensions.Logging.Abstractions") | .Version')
real_net10=$(dotnet msbuild "$real_props" -nologo -getItem:PackageVersion -p:TargetFramework=net10.0 2>/dev/null | jq -r '.Items.PackageVersion[]? | select(.Identity == "Microsoft.Extensions.Logging.Abstractions") | .Version')

nuspec_per_tfm_matching="$work_dir/per-tfm-matching.nuspec"
make_multi_tfm_nuspec "$nuspec_per_tfm_matching" "Microsoft.Extensions.Logging.Abstractions" "$real_net8" "$real_net9" "$real_net10" "no"
expect_pass "per-TFM range matching Directory.Packages.props for net8/9/10, absent for net11.0" \
assert_dependency_range_per_tfm "$nuspec_per_tfm_matching" "Compono.Logging" "Microsoft.Extensions.Logging.Abstractions" "$real_props"

# 6b. A stale net9.0 range must fail even though net8.0/net10.0 still match - proves each TFM is
# checked independently, not just "at least one matches".
nuspec_per_tfm_stale_net9="$work_dir/per-tfm-stale-net9.nuspec"
make_multi_tfm_nuspec "$nuspec_per_tfm_stale_net9" "Microsoft.Extensions.Logging.Abstractions" "$real_net8" "[0.0.1, 0.0.2)" "$real_net10" "no"
expect_fail "per-TFM check fails when only net9.0's range disagrees" \
assert_dependency_range_per_tfm "$nuspec_per_tfm_stale_net9" "Compono.Logging" "Microsoft.Extensions.Logging.Abstractions" "$real_props"

# 6c. An unexpected net11.0 dependency entry must fail - proves the "must be absent" direction is
# actually checked, not merely unchecked.
nuspec_per_tfm_unexpected_net11="$work_dir/per-tfm-unexpected-net11.nuspec"
make_multi_tfm_nuspec "$nuspec_per_tfm_unexpected_net11" "Microsoft.Extensions.Logging.Abstractions" "$real_net8" "$real_net9" "$real_net10" "yes"
expect_fail "per-TFM check fails when net11.0 unexpectedly declares the dependency" \
assert_dependency_range_per_tfm "$nuspec_per_tfm_unexpected_net11" "Compono.Logging" "Microsoft.Extensions.Logging.Abstractions" "$real_props"

# 6. Sanity check against the real repository policy file, so this test suite
# breaks if Directory.Packages.props' shape (Identity/Version JSON) ever stops
# being what the validator expects - independent of any specific package.
Expand Down
17 changes: 4 additions & 13 deletions .github/workflows/package-validation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ jobs:
# CS1591 enforcement.
BREAKING_CHANGE: ${{ contains(github.event.pull_request.labels.*.name, 'breaking-change') }}
PACK_OUTPUT: ${{ github.workspace }}/artifacts/package-validation
# Single authoritative publishable-package list (PLAN-0061 Phase 1) - the baseline-lookup,
# pack, and CS1591-enforcement steps below all derive from this one job-level env var instead
# of each repeating the same 11-package literal, so adding/removing a package can't drift
# across the three independently. A job-level `env:` entry is injected into every step's own
# Single authoritative publishable-package list (PLAN-0061 Phase 1) - the baseline-lookup and
# pack steps below both derive from this one job-level env var instead of each repeating the
# same 11-package literal, so adding/removing a package can't drift across the two
# independently. A job-level `env:` entry is injected into every step's own
# process environment by GitHub Actions itself, so this survives across the separate `run:`
# steps below (each its own shell process) with no extra plumbing - deliberately not a Bash
# array, which would only live for the one step that declared it.
Expand Down Expand Up @@ -108,15 +108,6 @@ jobs:
pack_one "src/$pkg/$pkg.csproj" "BASELINE_$(echo "$pkg" | tr '.' '_')"
done

- name: Enforce XML doc comments (CS1591) on publishable packages
run: |
set -euo pipefail
for pkg in $PACKAGES; do
csproj="src/$pkg/$pkg.csproj"
echo "Building $csproj with CS1591 as an error"
dotnet build "$csproj" -c Release -p:WarningsAsErrors=CS1591
done

- name: Test inspect-packed-nupkgs.sh itself
# Regression coverage for issue #122 (dependency-range-literal drift) -
# runs before the real inspection below so a broken validator fails
Expand Down
17 changes: 17 additions & 0 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,21 @@
package surface, so test code doesn't need to satisfy CS1591. -->
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>

<!-- Promotes CS1591 (already a plain warning for every project, via
Directory.Build.props' unconditional GenerateDocumentationFile) to a build
error for every publishable package specifically - PLAN-0062: this used to be
enforced only by package-validation.yaml's own separate, full second rebuild
of all 11 packages with -p:WarningsAsErrors=CS1591; enforcing it here instead
means an ordinary pr-build.yaml build catches a missing doc comment on the PR
that introduces it, not only at the later, separate package-validation gate.
Scoped to IsPackable != 'false' (matched here, after the project body, the
same reason IsTestProject is only reliably readable at this import point) -
every non-packable project (samples, benchmarks, every test/*.AotSmokeTest/
*SampleTests/*.Tests project) sets IsPackable=false directly in its own
csproj, confirmed against all of them, so none of them are newly subject to
this policy. -->
<PropertyGroup Condition="'$(IsPackable)' != 'false'">
<WarningsAsErrors>$(WarningsAsErrors);CS1591</WarningsAsErrors>
</PropertyGroup>
Comment thread
ncipollina marked this conversation as resolved.
</Project>
14 changes: 8 additions & 6 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,14 @@ need to scope a run to one project or class.
handwritten/explicit test data (this repo deliberately doesn't use
AutoFixture-style generated test data for its own tests — see
[Architecture](architecture/index.md) if you're curious why).
- **XML doc comments are required on every new or changed public member**
across all projects — `Compono` and its integration packages are
published NuGet libraries, and IntelliSense is the primary
discoverability surface for a consumer who's never read the source.
`dotnet build -p:WarningsAsErrors=CS1591` fails a PR that's missing one,
the same gate that runs in CI.
- **XML doc comments are required on every new or changed public member of
a publishable package** (`Compono` and every integration package) —
they're published NuGet libraries, and IntelliSense is the primary
discoverability surface for a consumer who's never read the source. An
ordinary `dotnet build Compono.slnx` already enforces this as a build
error for those packages — no extra flag needed. Non-packable projects
(samples, benchmarks, tests, fixtures) are intentionally outside this
boundary.
- **Update the relevant docs page in the same PR**, not as a follow-up —
if your change affects behavior a Concept, How-to Guide, or Package
Guide already describes, update that page alongside the code.
Expand Down
Loading
Loading