Skip to content
Draft
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
123 changes: 85 additions & 38 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -255,53 +255,100 @@ jobs:
shell: pwsh
run: |
Write-Output "Validating NuGet packages..."

Add-Type -AssemblyName System.IO.Compression.FileSystem

function Get-NupkgId {
param([string]$Path)
$zip = [System.IO.Compression.ZipFile]::OpenRead($Path)
try {
$entry = $zip.Entries | Where-Object { $_.FullName -like '*.nuspec' -and -not $_.FullName.Contains('/') } | Select-Object -First 1
if ($null -eq $entry) { throw "No .nuspec entry found in '$Path'." }
$stream = $entry.Open()
try {
$reader = New-Object System.IO.StreamReader($stream)
[xml]$xml = $reader.ReadToEnd()
$reader.Dispose()
} finally {
$stream.Dispose()
}
} finally {
$zip.Dispose()
}
$id = $xml.package.metadata.id
if ([string]::IsNullOrEmpty($id)) { throw "Package ID not found in .nuspec of '$Path'." }
return $id
}

# Per-package rule overrides: key = NuGet package ID, value = rules to exclude for that package.
# Add a new entry here whenever a new package is introduced in the repository.
$packageRuleOverrides = [ordered]@{
'Reqnroll' = @('AuthorMustBeSet', 'XmlDocumentationMustBePresent')
'Reqnroll.Assist.Dynamic' = @('AssembliesMustBeOptimized', 'ReadmeMustBeSet', 'XmlDocumentationMustBePresent')
'Reqnroll.Autofac' = @('ReadmeMustBeSet', 'XmlDocumentationMustBePresent')
'Reqnroll.CustomPlugin' = @('ReadmeMustBeSet', 'Symbols', 'XmlDocumentationMustBePresent')
'Reqnroll.ExternalData' = @('ReadmeMustBeSet')
'Reqnroll.Generator' = @('XmlDocumentationMustBePresent')
'Reqnroll.Microsoft.Extensions.DependencyInjection' = @('ReadmeMustBeSet', 'XmlDocumentationMustBePresent')
'Reqnroll.MSTest' = @('ReadmeMustBeSet', 'XmlDocumentationMustBePresent')
'Reqnroll.NUnit' = @('ReadmeMustBeSet', 'XmlDocumentationMustBePresent')
'Reqnroll.Templates.DotNet' = @('ReadmeMustBeSet')
'Reqnroll.Tools.MsBuild.Generation' = @('AssembliesMustBeOptimized', 'ReadmeMustBeSet')
'Reqnroll.TUnit' = @()
'Reqnroll.Verify' = @('AssembliesMustBeOptimized', 'ReadmeMustBeSet', 'Symbols', 'XmlDocumentationMustBePresent')
'Reqnroll.Windsor' = @('ReadmeMustBeSet', 'XmlDocumentationMustBePresent')
'Reqnroll.xUnit' = @('ReadmeMustBeSet', 'XmlDocumentationMustBePresent')
'Reqnroll.xunit.v3' = @('ReadmeMustBeSet', 'XmlDocumentationMustBePresent')
}

# Rules excluded for all packages on non-main branches (e.g. debug builds in PRs)
$globalExcludedRules = @()
if ('${{ github.ref }}' -ne 'refs/heads/main') {
$globalExcludedRules += 'AssembliesMustBeOptimized'
Write-Output '::notice::Assembly optimization validation skipped for non-main branch'
}

$packages = Get-ChildItem "GeneratedNuGetPackages/${{ needs.build.outputs.product_configuration }}/*.nupkg" -Exclude "*.snupkg"
$errorCount = 0
$totalPackages = $packages.Count

# Determine excluded rules based on branch
$excludedRules = @()
if ("${{ github.ref }}" -ne "refs/heads/main") {
$excludedRules += "AssembliesMustBeOptimized"
Write-Output "::notice::Assembly optimization validation skipped for non-main branch"
}


foreach ($package in $packages) {
Write-Output "Validating package: $($package.Name)"
try {
# Build validation command with conditional exclusions
$validationArgs = @($package.FullName)
if ($excludedRules.Count -gt 0) {
$validationArgs += "--excluded-rules"
$validationArgs += ($excludedRules -join ",")
}

# Run validation
$validationResult = & meziantou.validate-nuget-package @validationArgs
$exitCode = $LASTEXITCODE

if ($exitCode -eq 0) {
Write-Output "✅ $($package.Name) passed validation"
} else {
Write-Output "::warning title=Package Validation Issues::$($package.Name) has validation issues"
Write-Output $validationResult
# For now, we'll log warnings but not fail the build to allow gradual improvement
# This can be changed to fail the build once all validation issues are resolved
Write-Output "::notice::Package validation issues detected but not failing build (gradual improvement mode)"
}
} catch {
Write-Output "::error title=Package Validation Error::Error validating $($package.Name): $_"
$packageId = Get-NupkgId -Path $package.FullName
Write-Output "--- Validating: $packageId ($($package.Name)) ---"

if (-not $packageRuleOverrides.Contains($packageId)) {
Write-Output "::error title=Unknown Package::Package '$packageId' is not registered in `$packageRuleOverrides."
Write-Output "::error::To register it, add an entry to the hashtable in the 'Validate NuGet packages' step:"
Write-Output "::error:: '$packageId' = @() # no exclusions"
Write-Output "::error:: '$packageId' = @('RuleName') # with exclusions"
$errorCount++
continue
}

$excluded = ($globalExcludedRules + $packageRuleOverrides[$packageId]) | Select-Object -Unique
$validationArgs = @($package.FullName)
if ($excluded.Count -gt 0) {
$validationArgs += '--excluded-rules'
$validationArgs += ($excluded -join ',')
Write-Output " Excluded rules: $($excluded -join ', ')"
}

$validationResult = & meziantou.validate-nuget-package @validationArgs 2>&1
$exitCode = $LASTEXITCODE

if ($exitCode -eq 0) {
Write-Output " ✅ Passed"
} else {
Write-Output "::error title=Validation Failed [$packageId]::$($package.Name) failed validation (exit $exitCode)"
$validationResult | ForEach-Object { Write-Output " $_" }
$errorCount++
}
}

if ($errorCount -gt 0) {
Write-Output "::error title=Package Validation Failed::$errorCount package(s) had validation errors"
Write-Output "::error title=Package Validation Failed::$errorCount package(s) failed validation."
exit 1
} else {
Write-Output "✅ Validation completed for $totalPackages packages"
exit 0
}
Write-Output "✅ All $($packages.Count) packages passed validation."

component-tests:
runs-on: ubuntu-latest
Expand Down
57 changes: 43 additions & 14 deletions PACKAGE_VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,55 @@ The tool validates various aspects of NuGet packages including:

## Current configuration

The validation is configured in `.github/workflows/ci.yml` and currently:
The validation is configured in `.github/workflows/ci.yml` and:

- ✅ Runs after package building in a separate validation job
- ✅ Includes all validation checks including deterministic builds
- ✅ Skips assembly optimization validation on non-main branches (to allow Debug builds in PRs)
- ⚠️ Reports validation issues as warnings but does not fail the build (gradual improvement mode)
- ✅ **Fails the build** when a package has validation issues not covered by its exclusions
- ✅ Uses per-package rule exclusions so each package can suppress only the checks it genuinely cannot pass yet

## Error codes
## Per-package rule overrides

Each known package is registered in an ordered hashtable (`$packageRuleOverrides`) inside the `Validate NuGet packages` step in `.github/workflows/ci.yml`. The key is the **NuGet package ID** (read from the `.nuspec` inside the `.nupkg`) and the value is an array of rule names to skip for that package.

### Adding a new package

When a new package is added to the repository, you **must** also register it in the hashtable, otherwise the build will fail with:

Common error codes you might encounter:
```
::error title=Unknown Package:: Package '<id>' is not registered in $packageRuleOverrides.
```

- **12**: Author element not set explicitly
- **33**: Icon file not found
- **52**: Project URL not accessible
- **61**: Readme not set
- **81**: Assembly not optimized (Debug builds)
- **101**: XML documentation not found
- **112**: Deterministic build issues
- **119**: Source file not accessible
Add an entry like this to the hashtable in the `Validate NuGet packages` step:

## Future improvements
```powershell
# No exclusions needed (preferred — the package passes all checks)
'My.New.Package' = @()

Once package validation issues are resolved, the CI can be updated to fail builds on validation errors instead of just reporting warnings.
# With exclusions for known issues that are not yet resolved
'My.New.Package' = @('ReadmeMustBeSet', 'XmlDocumentationMustBePresent')
```

Run the validation locally (see below) to discover which rules need to be excluded, then add as few exclusions as possible.

## Error codes

Common error codes you might encounter, and their corresponding rule names:

| Code | Rule name | Description |
|------|------------------------------|--------------------------------------------------|
| 12 | `AuthorMustBeSet` | Author element not set explicitly |
| 33 | `IconMustBeSet` | Icon file not found |
| 52 | `ProjectUrlMustBeSet` | Project URL not accessible |
| 61 | `ReadmeMustBeSet` | Readme not set |
| 81 | `AssembliesMustBeOptimized` | Assembly not optimized (Debug builds) |
| 101 | `XmlDocumentationMustBePresent` | XML documentation not found |
| 111 | `Symbols` | Symbol file not found |
| 112 | `Symbols` | Deterministic build issues in symbol file |
| 119 | `Symbols` | Source file not accessible in symbol package |

Run `meziantou.validate-nuget-package --help` for the full list of available rule names.

## Manual validation

Expand All @@ -51,6 +77,9 @@ dotnet tool install --global Meziantou.Framework.NuGetPackageValidation.Tool
# Validate a package
meziantou.validate-nuget-package path/to/package.nupkg

# Validate a package with specific rule exclusions
meziantou.validate-nuget-package path/to/package.nupkg --excluded-rules ReadmeMustBeSet,XmlDocumentationMustBePresent

# Validate a package excluding assembly optimization (for non-main branches)
meziantou.validate-nuget-package path/to/package.nupkg --excluded-rules AssembliesMustBeOptimized

Expand Down
Loading