param( [string]$ZipPath, [string]$HashPath ) $ErrorActionPreference = "Stop" function Resolve-LatestReleaseFile { param( [string]$ReleaseRoot, [string]$Filter ) $item = Get-ChildItem -LiteralPath $ReleaseRoot -Filter $Filter | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if (-not $item) { throw "No file found for filter: $Filter" } return $item.FullName } $root = Split-Path -Parent $MyInvocation.MyCommand.Path $releaseRoot = Join-Path $root "release" if (-not $ZipPath) { $ZipPath = Resolve-LatestReleaseFile -ReleaseRoot $releaseRoot -Filter "*.zip" } if (-not $HashPath) { $baseName = [System.IO.Path]::GetFileNameWithoutExtension($ZipPath) $candidate = Join-Path $releaseRoot "$baseName.sha256.txt" if (Test-Path -LiteralPath $candidate) { $HashPath = $candidate } else { $HashPath = Resolve-LatestReleaseFile -ReleaseRoot $releaseRoot -Filter "*.sha256.txt" } } if (-not (Test-Path -LiteralPath $ZipPath)) { throw "Zip not found: $ZipPath" } if (-not (Test-Path -LiteralPath $HashPath)) { throw "Hash file not found: $HashPath" } $hashLines = Get-Content -LiteralPath $HashPath $expectedHash = ($hashLines | Where-Object { $_ -like "sha256=*" } | Select-Object -First 1) -replace "^sha256=", "" $expectedFile = ($hashLines | Where-Object { $_ -like "file=*" } | Select-Object -First 1) -replace "^file=", "" if (-not $expectedHash) { throw "sha256 entry missing in $HashPath" } $actualHash = (Get-FileHash -LiteralPath $ZipPath -Algorithm SHA256).Hash.ToLowerInvariant() $fileName = [System.IO.Path]::GetFileName($ZipPath) Add-Type -AssemblyName System.IO.Compression.FileSystem $zip = [System.IO.Compression.ZipFile]::OpenRead($ZipPath) $missingEntries = @() $expectsSmokeReport = $false try { $requiredEntries = @( "README_RELEASE.txt", "release_manifest.json", "domain-api/README.md", "domain-web/README.md", "scripts/package_domain_release.ps1", "scripts/verify_domain_release.ps1", "scripts/package_domain_release.sh", "scripts/verify_domain_release.sh", "scripts/smoke_test_stack.ps1" ) $entryNames = $zip.Entries | ForEach-Object { $_.FullName.Replace('\', '/') } $manifestEntry = $zip.Entries | Where-Object { $_.FullName.Replace('\', '/') -eq "release_manifest.json" } | Select-Object -First 1 if ($manifestEntry) { $manifestStream = $manifestEntry.Open() $manifestReader = $null try { $manifestReader = New-Object System.IO.StreamReader($manifestStream) $manifestText = $manifestReader.ReadToEnd() $manifest = $manifestText | ConvertFrom-Json $smokeReport = [string]$manifest.smoke_test.report if (-not [string]::IsNullOrWhiteSpace($smokeReport)) { $expectsSmokeReport = $true } } finally { if ($manifestReader) { $manifestReader.Dispose() } $manifestStream.Dispose() } } foreach ($required in $requiredEntries) { if ($entryNames -notcontains $required) { $missingEntries += $required } } if ($expectsSmokeReport -and ($entryNames -notcontains "smoke_test_report.json")) { $missingEntries += "smoke_test_report.json" } } finally { $zip.Dispose() } $ok = ($actualHash -eq $expectedHash.ToLowerInvariant()) -and ($missingEntries.Count -eq 0) -and (($expectedFile -eq "") -or ($expectedFile -eq $fileName)) $report = [ordered]@{ ok = $ok zip = $ZipPath hash_file = $HashPath expected_file = $expectedFile actual_file = $fileName expected_sha256 = $expectedHash.ToLowerInvariant() actual_sha256 = $actualHash missing_entries = $missingEntries } $report | ConvertTo-Json -Depth 6 if (-not $ok) { exit 1 }