diff --git a/Directory.Packages.props b/Directory.Packages.props index ffefe7a..5a149f3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,7 +12,7 @@ - + diff --git a/README.md b/README.md index 986bfa7..a50ea4b 100644 --- a/README.md +++ b/README.md @@ -366,7 +366,7 @@ run once from its Git root: ```powershell dotnet new tool-manifest --output .config -dotnet tool install Trellis.AgentDocs --version 0.1.0-preview.14 --tool-manifest .config/dotnet-tools.json +dotnet tool install Trellis.AgentDocs --version 0.1.0-preview.17 --tool-manifest .config/dotnet-tools.json dotnet tool run agentdocs init ``` diff --git a/Trellis.ServiceLevelIndicators.Asp.ApiVersioning/src/README.md b/Trellis.ServiceLevelIndicators.Asp.ApiVersioning/src/README.md index 7436148..b35ebe3 100644 --- a/Trellis.ServiceLevelIndicators.Asp.ApiVersioning/src/README.md +++ b/Trellis.ServiceLevelIndicators.Asp.ApiVersioning/src/README.md @@ -55,7 +55,7 @@ run from its Git root: ```powershell dotnet new tool-manifest --output .config -dotnet tool install Trellis.AgentDocs --version 0.1.0-preview.14 --tool-manifest .config/dotnet-tools.json +dotnet tool install Trellis.AgentDocs --version 0.1.0-preview.17 --tool-manifest .config/dotnet-tools.json dotnet tool run agentdocs init ``` diff --git a/Trellis.ServiceLevelIndicators.Asp/src/README.md b/Trellis.ServiceLevelIndicators.Asp/src/README.md index 14231b7..c9651f8 100644 --- a/Trellis.ServiceLevelIndicators.Asp/src/README.md +++ b/Trellis.ServiceLevelIndicators.Asp/src/README.md @@ -212,7 +212,7 @@ run from its Git root: ```powershell dotnet new tool-manifest --output .config -dotnet tool install Trellis.AgentDocs --version 0.1.0-preview.14 --tool-manifest .config/dotnet-tools.json +dotnet tool install Trellis.AgentDocs --version 0.1.0-preview.17 --tool-manifest .config/dotnet-tools.json dotnet tool run agentdocs init ``` diff --git a/Trellis.ServiceLevelIndicators/src/README.md b/Trellis.ServiceLevelIndicators/src/README.md index 33f7585..6af28af 100644 --- a/Trellis.ServiceLevelIndicators/src/README.md +++ b/Trellis.ServiceLevelIndicators/src/README.md @@ -125,7 +125,7 @@ run from its Git root: ```powershell dotnet new tool-manifest --output .config -dotnet tool install Trellis.AgentDocs --version 0.1.0-preview.14 --tool-manifest .config/dotnet-tools.json +dotnet tool install Trellis.AgentDocs --version 0.1.0-preview.17 --tool-manifest .config/dotnet-tools.json dotnet tool run agentdocs init ``` diff --git a/build/test-guidance-packages.ps1 b/build/test-guidance-packages.ps1 index e699f83..65bc96e 100644 --- a/build/test-guidance-packages.ps1 +++ b/build/test-guidance-packages.ps1 @@ -10,86 +10,100 @@ $references = [ordered]@{ 'Trellis.ServiceLevelIndicators.Asp.ApiVersioning' = 'trellis-api-sli-apiversioning.md' } -foreach ($id in $references.Keys) { - $packages = @(Get-ChildItem -LiteralPath $PackagesDirectory -Filter '*.nupkg' | - Where-Object { $_.Name -match "^$([regex]::Escape($id))\.\d" -and $_.Name -notlike '*.symbols.nupkg' }) - if ($packages.Count -ne 1) { - throw "Expected one packed $id in $PackagesDirectory, found $($packages.Count)." - } +# The published validator checks the contract and discoverability (links that leave the package, front matter, +# size budgets); --strict makes a warning fail like an error. It is pinned to the packaging helper's version, which +# is published in lockstep with the tool. +$props = [xml] (Get-Content -LiteralPath (Join-Path (Join-Path $PSScriptRoot '..') 'Directory.Packages.props') -Raw) +$toolVersion = $props.SelectSingleNode('//PackageVersion[@Include="Trellis.AgentDocs.Packaging"]').Version +$toolDirectory = Join-Path ([System.IO.Path]::GetTempPath()) "agentdocs-validate-$([guid]::NewGuid().ToString('N'))" +$install = & dotnet tool install Trellis.AgentDocs --version $toolVersion --tool-path $toolDirectory 2>&1 +if ($LASTEXITCODE -ne 0) { throw "Could not install Trellis.AgentDocs $toolVersion for validation:`n$($install | Out-String)" } - $archive = [System.IO.Compression.ZipFile]::OpenRead($packages[0].FullName) - try { - $manifestEntry = $archive.GetEntry('guidance/reference-manifest.json') - $referencePath = $references[$id] - $referenceEntry = $archive.GetEntry($referencePath) - if (-not $manifestEntry -or -not $referenceEntry) { - throw "$id must pack its guidance manifest and $referencePath." - } - if (@($archive.Entries | Where-Object { - $_.FullName.StartsWith('trellis/', [StringComparison]::OrdinalIgnoreCase) - }).Count -ne 0) { - throw "$id must not pack a trellis/ directory." +try { + foreach ($id in $references.Keys) { + $packages = @(Get-ChildItem -LiteralPath $PackagesDirectory -Filter '*.nupkg' | + Where-Object { $_.Name -match "^$([regex]::Escape($id))\.\d" -and $_.Name -notlike '*.symbols.nupkg' }) + if ($packages.Count -ne 1) { + throw "Expected one packed $id in $PackagesDirectory, found $($packages.Count)." } - $reader = [System.IO.StreamReader]::new($manifestEntry.Open()) - try { $manifest = $reader.ReadToEnd() | ConvertFrom-Json } - finally { $reader.Dispose() } - $referenceBytes = [System.IO.MemoryStream]::new() + $archive = [System.IO.Compression.ZipFile]::OpenRead($packages[0].FullName) try { - $referenceEntry.Open().CopyTo($referenceBytes) - $hash = [Convert]::ToHexString( - [System.Security.Cryptography.SHA256]::HashData($referenceBytes.ToArray())).ToLowerInvariant() - $referenceText = [System.Text.Encoding]::UTF8.GetString($referenceBytes.ToArray()) - } - finally { $referenceBytes.Dispose() } + $manifestEntry = $archive.GetEntry('guidance/reference-manifest.json') + $referencePath = $references[$id] + $referenceEntry = $archive.GetEntry($referencePath) + if (-not $manifestEntry -or -not $referenceEntry) { + throw "$id must pack its guidance manifest and $referencePath." + } + if (@($archive.Entries | Where-Object { + $_.FullName.StartsWith('trellis/', [StringComparison]::OrdinalIgnoreCase) + }).Count -ne 0) { + throw "$id must not pack a trellis/ directory." + } - $description = [string] $manifest.documents[0].description - if ($manifest.schemaVersion -ne 1 -or @($manifest.documents).Count -ne 1 -or - $manifest.documents[0].path -ne $referencePath -or - $manifest.documents[0].sha256 -ne $hash -or - $manifest.documents[0].usage -ne 'onDemand' -or - $description.Length -eq 0 -or $description.Length -gt 200 -or $description -notmatch '^Open when ' -or - $null -ne $manifest.PSObject.Properties['entryPoints']) { - throw "$id manifest does not match the packed reference bytes, on-demand usage, and description." - } - # Each package installs into its own directory, so a relative link to a sibling package's reference breaks. - # Any non-absolute link to a trellis-*.md file counts, whatever its prefix (./, ../, subfolders) or anchor, - # in inline form [text](target) or reference form [id]: target. - if ($referenceText -match '\]\(\s*(?!https?:|mailto:|#)[^)\s]*trellis-[a-z0-9-]+\.md' -or - $referenceText -match '(?m)^\s*\[[^\]]+\]:\s*(?!https?:|mailto:|#)\S*trellis-[a-z0-9-]+\.md') { - throw "$id links to another package's reference file; refer to other packages by ID instead." - } + $reader = [System.IO.StreamReader]::new($manifestEntry.Open()) + try { $manifest = $reader.ReadToEnd() | ConvertFrom-Json } + finally { $reader.Dispose() } + $referenceBytes = [System.IO.MemoryStream]::new() + try { + $referenceEntry.Open().CopyTo($referenceBytes) + $hash = [Convert]::ToHexString( + [System.Security.Cryptography.SHA256]::HashData($referenceBytes.ToArray())).ToLowerInvariant() + $referenceText = [System.Text.Encoding]::UTF8.GetString($referenceBytes.ToArray()) + } + finally { $referenceBytes.Dispose() } - if (@($archive.Entries | Where-Object { - $_.FullName -match '^(build|buildTransitive)/' - }).Count -ne 0) { - throw "$id still ships a consumer build target." - } - $nuspec = $archive.GetEntry("$id.nuspec") - if (-not $nuspec) { throw "$id is missing its package metadata." } - $metadataReader = [System.IO.StreamReader]::new($nuspec.Open()) - try { [xml] $metadata = $metadataReader.ReadToEnd() } - finally { $metadataReader.Dispose() } - if ($metadata.SelectSingleNode('//*[local-name()="dependency" and (@id="Trellis.AgentDocs.Packaging" or @id="Trellis.Core")]')) { - throw "$id leaks a publisher-only dependency or introduces a Core dependency." - } - if ($metadata.SelectSingleNode('//*[local-name()="readme"]')?.InnerText -ne 'README.md') { - throw "$id must declare its packed README.md as the NuGet readme." - } - $readmeEntry = $archive.GetEntry('README.md') - if (-not $readmeEntry) { throw "$id is missing its NuGet readme." } - $readmeReader = [System.IO.StreamReader]::new($readmeEntry.Open()) - try { $readme = $readmeReader.ReadToEnd() } - finally { $readmeReader.Dispose() } - if ($readme -notmatch '(?m)^dotnet tool install Trellis\.AgentDocs --version \S+ --tool-manifest \.config/dotnet-tools\.json\r?$' -or - -not $readme.Contains('dotnet tool run agentdocs init ')) { - throw "$id NuGet readme must explain how to install and initialize AgentDocs." - } - # Restoring a package never activates its guide: the readme must describe approval and sync. - if (-not $readme.Contains('approvedPackages') -or -not $readme.Contains('dotnet tool run agentdocs sync')) { - throw "$id NuGet readme must explain approving the package and running sync." + $description = [string] $manifest.documents[0].description + if ($manifest.schemaVersion -ne 1 -or @($manifest.documents).Count -ne 1 -or + $manifest.documents[0].path -ne $referencePath -or + $manifest.documents[0].sha256 -ne $hash -or + $manifest.documents[0].usage -ne 'onDemand' -or + $description.Length -eq 0 -or $description.Length -gt 200 -or $description -notmatch '^Open when ' -or + $null -ne $manifest.PSObject.Properties['entryPoints']) { + throw "$id manifest does not match the packed reference bytes, on-demand usage, and description." + } + # Each package installs into its own directory, so a relative link to a sibling package's reference breaks. + # Any non-absolute link to a trellis-*.md file counts, whatever its prefix (./, ../, subfolders) or anchor, + # in inline form [text](target) or reference form [id]: target. + if ($referenceText -match '\]\(\s*(?!https?:|mailto:|#)[^)\s]*trellis-[a-z0-9-]+\.md' -or + $referenceText -match '(?m)^\s*\[[^\]]+\]:\s*(?!https?:|mailto:|#)\S*trellis-[a-z0-9-]+\.md') { + throw "$id links to another package's reference file; refer to other packages by ID instead." + } + + if (@($archive.Entries | Where-Object { + $_.FullName -match '^(build|buildTransitive)/' + }).Count -ne 0) { + throw "$id still ships a consumer build target." + } + $nuspec = $archive.GetEntry("$id.nuspec") + if (-not $nuspec) { throw "$id is missing its package metadata." } + $metadataReader = [System.IO.StreamReader]::new($nuspec.Open()) + try { [xml] $metadata = $metadataReader.ReadToEnd() } + finally { $metadataReader.Dispose() } + if ($metadata.SelectSingleNode('//*[local-name()="dependency" and (@id="Trellis.AgentDocs.Packaging" or @id="Trellis.Core")]')) { + throw "$id leaks a publisher-only dependency or introduces a Core dependency." + } + if ($metadata.SelectSingleNode('//*[local-name()="readme"]')?.InnerText -ne 'README.md') { + throw "$id must declare its packed README.md as the NuGet readme." + } + $readmeEntry = $archive.GetEntry('README.md') + if (-not $readmeEntry) { throw "$id is missing its NuGet readme." } + $readmeReader = [System.IO.StreamReader]::new($readmeEntry.Open()) + try { $readme = $readmeReader.ReadToEnd() } + finally { $readmeReader.Dispose() } + if ($readme -notmatch '(?m)^dotnet tool install Trellis\.AgentDocs --version \S+ --tool-manifest \.config/dotnet-tools\.json\r?$' -or + -not $readme.Contains('dotnet tool run agentdocs init ')) { + throw "$id NuGet readme must explain how to install and initialize AgentDocs." + } + # Restoring a package never activates its guide: the readme must describe approval and sync. + if (-not $readme.Contains('approvedPackages') -or -not $readme.Contains('dotnet tool run agentdocs sync')) { + throw "$id NuGet readme must explain approving the package and running sync." + } + $validation = & (Join-Path $toolDirectory 'agentdocs') validate $packages[0].FullName --strict 2>&1 + if ($LASTEXITCODE -ne 0) { throw "agentdocs validate --strict rejected ${id}:`n$($validation | Out-String)" } + Write-Host "PASS $($packages[0].Name) ships validated guidance without consumer targets" } - Write-Host "PASS $($packages[0].Name) ships validated guidance without consumer targets" + finally { $archive.Dispose() } } - finally { $archive.Dispose() } } +finally { Remove-Item -LiteralPath $toolDirectory -Recurse -Force -ErrorAction SilentlyContinue }