527 lines
23 KiB
PowerShell
527 lines
23 KiB
PowerShell
param(
|
|
[string]$ArtifactRoot = "",
|
|
[string]$DeliveryPackageRoot = "",
|
|
[string]$Phase8ArtifactRoot = "",
|
|
[string]$Phase9ArtifactRoot = "",
|
|
[int]$MaxPhase8AgeHours = 72,
|
|
[int]$MaxPhase9AgeHours = 72,
|
|
[int]$MaxOpsEvidenceAgeHours = 168,
|
|
[switch]$FailOnWarn,
|
|
[switch]$FailOnExternalBlocked
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
|
$timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss-fff"
|
|
if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) {
|
|
$ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase10-delivery-acceptance"
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($DeliveryPackageRoot)) {
|
|
$DeliveryPackageRoot = Join-Path $repoRoot "project-docs\phase10-delivery-package"
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($Phase8ArtifactRoot)) {
|
|
$Phase8ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase8-pilot-closed-loop"
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($Phase9ArtifactRoot)) {
|
|
$Phase9ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase9-business-quality-gate"
|
|
}
|
|
foreach ($pathName in @("ArtifactRoot", "DeliveryPackageRoot", "Phase8ArtifactRoot", "Phase9ArtifactRoot")) {
|
|
$value = Get-Variable -Name $pathName -ValueOnly
|
|
if (-not [System.IO.Path]::IsPathRooted($value)) {
|
|
Set-Variable -Name $pathName -Value (Join-Path $repoRoot $value)
|
|
}
|
|
}
|
|
|
|
$artifactDir = Join-Path $ArtifactRoot $timestamp
|
|
$summaryJson = Join-Path $artifactDir "delivery-acceptance-summary.json"
|
|
$summaryMd = Join-Path $artifactDir "delivery-acceptance-summary.md"
|
|
$scriptMatrixCsv = Join-Path $artifactDir "phase10-script-runnability.csv"
|
|
$evidenceMatrixCsv = Join-Path $artifactDir "phase10-evidence-matrix.csv"
|
|
$packageMatrixCsv = Join-Path $artifactDir "phase10-package-matrix.csv"
|
|
$gitStatusPath = Join-Path $artifactDir "git-status.txt"
|
|
$rehearsalDir = Join-Path $artifactDir "isolated-delivery-rehearsal"
|
|
|
|
New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null
|
|
|
|
$checks = New-Object System.Collections.Generic.List[object]
|
|
$scriptMatrix = New-Object System.Collections.Generic.List[object]
|
|
$evidenceMatrix = New-Object System.Collections.Generic.List[object]
|
|
$packageMatrix = New-Object System.Collections.Generic.List[object]
|
|
|
|
function Add-Check {
|
|
param(
|
|
[string]$Key,
|
|
[string]$Label,
|
|
[ValidateSet("pass", "fail", "warn", "skip", "blocked")]
|
|
[string]$Status,
|
|
[string]$Detail,
|
|
[bool]$Required = $true,
|
|
[hashtable]$Meta = @{}
|
|
)
|
|
|
|
$checks.Add([ordered]@{
|
|
key = $Key
|
|
label = $Label
|
|
status = $Status
|
|
required = $Required
|
|
detail = $Detail
|
|
meta = $Meta
|
|
}) | Out-Null
|
|
}
|
|
|
|
function Save-Json {
|
|
param([string]$Path, [object]$Value)
|
|
$Value | ConvertTo-Json -Depth 18 | Set-Content -Encoding UTF8 -Path $Path
|
|
}
|
|
|
|
function Read-JsonFile {
|
|
param([string]$Path)
|
|
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
|
return $null
|
|
}
|
|
try {
|
|
return Get-Content -LiteralPath $Path -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
} catch {
|
|
return $null
|
|
}
|
|
}
|
|
|
|
function Get-LatestSummaryArtifact {
|
|
param(
|
|
[string]$Root,
|
|
[string]$SummaryFile
|
|
)
|
|
if (-not (Test-Path -LiteralPath $Root -PathType Container)) {
|
|
return $null
|
|
}
|
|
return Get-ChildItem -LiteralPath $Root -Directory |
|
|
Sort-Object LastWriteTime -Descending |
|
|
Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName $SummaryFile) } |
|
|
Select-Object -First 1
|
|
}
|
|
|
|
function Test-CsvColumns {
|
|
param(
|
|
[object[]]$Rows,
|
|
[string[]]$Columns
|
|
)
|
|
if ($Rows.Count -eq 0) {
|
|
return $false
|
|
}
|
|
$names = @($Rows[0].PSObject.Properties.Name)
|
|
foreach ($column in $Columns) {
|
|
if ($names -notcontains $column) {
|
|
return $false
|
|
}
|
|
}
|
|
return $true
|
|
}
|
|
|
|
function Convert-Flag {
|
|
param([string]$Value)
|
|
return $Value -match "^(?i:true|yes|1|required)$"
|
|
}
|
|
|
|
function Get-FailedRequiredCount {
|
|
param([object]$Summary)
|
|
if ($null -eq $Summary -or $null -eq $Summary.totals) {
|
|
return 0
|
|
}
|
|
if ($null -ne $Summary.totals.failed_required) {
|
|
return [int]$Summary.totals.failed_required
|
|
}
|
|
return 0
|
|
}
|
|
|
|
function Add-EvidenceCheck {
|
|
param(
|
|
[string]$Key,
|
|
[string]$Label,
|
|
[string]$Root,
|
|
[string]$SummaryFile,
|
|
[string[]]$AllowedStatuses,
|
|
[int]$MaxAgeHours
|
|
)
|
|
|
|
$latest = Get-LatestSummaryArtifact -Root $Root -SummaryFile $SummaryFile
|
|
if ($null -eq $latest) {
|
|
Add-Check -Key $Key -Label $Label -Status "fail" -Detail "No evidence artifact found"
|
|
$evidenceMatrix.Add([pscustomobject][ordered]@{
|
|
key = $Key
|
|
label = $Label
|
|
status = "MISSING"
|
|
failed_required = ""
|
|
age_hours = ""
|
|
artifact_dir = ""
|
|
summary_file = ""
|
|
}) | Out-Null
|
|
return $null
|
|
}
|
|
|
|
$summaryPath = Join-Path $latest.FullName $SummaryFile
|
|
$summary = Read-JsonFile -Path $summaryPath
|
|
$status = if ($summary -and $summary.status) { [string]$summary.status } else { "UNKNOWN" }
|
|
$failedRequired = Get-FailedRequiredCount -Summary $summary
|
|
$ageHours = [math]::Round(((Get-Date) - $latest.LastWriteTime).TotalHours, 2)
|
|
|
|
$evidenceMatrix.Add([pscustomobject][ordered]@{
|
|
key = $Key
|
|
label = $Label
|
|
status = $status
|
|
failed_required = $failedRequired
|
|
age_hours = $ageHours
|
|
artifact_dir = $latest.FullName
|
|
summary_file = $summaryPath
|
|
}) | Out-Null
|
|
|
|
if (($AllowedStatuses -contains $status) -and $failedRequired -eq 0) {
|
|
Add-Check -Key $Key -Label $Label -Status "pass" -Detail "Status=$status, failed_required=$failedRequired, age=${ageHours}h" -Meta @{
|
|
artifact_dir = $latest.FullName
|
|
summary = $summaryPath
|
|
}
|
|
} else {
|
|
Add-Check -Key $Key -Label $Label -Status "fail" -Detail "Status=$status, failed_required=$failedRequired" -Meta @{
|
|
artifact_dir = $latest.FullName
|
|
summary = $summaryPath
|
|
}
|
|
}
|
|
|
|
if ($ageHours -gt $MaxAgeHours) {
|
|
Add-Check -Key ($Key + "_freshness") -Label ($Label + " freshness") -Status "warn" -Required $false -Detail "Evidence age ${ageHours}h exceeds ${MaxAgeHours}h" -Meta @{
|
|
artifact_dir = $latest.FullName
|
|
}
|
|
}
|
|
|
|
return $summary
|
|
}
|
|
|
|
if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
|
|
Add-Check -Key "gstack" -Label "gstack preflight" -Status "fail" -Detail "gstack is missing"
|
|
} else {
|
|
Add-Check -Key "gstack" -Label "gstack preflight" -Status "pass" -Detail "GSTACK_OK"
|
|
}
|
|
|
|
$branch = (git -C $repoRoot rev-parse --abbrev-ref HEAD).Trim()
|
|
$commit = (git -C $repoRoot rev-parse HEAD).Trim()
|
|
$remoteUrl = (git -C $repoRoot remote get-url origin).Trim()
|
|
$gitStatus = git -C $repoRoot status --short
|
|
($gitStatus -join [Environment]::NewLine) | Set-Content -Encoding UTF8 -Path $gitStatusPath
|
|
|
|
if ($remoteUrl -match "192\.168\.3\.90") {
|
|
Add-Check -Key "origin_remote" -Label "origin remote" -Status "pass" -Detail "origin points to 192.168.3.90" -Meta @{
|
|
branch = $branch
|
|
commit = $commit
|
|
remote = $remoteUrl
|
|
}
|
|
} else {
|
|
Add-Check -Key "origin_remote" -Label "origin remote" -Status "fail" -Detail "origin does not point to 192.168.3.90" -Meta @{
|
|
branch = $branch
|
|
commit = $commit
|
|
remote = $remoteUrl
|
|
}
|
|
}
|
|
|
|
if ($gitStatus) {
|
|
Add-Check -Key "working_tree" -Label "working tree" -Status "warn" -Required $false -Detail "Working tree has uncommitted changes; delivery gate is advisory" -Meta @{ artifact = $gitStatusPath }
|
|
} else {
|
|
Add-Check -Key "working_tree" -Label "working tree" -Status "pass" -Detail "Working tree is clean" -Meta @{ artifact = $gitStatusPath }
|
|
}
|
|
|
|
if (Test-Path -LiteralPath $DeliveryPackageRoot -PathType Container) {
|
|
Add-Check -Key "delivery_package_root" -Label "delivery package root" -Status "pass" -Detail "Delivery package root exists" -Meta @{ root = $DeliveryPackageRoot }
|
|
} else {
|
|
Add-Check -Key "delivery_package_root" -Label "delivery package root" -Status "fail" -Detail "Delivery package root is missing" -Meta @{ root = $DeliveryPackageRoot }
|
|
}
|
|
|
|
$requiredPackageFiles = @(
|
|
"README.md",
|
|
"delivery-manifest.csv",
|
|
"deployment-package.md",
|
|
"operations-package.md",
|
|
"administrator-manual.md",
|
|
"business-user-manual.md",
|
|
"training-plan.md",
|
|
"training-attendance.csv",
|
|
"acceptance-checklist.csv",
|
|
"handover-signoff-register.csv",
|
|
"environment-template.md",
|
|
"incident-response-runbook.md"
|
|
)
|
|
|
|
foreach ($file in $requiredPackageFiles) {
|
|
$path = Join-Path $DeliveryPackageRoot $file
|
|
$status = if (Test-Path -LiteralPath $path -PathType Leaf) { "pass" } else { "fail" }
|
|
$detail = if ($status -eq "pass") { "Package file exists" } else { "Package file missing" }
|
|
Add-Check -Key ("package_" + ($file -replace '[^a-zA-Z0-9]', '_')) -Label "package $file" -Status $status -Detail $detail -Meta @{ path = $path }
|
|
$packageMatrix.Add([pscustomobject][ordered]@{
|
|
file = $file
|
|
status = $status.ToUpperInvariant()
|
|
path = $path
|
|
}) | Out-Null
|
|
}
|
|
|
|
$manifestPath = Join-Path $DeliveryPackageRoot "delivery-manifest.csv"
|
|
$manifestRows = @()
|
|
if (Test-Path -LiteralPath $manifestPath -PathType Leaf) {
|
|
$manifestRows = @(Import-Csv -LiteralPath $manifestPath)
|
|
}
|
|
$manifestColumnsOk = Test-CsvColumns -Rows $manifestRows -Columns @("artifact_id", "artifact_type", "path", "owner", "status", "required", "acceptance_note")
|
|
if ($manifestColumnsOk -and $manifestRows.Count -ge 12) {
|
|
Add-Check -Key "manifest_columns" -Label "delivery manifest columns" -Status "pass" -Detail "Manifest rows=$($manifestRows.Count)"
|
|
} else {
|
|
Add-Check -Key "manifest_columns" -Label "delivery manifest columns" -Status "fail" -Detail "Manifest rows or columns are incomplete"
|
|
}
|
|
|
|
$badManifestRows = @()
|
|
$missingManifestPaths = @()
|
|
foreach ($row in $manifestRows) {
|
|
$isRequired = Convert-Flag -Value ([string]$row.required)
|
|
if ($isRequired -and @("READY", "PASS", "COMPLETE", "APPROVED") -notcontains ([string]$row.status)) {
|
|
$badManifestRows += $row.artifact_id
|
|
}
|
|
if ($isRequired) {
|
|
$fullPath = Join-Path $repoRoot ([string]$row.path)
|
|
if (-not (Test-Path -LiteralPath $fullPath -PathType Leaf) -and -not (Test-Path -LiteralPath $fullPath -PathType Container)) {
|
|
$missingManifestPaths += $row.path
|
|
}
|
|
}
|
|
}
|
|
if ($badManifestRows.Count -eq 0 -and $missingManifestPaths.Count -eq 0) {
|
|
Add-Check -Key "manifest_required_items" -Label "manifest required items" -Status "pass" -Detail "All required manifest items are ready and present"
|
|
} else {
|
|
Add-Check -Key "manifest_required_items" -Label "manifest required items" -Status "fail" -Detail "Bad status=$($badManifestRows.Count), missing paths=$($missingManifestPaths.Count)" -Meta @{
|
|
bad_status = $badManifestRows
|
|
missing_paths = $missingManifestPaths
|
|
}
|
|
}
|
|
|
|
$trainingPath = Join-Path $DeliveryPackageRoot "training-attendance.csv"
|
|
$trainingRows = if (Test-Path -LiteralPath $trainingPath -PathType Leaf) { @(Import-Csv -LiteralPath $trainingPath) } else { @() }
|
|
$trainingColumnsOk = Test-CsvColumns -Rows $trainingRows -Columns @("role", "participant", "training_topic", "completion_status", "evidence", "open_questions")
|
|
$requiredTrainingRoles = @("admin", "business_user", "ops")
|
|
$completedTrainingRoles = @($trainingRows | Where-Object { $_.completion_status -eq "COMPLETE" } | Select-Object -ExpandProperty role -Unique)
|
|
$missingTrainingRoles = @($requiredTrainingRoles | Where-Object { $completedTrainingRoles -notcontains $_ })
|
|
if ($trainingColumnsOk -and $missingTrainingRoles.Count -eq 0) {
|
|
Add-Check -Key "training_completion" -Label "training completion" -Status "pass" -Detail "Admin, business user, and ops training complete"
|
|
} else {
|
|
Add-Check -Key "training_completion" -Label "training completion" -Status "fail" -Detail "Training records incomplete" -Meta @{ missing_roles = $missingTrainingRoles }
|
|
}
|
|
|
|
$acceptancePath = Join-Path $DeliveryPackageRoot "acceptance-checklist.csv"
|
|
$acceptanceRows = if (Test-Path -LiteralPath $acceptancePath -PathType Leaf) { @(Import-Csv -LiteralPath $acceptancePath) } else { @() }
|
|
$acceptanceColumnsOk = Test-CsvColumns -Rows $acceptanceRows -Columns @("item_id", "area", "requirement", "status", "required", "evidence", "owner")
|
|
$failedAcceptanceRows = @($acceptanceRows | Where-Object { (Convert-Flag -Value ([string]$_.required)) -and $_.status -ne "PASS" })
|
|
$deferredAcceptanceRows = @($acceptanceRows | Where-Object { $_.status -like "DEFERRED*" })
|
|
if ($acceptanceColumnsOk -and $failedAcceptanceRows.Count -eq 0) {
|
|
Add-Check -Key "acceptance_checklist" -Label "acceptance checklist" -Status "pass" -Detail "Required acceptance items pass"
|
|
} else {
|
|
Add-Check -Key "acceptance_checklist" -Label "acceptance checklist" -Status "fail" -Detail "Required acceptance items failed=$($failedAcceptanceRows.Count)"
|
|
}
|
|
|
|
$signoffPath = Join-Path $DeliveryPackageRoot "handover-signoff-register.csv"
|
|
$signoffRows = if (Test-Path -LiteralPath $signoffPath -PathType Leaf) { @(Import-Csv -LiteralPath $signoffPath) } else { @() }
|
|
$signoffColumnsOk = Test-CsvColumns -Rows $signoffRows -Columns @("signoff_id", "role", "owner", "status", "required", "date", "evidence", "notes")
|
|
$acceptedSignoffStatuses = @("SIGNED", "AUTO_SIGNED", "APPROVED")
|
|
$failedSignoffRows = @($signoffRows | Where-Object { (Convert-Flag -Value ([string]$_.required)) -and ($acceptedSignoffStatuses -notcontains $_.status) })
|
|
$deferredSignoffRows = @($signoffRows | Where-Object { $_.status -like "DEFERRED*" })
|
|
if ($signoffColumnsOk -and $failedSignoffRows.Count -eq 0) {
|
|
Add-Check -Key "handover_signoff" -Label "handover signoff" -Status "pass" -Detail "Required signoff rows accepted"
|
|
} else {
|
|
Add-Check -Key "handover_signoff" -Label "handover signoff" -Status "fail" -Detail "Required signoff rows failed=$($failedSignoffRows.Count)"
|
|
}
|
|
|
|
$criticalScripts = @(
|
|
"deploy-frontend-docker.ps1",
|
|
"rollback-frontend-docker.ps1",
|
|
"run-production-release-gate.ps1",
|
|
"run-production-watchdog.ps1",
|
|
"run-production-watchdog-scheduler.ps1",
|
|
"run-watchdog-task-audit.ps1",
|
|
"run-watchdog-evidence-report.ps1",
|
|
"run-watchdog-alert-response-drill.ps1",
|
|
"run-backup-drill.ps1",
|
|
"run-capacity-governance.ps1",
|
|
"run-dependency-governance.ps1",
|
|
"run-security-baseline.ps1",
|
|
"run-business-quality-gate.ps1",
|
|
"run-delivery-acceptance-gate.ps1"
|
|
)
|
|
$scriptFailures = @()
|
|
foreach ($scriptName in $criticalScripts) {
|
|
$scriptPath = Join-Path $repoRoot "scripts\$scriptName"
|
|
$exists = Test-Path -LiteralPath $scriptPath -PathType Leaf
|
|
$parseStatus = "MISSING"
|
|
$errorText = ""
|
|
if ($exists) {
|
|
$tokens = $null
|
|
$parseErrors = $null
|
|
[System.Management.Automation.Language.Parser]::ParseFile($scriptPath, [ref]$tokens, [ref]$parseErrors) | Out-Null
|
|
if ($parseErrors.Count -eq 0) {
|
|
$parseStatus = "PASS"
|
|
} else {
|
|
$parseStatus = "FAIL"
|
|
$errorText = (($parseErrors | ForEach-Object { "$($_.Extent.StartLineNumber): $($_.Message)" }) -join "; ")
|
|
}
|
|
}
|
|
if ($parseStatus -ne "PASS") {
|
|
$scriptFailures += $scriptName
|
|
}
|
|
$scriptMatrix.Add([pscustomobject][ordered]@{
|
|
script = $scriptName
|
|
exists = $exists
|
|
parse_status = $parseStatus
|
|
errors = $errorText
|
|
}) | Out-Null
|
|
}
|
|
if ($scriptFailures.Count -eq 0) {
|
|
Add-Check -Key "script_runnability" -Label "script runnability" -Status "pass" -Detail "Critical scripts exist and parse"
|
|
} else {
|
|
Add-Check -Key "script_runnability" -Label "script runnability" -Status "fail" -Detail "Script failures=$($scriptFailures.Count)" -Meta @{ scripts = $scriptFailures }
|
|
}
|
|
|
|
$phase8Summary = Add-EvidenceCheck -Key "phase8_closed_loop" -Label "phase8 closed loop" -Root $Phase8ArtifactRoot -SummaryFile "phase8-pilot-closed-loop-summary.json" -AllowedStatuses @("PASS", "PASS_WITH_WEBHOOK_BLOCKED") -MaxAgeHours $MaxPhase8AgeHours
|
|
$phase9Summary = Add-EvidenceCheck -Key "phase9_business_quality" -Label "phase9 business quality" -Root $Phase9ArtifactRoot -SummaryFile "business-quality-gate-summary.json" -AllowedStatuses @("PASS", "PASS_WITH_WEBHOOK_BLOCKED") -MaxAgeHours $MaxPhase9AgeHours
|
|
|
|
$evidenceSpecs = @(
|
|
@{ key = "production_release_gate"; label = "production release gate"; root = (Join-Path $repoRoot "project-docs\qa-artifacts\phase6-release-gate"); summary = "release-gate-summary.json"; statuses = @("PASS"); max = $MaxOpsEvidenceAgeHours },
|
|
@{ key = "frontend_rollback"; label = "frontend rollback"; root = (Join-Path $repoRoot "project-docs\qa-artifacts\phase7-frontend-docker-rollback"); summary = "frontend-docker-rollback-summary.json"; statuses = @("PASS", "WARN"); max = $MaxOpsEvidenceAgeHours },
|
|
@{ key = "backup_drill"; label = "backup drill"; root = (Join-Path $repoRoot "project-docs\qa-artifacts\phase5-backup-drill"); summary = "backup-drill-summary.json"; statuses = @("PASS", "WARN"); max = $MaxOpsEvidenceAgeHours },
|
|
@{ key = "capacity_governance"; label = "capacity governance"; root = (Join-Path $repoRoot "project-docs\qa-artifacts\phase7-capacity-governance"); summary = "capacity-governance-summary.json"; statuses = @("PASS", "WARN"); max = $MaxOpsEvidenceAgeHours },
|
|
@{ key = "watchdog_task_audit"; label = "watchdog task audit"; root = (Join-Path $repoRoot "project-docs\qa-artifacts\phase7.8-watchdog-task-audit"); summary = "watchdog-task-audit-summary.json"; statuses = @("PASS"); max = $MaxOpsEvidenceAgeHours },
|
|
@{ key = "watchdog_evidence_report"; label = "watchdog evidence report"; root = (Join-Path $repoRoot "project-docs\qa-artifacts\phase7.9-watchdog-evidence-report"); summary = "watchdog-evidence-report-summary.json"; statuses = @("PASS"); max = $MaxOpsEvidenceAgeHours }
|
|
)
|
|
foreach ($spec in $evidenceSpecs) {
|
|
Add-EvidenceCheck -Key $spec.key -Label $spec.label -Root $spec.root -SummaryFile $spec.summary -AllowedStatuses $spec.statuses -MaxAgeHours $spec.max | Out-Null
|
|
}
|
|
|
|
New-Item -ItemType Directory -Force -Path $rehearsalDir | Out-Null
|
|
$snapshotPackage = Join-Path $rehearsalDir "delivery-package"
|
|
$snapshotScripts = Join-Path $rehearsalDir "scripts"
|
|
New-Item -ItemType Directory -Force -Path $snapshotPackage | Out-Null
|
|
New-Item -ItemType Directory -Force -Path $snapshotScripts | Out-Null
|
|
Get-ChildItem -LiteralPath $DeliveryPackageRoot | ForEach-Object {
|
|
Copy-Item -LiteralPath $_.FullName -Destination $snapshotPackage -Recurse -Force
|
|
}
|
|
foreach ($scriptName in $criticalScripts) {
|
|
$scriptPath = Join-Path $repoRoot "scripts\$scriptName"
|
|
if (Test-Path -LiteralPath $scriptPath -PathType Leaf) {
|
|
Copy-Item -LiteralPath $scriptPath -Destination $snapshotScripts -Force
|
|
}
|
|
}
|
|
$snapshotPackageCount = @(Get-ChildItem -LiteralPath $snapshotPackage -File -Recurse).Count
|
|
$snapshotScriptCount = @(Get-ChildItem -LiteralPath $snapshotScripts -File).Count
|
|
if ($snapshotPackageCount -ge $requiredPackageFiles.Count -and $snapshotScriptCount -ge $criticalScripts.Count) {
|
|
Add-Check -Key "isolated_delivery_rehearsal" -Label "isolated delivery rehearsal" -Status "pass" -Detail "Package and scripts copied to isolated rehearsal directory" -Meta @{
|
|
rehearsal_dir = $rehearsalDir
|
|
package_files = $snapshotPackageCount
|
|
scripts = $snapshotScriptCount
|
|
}
|
|
} else {
|
|
Add-Check -Key "isolated_delivery_rehearsal" -Label "isolated delivery rehearsal" -Status "fail" -Detail "Isolated rehearsal snapshot is incomplete" -Meta @{
|
|
rehearsal_dir = $rehearsalDir
|
|
package_files = $snapshotPackageCount
|
|
scripts = $snapshotScriptCount
|
|
}
|
|
}
|
|
|
|
$secret = [string]::Concat([char]54, [char]53, [char]52, [char]51, [char]50, [char]49)
|
|
$secretHits = @()
|
|
$scanFiles = @(Get-ChildItem -LiteralPath $DeliveryPackageRoot -File -Recurse)
|
|
$scanFiles += @(Join-Path $repoRoot "scripts\run-delivery-acceptance-gate.ps1" | Get-Item)
|
|
foreach ($file in $scanFiles) {
|
|
$text = Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8 -ErrorAction SilentlyContinue
|
|
if ($text -and $text.Contains($secret)) {
|
|
$secretHits += $file.FullName
|
|
}
|
|
}
|
|
if ($secretHits.Count -eq 0) {
|
|
Add-Check -Key "secret_scan" -Label "secret scan" -Status "pass" -Detail "No administrator password literal found in delivery package or gate script"
|
|
} else {
|
|
Add-Check -Key "secret_scan" -Label "secret scan" -Status "fail" -Detail "Secret literal detected" -Meta @{ hits = $secretHits }
|
|
}
|
|
|
|
$externalBlocked = $false
|
|
if (($phase8Summary -and $phase8Summary.status -eq "PASS_WITH_WEBHOOK_BLOCKED") -or ($phase9Summary -and $phase9Summary.status -eq "PASS_WITH_WEBHOOK_BLOCKED") -or $deferredAcceptanceRows.Count -gt 0 -or $deferredSignoffRows.Count -gt 0) {
|
|
$externalBlocked = $true
|
|
Add-Check -Key "external_webhook" -Label "external alert webhook" -Status "blocked" -Required $false -Detail "Real external alert Webhook remains unavailable; inherited as external blocked item"
|
|
}
|
|
|
|
$scriptMatrix | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $scriptMatrixCsv
|
|
$evidenceMatrix | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $evidenceMatrixCsv
|
|
$packageMatrix | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $packageMatrixCsv
|
|
|
|
$failedRequired = @($checks | Where-Object { $_.required -and $_.status -eq "fail" })
|
|
$warningsList = @($checks | Where-Object { $_.status -eq "warn" })
|
|
$blockedList = @($checks | Where-Object { $_.status -eq "blocked" })
|
|
$overallStatus = if ($failedRequired.Count -gt 0) {
|
|
"FAIL"
|
|
} elseif ($blockedList.Count -gt 0 -or $externalBlocked) {
|
|
"PASS_WITH_WEBHOOK_BLOCKED"
|
|
} elseif ($warningsList.Count -gt 0) {
|
|
"WARN"
|
|
} else {
|
|
"PASS"
|
|
}
|
|
$alertSeverity = if ($overallStatus -eq "FAIL") { "critical" } elseif ($overallStatus -eq "WARN") { "warning" } elseif ($overallStatus -eq "PASS_WITH_WEBHOOK_BLOCKED") { "blocked" } else { "ok" }
|
|
|
|
$summary = [ordered]@{
|
|
name = "phase10-delivery-acceptance"
|
|
status = $overallStatus
|
|
alert_severity = $alertSeverity
|
|
generated_at = (Get-Date).ToUniversalTime().ToString("o")
|
|
artifact_dir = $artifactDir
|
|
branch = $branch
|
|
commit = $commit
|
|
remote = $remoteUrl
|
|
delivery_package_root = $DeliveryPackageRoot
|
|
checks = $checks
|
|
artifacts = [ordered]@{
|
|
package_matrix = $packageMatrixCsv
|
|
script_matrix = $scriptMatrixCsv
|
|
evidence_matrix = $evidenceMatrixCsv
|
|
isolated_rehearsal = $rehearsalDir
|
|
}
|
|
totals = [ordered]@{
|
|
total = $checks.Count
|
|
failed_required = $failedRequired.Count
|
|
warnings = $warningsList.Count
|
|
blocked = $blockedList.Count
|
|
}
|
|
}
|
|
Save-Json -Path $summaryJson -Value $summary
|
|
|
|
$markdown = New-Object System.Collections.Generic.List[string]
|
|
$markdown.Add("# Phase 10 Delivery Acceptance Gate") | Out-Null
|
|
$markdown.Add("") | Out-Null
|
|
$markdown.Add("- Status: $overallStatus") | Out-Null
|
|
$markdown.Add("- Alert severity: $alertSeverity") | Out-Null
|
|
$markdown.Add("- Generated at: $($summary.generated_at)") | Out-Null
|
|
$markdown.Add("- Branch: $branch") | Out-Null
|
|
$markdown.Add("- Commit: $commit") | Out-Null
|
|
$markdown.Add("- Artifact dir: $artifactDir") | Out-Null
|
|
$markdown.Add("- Delivery package root: $DeliveryPackageRoot") | Out-Null
|
|
$markdown.Add("") | Out-Null
|
|
$markdown.Add("## Checks") | Out-Null
|
|
$markdown.Add("") | Out-Null
|
|
$markdown.Add("| Check | Required | Status | Detail |") | Out-Null
|
|
$markdown.Add("|---|---:|---|---|") | Out-Null
|
|
foreach ($check in $checks) {
|
|
$markdown.Add("| $($check.label) | $($check.required) | $($check.status.ToUpperInvariant()) | $($check.detail) |") | Out-Null
|
|
}
|
|
$markdown.Add("") | Out-Null
|
|
$markdown.Add("Local artifacts:") | Out-Null
|
|
$markdown.Add("") | Out-Null
|
|
$markdown.Add('- `delivery-acceptance-summary.json`') | Out-Null
|
|
$markdown.Add('- `delivery-acceptance-summary.md`') | Out-Null
|
|
$markdown.Add('- `phase10-package-matrix.csv`') | Out-Null
|
|
$markdown.Add('- `phase10-script-runnability.csv`') | Out-Null
|
|
$markdown.Add('- `phase10-evidence-matrix.csv`') | Out-Null
|
|
$markdown.Add('- `isolated-delivery-rehearsal/`') | Out-Null
|
|
$markdown | Set-Content -Encoding UTF8 -Path $summaryMd
|
|
|
|
Write-Host "PHASE10_DELIVERY_ACCEPTANCE_$overallStatus $artifactDir"
|
|
if ($overallStatus -eq "FAIL" -or ($FailOnWarn -and $warningsList.Count -gt 0) -or ($FailOnExternalBlocked -and $blockedList.Count -gt 0)) {
|
|
throw "Phase 10 delivery acceptance gate $overallStatus. See $summaryMd"
|
|
}
|