初版功能完成
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PackagePath,
|
||||
|
||||
[ValidateSet("Engineer", "Site", "Server")]
|
||||
[string]$Role
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$resolvedPackage = [System.IO.Path]::GetFullPath($PackagePath)
|
||||
$temporaryRoot = $null
|
||||
$packageRoot = $resolvedPackage
|
||||
try {
|
||||
if (Test-Path -LiteralPath $resolvedPackage -PathType Leaf) {
|
||||
if ([System.IO.Path]::GetExtension($resolvedPackage) -ne ".zip") {
|
||||
throw "Release package file must be a ZIP: $resolvedPackage"
|
||||
}
|
||||
$temporaryRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("remlink-release-verify-" + [guid]::NewGuid().ToString("N"))
|
||||
New-Item -ItemType Directory -Path $temporaryRoot | Out-Null
|
||||
Expand-Archive -LiteralPath $resolvedPackage -DestinationPath $temporaryRoot
|
||||
$roots = @(Get-ChildItem -LiteralPath $temporaryRoot -Directory)
|
||||
$looseFiles = @(Get-ChildItem -LiteralPath $temporaryRoot -File)
|
||||
if ($roots.Count -ne 1 -or $looseFiles.Count -ne 0) {
|
||||
throw "Release ZIP must contain exactly one top-level package directory"
|
||||
}
|
||||
$packageRoot = $roots[0].FullName
|
||||
} elseif (-not (Test-Path -LiteralPath $resolvedPackage -PathType Container)) {
|
||||
throw "Release package not found: $resolvedPackage"
|
||||
}
|
||||
|
||||
$roleMarkers = [ordered]@{
|
||||
Engineer = "RemLinkEngineer.exe"
|
||||
Site = "RemLinkSite.exe"
|
||||
Server = "linux-amd64/remlink-server"
|
||||
}
|
||||
$detectedRoles = @($roleMarkers.GetEnumerator() | Where-Object {
|
||||
Test-Path -LiteralPath (Join-Path $packageRoot $_.Value) -PathType Leaf
|
||||
} | ForEach-Object Key)
|
||||
if (-not $Role) {
|
||||
if ($detectedRoles.Count -ne 1) {
|
||||
throw "Unable to infer one package role; found markers for: $($detectedRoles -join ', ')"
|
||||
}
|
||||
$Role = $detectedRoles[0]
|
||||
}
|
||||
if ($detectedRoles.Count -ne 1 -or $detectedRoles[0] -ne $Role) {
|
||||
throw "$Role package must contain only its own executable; found markers for: $($detectedRoles -join ', ')"
|
||||
}
|
||||
$forbiddenExecutableNames = switch ($Role) {
|
||||
Engineer { @("RemLinkSite.exe", "remlink-server") }
|
||||
Site { @("RemLinkEngineer.exe", "remlink-server") }
|
||||
Server { @("RemLinkEngineer.exe", "RemLinkSite.exe") }
|
||||
}
|
||||
$foreignExecutables = @(Get-ChildItem -LiteralPath $packageRoot -Recurse -File | Where-Object { $_.Name -in $forbiddenExecutableNames })
|
||||
if ($foreignExecutables.Count -ne 0) {
|
||||
throw "$Role package contains a foreign executable: $($foreignExecutables[0].FullName)"
|
||||
}
|
||||
|
||||
$required = @(
|
||||
"README.md", "BUILD-INFO.json", "SHA256SUMS.txt", "docs/deployment-and-usage.md",
|
||||
"scripts/validation/Test-ReleasePackage.ps1"
|
||||
)
|
||||
switch ($Role) {
|
||||
Engineer {
|
||||
$required += @("RemLinkEngineer.exe", "engineer.yaml", "THIRD_PARTY_NOTICES.md")
|
||||
}
|
||||
Site {
|
||||
$required += @("RemLinkSite.exe", "site.yaml", "THIRD_PARTY_NOTICES.md")
|
||||
}
|
||||
Server {
|
||||
$required += @(
|
||||
"linux-amd64/remlink-server", "linux-amd64/server.yaml", "linux-amd64/THIRD_PARTY_NOTICES.md",
|
||||
"docker/compose.yaml", "docker/Dockerfile", "docker/compose.release.yaml", "docker/Dockerfile.release",
|
||||
"docker/.env.example", "docker/.env.china.example", "docker/preflight.sh", "docker/server.yaml", "docker/README.md",
|
||||
"docs/implementation-status.md",
|
||||
"scripts/validation/New-AcceptanceRun.ps1", "scripts/validation/Set-AcceptanceResult.ps1",
|
||||
"scripts/validation/Test-AcceptanceRun.ps1", "scripts/validation/Test-AcceptanceTools.ps1",
|
||||
"scripts/validation/Test-ReleasePackage.ps1", "scripts/validation/Collect-WindowsEvidence.ps1",
|
||||
"scripts/validation/Collect-ServerEvidence.sh", "scripts/validation/Test-RemoteTargets.ps1",
|
||||
"docs/validation/T01-T18-runbook.md", "docs/validation/requirements-evidence.md"
|
||||
)
|
||||
}
|
||||
}
|
||||
foreach ($relative in $required) {
|
||||
$target = Join-Path $packageRoot $relative
|
||||
if (-not (Test-Path -LiteralPath $target -PathType Leaf)) {
|
||||
throw "Required $Role release entry is missing: $relative"
|
||||
}
|
||||
if ((Get-Item -LiteralPath $target).Length -eq 0) {
|
||||
throw "Required $Role release entry is empty: $relative"
|
||||
}
|
||||
}
|
||||
if ($Role -in @("Engineer", "Site")) {
|
||||
$clientConfigName = if ($Role -eq "Engineer") { "engineer.yaml" } else { "site.yaml" }
|
||||
$clientConfig = Get-Content -Raw -LiteralPath (Join-Path $packageRoot $clientConfigName)
|
||||
if ($clientConfig -notmatch '(?m)^join_token:\s*') {
|
||||
throw "$Role package YAML does not expose the join_token convenience field"
|
||||
}
|
||||
if ($clientConfig -match '(?im)^\s*(node_token|wg_private_key)\s*:') {
|
||||
throw "$Role package YAML contains a durable Node secret field"
|
||||
}
|
||||
}
|
||||
|
||||
$checksumPath = Join-Path $packageRoot "SHA256SUMS.txt"
|
||||
$checksums = @{}
|
||||
foreach ($line in Get-Content -LiteralPath $checksumPath) {
|
||||
if ($line -notmatch '^([0-9a-f]{64}) (.+)$') {
|
||||
throw "Malformed checksum line: $line"
|
||||
}
|
||||
$relative = $Matches[2].Replace('\', '/')
|
||||
if ([System.IO.Path]::IsPathRooted($relative) -or $relative -match '(^|/)\.\.(/|$)' -or $relative -eq "SHA256SUMS.txt") {
|
||||
throw "Unsafe or self-referential checksum path: $relative"
|
||||
}
|
||||
if ($checksums.ContainsKey($relative)) {
|
||||
throw "Duplicate checksum path: $relative"
|
||||
}
|
||||
$checksums[$relative] = $Matches[1]
|
||||
}
|
||||
$actualFiles = @(Get-ChildItem -LiteralPath $packageRoot -Recurse -File | ForEach-Object {
|
||||
[System.IO.Path]::GetRelativePath($packageRoot, $_.FullName).Replace('\', '/')
|
||||
} | Where-Object { $_ -ne "SHA256SUMS.txt" })
|
||||
if ($checksums.Count -ne $actualFiles.Count) {
|
||||
throw "Checksum count $($checksums.Count) does not match package file count $($actualFiles.Count)"
|
||||
}
|
||||
foreach ($relative in $actualFiles) {
|
||||
if (-not $checksums.ContainsKey($relative)) {
|
||||
throw "Package file is not covered by SHA256SUMS.txt: $relative"
|
||||
}
|
||||
$actualHash = (Get-FileHash -Algorithm SHA256 -LiteralPath (Join-Path $packageRoot $relative)).Hash.ToLowerInvariant()
|
||||
if ($actualHash -ne $checksums[$relative]) {
|
||||
throw "Checksum mismatch: $relative"
|
||||
}
|
||||
}
|
||||
|
||||
$buildInfo = Get-Content -Raw -LiteralPath (Join-Path $packageRoot "BUILD-INFO.json") | ConvertFrom-Json
|
||||
if (-not $buildInfo.version -or $buildInfo.role -ne $Role -or @($buildInfo.target).Count -ne 1) {
|
||||
throw "BUILD-INFO.json does not describe one $Role target"
|
||||
}
|
||||
if ($Role -in @("Engineer", "Site") -and $buildInfo.portable_data_root -ne "executable_directory") {
|
||||
throw "$Role BUILD-INFO.json does not declare its portable executable data root"
|
||||
}
|
||||
if ($Role -eq "Engineer" -and (@($buildInfo.wails_build_tags) -join ',') -ne 'desktop,production') {
|
||||
throw "Engineer BUILD-INFO.json does not declare the mandatory Wails desktop,production tags"
|
||||
}
|
||||
|
||||
if ($Role -eq "Server") {
|
||||
$acceptanceRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("remlink-acceptance-smoke-" + [guid]::NewGuid().ToString("N"))
|
||||
try {
|
||||
& (Join-Path $packageRoot "scripts/validation/New-AcceptanceRun.ps1") -OutputDirectory $acceptanceRoot
|
||||
$manifest = Get-Content -Raw -LiteralPath (Join-Path $acceptanceRoot "acceptance-run.json") | ConvertFrom-Json
|
||||
if ($manifest.schema_version -ne 2 -or @($manifest.gates).Count -ne 4 -or @($manifest.scenarios).Count -ne 18) {
|
||||
throw "Packaged acceptance initializer produced an invalid manifest"
|
||||
}
|
||||
if (@($manifest.gates + $manifest.scenarios | Where-Object status -ne "NOT_RUN").Count -ne 0) {
|
||||
throw "Packaged acceptance initializer fabricated a completed result"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $acceptanceRoot "T01-T18-runbook.md") -PathType Leaf)) {
|
||||
throw "Packaged acceptance initializer did not copy its runbook"
|
||||
}
|
||||
} finally {
|
||||
$tempBase = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath())
|
||||
if ($acceptanceRoot -and $acceptanceRoot.StartsWith($tempBase, [System.StringComparison]::OrdinalIgnoreCase) -and
|
||||
[System.IO.Path]::GetFileName($acceptanceRoot).StartsWith("remlink-acceptance-smoke-")) {
|
||||
Remove-Item -LiteralPath $acceptanceRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
Write-Host "$Role release package verified: $packageRoot ($($checksums.Count) checksums)"
|
||||
} finally {
|
||||
if ($temporaryRoot) {
|
||||
$tempBase = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath())
|
||||
if ($temporaryRoot.StartsWith($tempBase, [System.StringComparison]::OrdinalIgnoreCase) -and
|
||||
[System.IO.Path]::GetFileName($temporaryRoot).StartsWith("remlink-release-verify-")) {
|
||||
Remove-Item -LiteralPath $temporaryRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user