From e103997a217e971987ffc8c8f8f34aaa77c503d6 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:26:28 +0200 Subject: [PATCH 1/7] Add final release compliance gate (#91) Signed-off-by: KeyffMS <124252104+KeyffMS@users.noreply.github.com> --- tools/verify-release-compliance.ps1 | 228 ++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 tools/verify-release-compliance.ps1 diff --git a/tools/verify-release-compliance.ps1 b/tools/verify-release-compliance.ps1 new file mode 100644 index 00000000..1a43f93d --- /dev/null +++ b/tools/verify-release-compliance.ps1 @@ -0,0 +1,228 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$DirectoryPath, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$ArchivePath, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$ReportPath, + + [string]$ManifestPath = + (Join-Path $PSScriptRoot '..\release\required-files.txt') +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +if ($null -eq ('System.IO.Compression.ZipFile' -as [type])) { + Add-Type -AssemblyName System.IO.Compression.FileSystem +} + +$root = Split-Path -Parent $PSScriptRoot +$directory = (Resolve-Path -LiteralPath $DirectoryPath).Path +$archivePathResolved = (Resolve-Path -LiteralPath $ArchivePath).Path +$manifest = (Resolve-Path -LiteralPath $ManifestPath).Path +$reportFullPath = [System.IO.Path]::GetFullPath($ReportPath) +[System.IO.Directory]::CreateDirectory( + [System.IO.Path]::GetDirectoryName($reportFullPath)) | Out-Null + +[xml]$props = Get-Content -LiteralPath (Join-Path $root 'Directory.Build.props') +$group = $props.Project.PropertyGroup +$productVersion = [string]$group.SightAdaptProductVersion +$sdkVersion = [string]$group.SightAdaptDotNetSdkVersion +$runtimeVersion = [string]$group.SightAdaptDotNetRuntimeVersion +$rid = [string]$group.SightAdaptRuntimeIdentifier +$artifactName = ([string]$group.SightAdaptArtifactName).Replace( + '$(SightAdaptProductVersion)', + $productVersion) + +$requiredFiles = @( + Get-Content -LiteralPath $manifest | + ForEach-Object { $_.Trim() } | + Where-Object { + -not [string]::IsNullOrWhiteSpace($_) -and + -not $_.StartsWith('#', [StringComparison]::Ordinal) + } +) + +$failures = [System.Collections.Generic.List[string]]::new() +$directoryFiles = @( + Get-ChildItem -LiteralPath $directory -File -Recurse | + ForEach-Object { + [System.IO.Path]::GetRelativePath( + $directory, + $_.FullName).Replace('\', '/') + } | + Sort-Object +) + +foreach ($requiredFile in $requiredFiles) { + $path = Join-Path $directory $requiredFile + if (-not [System.IO.File]::Exists($path)) { + $failures.Add("Publish directory is missing required file '$requiredFile'.") + continue + } + + $item = Get-Item -LiteralPath $path + if ($item.Length -le 0) { + $failures.Add("Required file '$requiredFile' is empty in the publish directory.") + continue + } + + if ($requiredFile -match '\.(txt|md|json)$') { + try { + $text = Get-Content -LiteralPath $path -Raw -Encoding utf8 + if ([string]::IsNullOrWhiteSpace($text)) { + $failures.Add("Required text file '$requiredFile' has no readable content.") + } + if ($text -match '(?im)\b(TODO|TBD|REPLACE_ME)\b|<\s*(version|date|name|url|email)\s*>|\{\{[^}\r\n]+\}\}') { + $failures.Add("Required text file '$requiredFile' contains an unresolved template marker.") + } + } + catch { + $failures.Add("Required text file '$requiredFile' cannot be read as UTF-8: $($_.Exception.Message)") + } + } +} + +$expectedArchiveName = "$artifactName.zip" +if ([System.IO.Path]::GetFileName($archivePathResolved) -ne $expectedArchiveName) { + $failures.Add( + "Archive name '$([System.IO.Path]::GetFileName($archivePathResolved))' does not match '$expectedArchiveName'.") +} + +try { + & (Join-Path $PSScriptRoot 'test-release-package.ps1') ` + -ArchivePath $archivePathResolved ` + -ManifestPath $manifest +} +catch { + $failures.Add("Final archive validation failed: $($_.Exception.Message)") +} + +$archive = [System.IO.Compression.ZipFile]::OpenRead($archivePathResolved) +try { + $archiveFiles = @( + $archive.Entries | + Where-Object { + -not [string]::IsNullOrWhiteSpace($_.Name) + } | + ForEach-Object { + $_.FullName.Replace('\', '/').TrimStart('/') + } | + Sort-Object + ) +} +finally { + $archive.Dispose() +} + +$missingFromArchive = @($directoryFiles | Where-Object { $archiveFiles -notcontains $_ }) +$unexpectedInArchive = @($archiveFiles | Where-Object { $directoryFiles -notcontains $_ }) +foreach ($path in $missingFromArchive) { + $failures.Add("Published file '$path' is missing from the final archive.") +} +foreach ($path in $unexpectedInArchive) { + $failures.Add("Final archive contains unexpected file '$path' not present in the staged directory.") +} + +$noticeMetadataPath = Join-Path $directory 'DOTNET-NOTICE-METADATA.json' +$licenseReportPath = Join-Path $directory 'LICENSE-REPORT.json' +$sbomPath = Join-Path $directory 'SBOM.spdx.json' + +try { + $noticeMetadata = Get-Content -LiteralPath $noticeMetadataPath -Raw | ConvertFrom-Json + if ([string]$noticeMetadata.productVersion -ne $productVersion -or + [string]$noticeMetadata.sdkVersion -ne $sdkVersion -or + [string]$noticeMetadata.runtimeVersion -ne $runtimeVersion -or + [string]$noticeMetadata.runtimeIdentifier -ne $rid) { + $failures.Add('Exact-version notice metadata does not match release metadata.') + } +} +catch { + $failures.Add("DOTNET-NOTICE-METADATA.json cannot be validated: $($_.Exception.Message)") +} + +try { + $licenseReport = Get-Content -LiteralPath $licenseReportPath -Raw | ConvertFrom-Json + if ([string]$licenseReport.result -ne 'pass') { + $failures.Add("LICENSE-REPORT.json result is '$($licenseReport.result)', not 'pass'.") + } + if ([string]$licenseReport.productVersion -ne $productVersion -or + [string]$licenseReport.sdkVersion -ne $sdkVersion -or + [string]$licenseReport.runtimeVersion -ne $runtimeVersion -or + [string]$licenseReport.runtimeIdentifier -ne $rid) { + $failures.Add('License report metadata does not match release metadata.') + } +} +catch { + $failures.Add("LICENSE-REPORT.json cannot be validated: $($_.Exception.Message)") +} + +try { + $sbom = Get-Content -LiteralPath $sbomPath -Raw | ConvertFrom-Json + if ([string]$sbom.spdxVersion -ne 'SPDX-2.3') { + $failures.Add("SBOM uses '$($sbom.spdxVersion)' instead of SPDX-2.3.") + } + if ([string]$sbom.name -ne "SightAdapt-$productVersion-$rid") { + $failures.Add("SBOM name '$($sbom.name)' does not match the release.") + } + + $sbomFiles = @($sbom.files | ForEach-Object { + ([string]$_.fileName).TrimStart('.', '/') + }) + foreach ($path in $directoryFiles) { + if ($path -eq 'SBOM.spdx.json') { + continue + } + if ($sbomFiles -notcontains $path) { + $failures.Add("SBOM does not contain shipped file '$path'.") + } + } + + $sightAdaptPackage = @($sbom.packages | Where-Object { + [string]$_.name -eq 'SightAdapt' -and + [string]$_.versionInfo -eq $productVersion + }) | Select-Object -First 1 + if ($null -eq $sightAdaptPackage) { + $failures.Add('SBOM does not identify the SightAdapt single-file packaging container.') + } +} +catch { + $failures.Add("SBOM.spdx.json cannot be validated: $($_.Exception.Message)") +} + +$report = [ordered]@{ + schemaVersion = 1 + generatedAtUtc = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ') + result = if ($failures.Count -eq 0) { 'pass' } else { 'fail' } + productVersion = $productVersion + sdkVersion = $sdkVersion + runtimeVersion = $runtimeVersion + runtimeIdentifier = $rid + artifactName = $artifactName + archiveFile = [System.IO.Path]::GetFileName($archivePathResolved) + archiveSha256 = (Get-FileHash -LiteralPath $archivePathResolved -Algorithm SHA256).Hash + manifest = 'release/required-files.txt' + requiredFiles = $requiredFiles + stagedFiles = $directoryFiles + archiveFiles = $archiveFiles + failures = @($failures) +} + +[System.IO.File]::WriteAllText( + $reportFullPath, + ($report | ConvertTo-Json -Depth 8) + [Environment]::NewLine, + [System.Text.UTF8Encoding]::new($false)) + +if ($failures.Count -gt 0) { + $details = $failures | ForEach-Object { " - $_" } + throw "Release compliance gate failed:`n$($details -join "`n")" +} + +Write-Host "Release compliance gate passed. Report: $reportFullPath" From 9ec09ba342ab4368464629957b6a3d9dd48b0cfc Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:26:45 +0200 Subject: [PATCH 2/7] Test rejection of incomplete release packages (#91) Signed-off-by: KeyffMS <124252104+KeyffMS@users.noreply.github.com> --- tools/test-release-compliance-negative.ps1 | 43 ++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tools/test-release-compliance-negative.ps1 diff --git a/tools/test-release-compliance-negative.ps1 b/tools/test-release-compliance-negative.ps1 new file mode 100644 index 00000000..c4a1fa95 --- /dev/null +++ b/tools/test-release-compliance-negative.ps1 @@ -0,0 +1,43 @@ +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +if ($null -eq ('System.IO.Compression.ZipFile' -as [type])) { + Add-Type -AssemblyName System.IO.Compression.FileSystem +} + +$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ( + 'sightadapt-incomplete-package-' + [Guid]::NewGuid().ToString('N')) +$staging = Join-Path $tempRoot 'staging' +$archive = Join-Path $tempRoot 'incomplete.zip' + +try { + [System.IO.Directory]::CreateDirectory($staging) | Out-Null + [System.IO.File]::WriteAllText( + (Join-Path $staging 'SightAdapt.exe'), + 'deliberately incomplete package', + [System.Text.UTF8Encoding]::new($false)) + + [System.IO.Compression.ZipFile]::CreateFromDirectory($staging, $archive) + + $failedAsExpected = $false + try { + & (Join-Path $PSScriptRoot 'test-release-package.ps1') ` + -ArchivePath $archive + } + catch { + $failedAsExpected = $true + Write-Host "Incomplete package was rejected as expected: $($_.Exception.Message)" + } + + if (-not $failedAsExpected) { + throw 'The deliberately incomplete package unexpectedly passed validation.' + } +} +finally { + Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue +} + +Write-Host 'Negative release-package validation passed.' From fae83bb4a6d99b9805ef6d6b3e3af61dc17b0414 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:27:20 +0200 Subject: [PATCH 3/7] Enforce release compliance before artifact upload (#91) Signed-off-by: KeyffMS <124252104+KeyffMS@users.noreply.github.com> --- .github/workflows/build.yml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a2327a44..a1d9361e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -94,21 +94,32 @@ jobs: .\tools\generate-sbom.ps1 -PublishDirectory artifacts/win-x64 + - name: Verify incomplete packages are rejected + shell: pwsh + run: .\tools\test-release-compliance-negative.ps1 + - name: Create and verify Windows alpha archive shell: pwsh run: | $archivePath = "artifacts/${{ steps.release.outputs.artifact_name }}.zip" + $reportPath = "artifacts/${{ steps.release.outputs.artifact_name }}-compliance.json" Remove-Item $archivePath -Force -ErrorAction SilentlyContinue + Remove-Item $reportPath -Force -ErrorAction SilentlyContinue Compress-Archive ` -Path "artifacts/win-x64/*" ` -DestinationPath $archivePath ` -CompressionLevel Optimal - .\tools\test-release-package.ps1 -ArchivePath $archivePath + .\tools\verify-release-compliance.ps1 ` + -DirectoryPath artifacts/win-x64 ` + -ArchivePath $archivePath ` + -ReportPath $reportPath - - name: Upload Windows alpha + - name: Upload verified Windows alpha and compliance report uses: actions/upload-artifact@v4 with: name: ${{ steps.release.outputs.artifact_name }}-${{ github.run_id }} - path: artifacts/${{ steps.release.outputs.artifact_name }}.zip + path: | + artifacts/${{ steps.release.outputs.artifact_name }}.zip + artifacts/${{ steps.release.outputs.artifact_name }}-compliance.json if-no-files-found: error retention-days: 7 From 621b087059910843ece437352932551a29df5a36 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:27:48 +0200 Subject: [PATCH 4/7] Document canonical release compliance gate (#91) Signed-off-by: KeyffMS <124252104+KeyffMS@users.noreply.github.com> --- docs/legal/RELEASE-COMPLIANCE-GATE.md | 70 +++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/legal/RELEASE-COMPLIANCE-GATE.md diff --git a/docs/legal/RELEASE-COMPLIANCE-GATE.md b/docs/legal/RELEASE-COMPLIANCE-GATE.md new file mode 100644 index 00000000..09eca3c8 --- /dev/null +++ b/docs/legal/RELEASE-COMPLIANCE-GATE.md @@ -0,0 +1,70 @@ +# Release compliance gate + +## Purpose + +The release compliance gate treats the legal, privacy, dependency, notice and SBOM bundle as a release invariant. It runs after the final publication directory has been produced and before any artifact is uploaded. + +This is a packaging control. It does not test or change SightAdapt application behavior. + +## Authoritative inputs + +The gate consumes existing authoritative outputs rather than creating independent legal data: + +- `release/required-files.txt` — canonical package manifest; +- `Directory.Build.props` — product, artifact, SDK, runtime and RID metadata; +- `DOTNET-NOTICE-METADATA.json` — exact-version Microsoft source/checksum/runtime evidence; +- `LICENSE-REPORT.json` — dependency-license review result; +- `SBOM.spdx.json` — SPDX component and shipped-file inventory; +- the final staged publication directory; +- the final ZIP archive. + +## Checks + +`tools/verify-release-compliance.ps1` verifies: + +- every manifest file exists in the staged publication directory; +- required files are non-empty and readable; +- required text does not contain common unresolved template markers; +- the final archive name matches the canonical artifact metadata; +- the existing archive validator accepts the final ZIP; +- every staged file is in the final ZIP and no unexpected file was added; +- exact-version notice metadata matches the product, SDK, runtime and RID; +- the license report has a `pass` result and matches the same build metadata; +- the SBOM is SPDX 2.3, describes the correct SightAdapt release and contains every separately shipped file except its own final bytes; +- the SBOM identifies SightAdapt as the single-file packaging container for embedded runtime components. + +The gate writes a machine-readable compliance report containing the result, build identity, canonical artifact name, archive SHA-256, manifest, staged/archive file lists and failures. + +## Deliberately incomplete package + +`tools/test-release-compliance-negative.ps1` creates a temporary ZIP containing only a fake `SightAdapt.exe` and confirms that the package validator rejects it. If the incomplete ZIP is accepted, the workflow fails. + +## Workflow order + +The maintained release sequence is: + +1. restore, build and run the existing application test suite; +2. publish the final self-contained directory; +3. generate exact-version .NET notices; +4. generate the SBOM, dependency summary and license report; +5. confirm an incomplete package is rejected; +6. create the final ZIP; +7. run the release compliance gate against the staged directory and ZIP; +8. upload the verified ZIP and compliance report together. + +No upload step runs after a failed compliance gate. + +## Other distribution workflows + +GitHub Releases, installer builds, store packages, portable packages and release mirrors must consume the same staged directory and canonical manifest. Each maintained packaging workflow must: + +- run the equivalent directory/package verification after its final files are staged; +- preserve all required documents in the installed or extractable application directory; +- retain a compliance report tied to the exact package bytes; +- stop before publication when verification fails. + +A platform-specific installer or store container may use a different outer format, but it must verify the unpacked/installed contents against `release/required-files.txt` and record the final container checksum. A mirror must publish the same verified bytes and report rather than rebuilding or removing files. + +## Maintenance + +Update the canonical manifest first when a required compliance artifact changes. Update the SBOM generator, packaging documentation and all distribution workflows in the same pull request. Do not add a separate hard-coded list to another workflow. From 820eae6f35a4d004983559fea957eaecda702eea Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:29:13 +0200 Subject: [PATCH 5/7] Document final package compliance gate (#91) Signed-off-by: KeyffMS <124252104+KeyffMS@users.noreply.github.com> --- docs/PACKAGING.md | 45 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/docs/PACKAGING.md b/docs/PACKAGING.md index 3923c6c0..36a75e2d 100644 --- a/docs/PACKAGING.md +++ b/docs/PACKAGING.md @@ -64,23 +64,38 @@ artifacts/win-x64/ Additional reviewed runtime files may be present depending on the publish settings. -## Archive creation and validation +## Final package compliance gate -Create the archive from the contents of the publish directory so the required files remain at the ZIP root: +Create the archive from the contents of the publish directory so the required files remain at the ZIP root, then validate both the staged directory and final archive: ```powershell $archive = '.\artifacts\SightAdapt-0.5.0.50-alpha-win-x64.zip' +$report = '.\artifacts\SightAdapt-0.5.0.50-alpha-win-x64-compliance.json' Remove-Item $archive -Force -ErrorAction SilentlyContinue +Remove-Item $report -Force -ErrorAction SilentlyContinue Compress-Archive ` -Path '.\artifacts\win-x64\*' ` -DestinationPath $archive ` -CompressionLevel Optimal -.\tools\test-release-package.ps1 -ArchivePath $archive +.\tools\verify-release-compliance.ps1 ` + -DirectoryPath '.\artifacts\win-x64' ` + -ArchivePath $archive ` + -ReportPath $report ``` -The validation script opens the final ZIP and checks the entries listed in `release/required-files.txt`. Required documents must be readable UTF-8 text. The exact .NET metadata must match the pinned release inputs and map the required runtime packs. SBOM generation has already failed the workflow if a dependency is absent from the reviewed policy, uses a different version or has a denied/unreviewed license. +The gate consumes the canonical manifest and existing notice, license-report and SBOM outputs. It verifies the staged directory, final ZIP, build identity, archive name, exact-version metadata, license-policy result and shipped-file coverage. It writes a machine-readable report containing the final archive SHA-256 and validation result. + +The negative check must also pass: + +```powershell +.\tools\test-release-compliance-negative.ps1 +``` + +This creates a deliberately incomplete temporary package and proves that the validator rejects it. + +See [Release compliance gate](legal/RELEASE-COMPLIANCE-GATE.md). ## Microsoft .NET notices and redistribution position @@ -96,20 +111,28 @@ The reviewed redistribution position is documented in [Microsoft .NET redistribu The generated SPDX document contains component relationships and SHA-256 checksums for every separately shipped file. `SightAdapt.exe` is the documented single-file container for embedded managed and native runtime components. See [SBOM and dependency-license review](legal/SBOM-AND-LICENSE-REVIEW.md). +## Installers, stores and mirrors + +Every maintained installer or store workflow must run the same logical gate after the final installed/unpacked file set is staged. The installed application directory must satisfy `release/required-files.txt`, and the outer installer/store container checksum must be retained in its compliance report. + +Portable packages use the ZIP gate directly. Release mirrors publish the same verified bytes and compliance report; they must not rebuild, strip or rename required legal/compliance files. + ## Release checklist Before publishing or mirroring a binary package: 1. verify the pinned release metadata; 2. verify the Microsoft .NET redistribution analysis and package notice; -3. restore, build and test the application; +3. restore, build and run the maintained project checks; 4. publish into a clean staging directory; 5. generate exact-version .NET notices from the hash-verified official SDK package and actual restore graph; 6. generate the SPDX SBOM, license report and human-readable dependency inventory; 7. resolve every component or license-policy failure; -8. create the final archive or platform package; -9. validate the final archive with `tools/test-release-package.ps1`; -10. inspect the package manually to confirm that every legal document opens without running SightAdapt; -11. publish the same verified bytes to every official mirror. - -Do not publish an official binary release when the legal bundle, redistribution notice, exact-version generation, SBOM, license report, package checksum, runtime mapping or final-archive validation is incomplete. Production or paid distribution additionally remains blocked until the qualified legal review required by Issue #93 is recorded. +8. confirm the deliberately incomplete package is rejected; +9. create the final archive or platform package; +10. run `verify-release-compliance.ps1` against the staged directory and final package; +11. retain and publish the compliance report with the package; +12. inspect the package manually to confirm that every legal document opens without running SightAdapt; +13. publish the same verified bytes to every official mirror. + +Do not publish an official binary release when the legal bundle, redistribution notice, exact-version generation, SBOM, license report, package checksum, runtime mapping or final compliance gate is incomplete. Production or paid distribution additionally remains blocked until the qualified legal review required by Issue #93 is recorded. From b7bc889ecb87eae66e59c66979e34112edf50873 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:29:42 +0200 Subject: [PATCH 6/7] Link release compliance gate (#91) Signed-off-by: KeyffMS <124252104+KeyffMS@users.noreply.github.com> --- docs/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index 39a98b60..0cbe4e36 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,7 +6,8 @@ The documentation describes the current SightAdapt implementation and the canoni - [Intended purpose and medical-device claims policy](legal/INTENDED-PURPOSE-AND-MDR.md) — approved general-purpose positioning, prohibited medical claims, terminology, reviewed surfaces and renewed-assessment triggers. - [Patent FTO commercialization gate](legal/PATENT-FTO-GATE.md) — technical feature map, current non-commercial decision, professional-review requirements and re-opening triggers. - [Release naming and attribution](RELEASING.md) — public release-title, tag, artifact, website, publisher, store-claim and mark conventions. -- [Binary packaging standard](PACKAGING.md) — required legal files, redistribution notice, SBOM, exact-version notices, archive validation and distribution-format rules. +- [Binary packaging standard](PACKAGING.md) — required legal files, redistribution notice, SBOM, exact-version notices, final compliance gate and distribution-format rules. +- [Release compliance gate](legal/RELEASE-COMPLIANCE-GATE.md) — staged-directory/final-package verification, negative test and retained compliance report. - [Microsoft .NET redistribution analysis](legal/DOTNET-REDISTRIBUTION.md) — Microsoft-origin component inventory, applicable terms, package implementation, review triggers and professional-review gate. - [Exact-version .NET notice generation](legal/DOTNET-NOTICE-GENERATION.md) — pinned SDK/runtime inputs, official package verification, notice import, checksums and update triggers. - [SBOM and dependency-license review](legal/SBOM-AND-LICENSE-REVIEW.md) — SPDX inventory, license policy, generated reports and release failure rules. From ae7aef394970bc5b9d20c8cc053f48255be5c13b Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:30:13 +0200 Subject: [PATCH 7/7] Require retained final compliance report for releases (#91) Signed-off-by: KeyffMS <124252104+KeyffMS@users.noreply.github.com> --- docs/RELEASING.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/RELEASING.md b/docs/RELEASING.md index f988b89d..0e00c0ba 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -80,12 +80,17 @@ Do not use `®`. Every binary distribution must comply with [the binary packaging standard](PACKAGING.md). The final archive or platform package must include every file listed in `release/required-files.txt`, including the license, exact-version notices, Microsoft redistribution notice, privacy notice, dependency inventory, SBOM and license report. -The legal documents must be readable without starting the application. The final ZIP must pass: +Before publication, validate the final staged directory and package and retain the resulting report: ```powershell -.\tools\test-release-package.ps1 -ArchivePath +.\tools\verify-release-compliance.ps1 ` + -DirectoryPath ` + -ArchivePath ` + -ReportPath ``` +The package and compliance report must be published together. GitHub Releases, installers, store packages, portable packages and mirrors must use the same canonical manifest and equivalent final-package gate. Do not upload or publish when the report result is not `pass`. + Do not publish an official binary release until the exact .NET SDK and runtime-pack versions have been recorded and the corresponding exact-version notice material has been generated and reviewed. ## Minimal release header