Files
ai-contract-platform/scripts/run-production-release-gate.ps1
T
2026-05-01 11:35:56 +08:00

260 lines
9.6 KiB
PowerShell

param(
[string]$ServerHost = "192.168.3.90",
[string]$BackendBaseUrl = "http://192.168.3.90:8001",
[string]$FrontendBaseUrl = "http://192.168.3.90:3003",
[string]$ApiUrl = "http://192.168.3.90:8001/api/v1",
[string]$HealthUrl = "http://192.168.3.90:8001/health",
[string]$ReadinessUrl = "http://192.168.3.90:8001/api/v1/system/readiness",
[string]$ArtifactRoot = "",
[switch]$RequireCleanWorktree,
[switch]$IncludeBackupDrill,
[switch]$SkipBuild,
[switch]$SkipProductionRegression
)
$ErrorActionPreference = "Stop"
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
$timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss"
if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) {
$ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase6-release-gate"
}
if (-not [System.IO.Path]::IsPathRooted($ArtifactRoot)) {
$ArtifactRoot = Join-Path $repoRoot $ArtifactRoot
}
$artifactDir = Join-Path $ArtifactRoot $timestamp
$summaryJson = Join-Path $artifactDir "release-gate-summary.json"
$summaryMd = Join-Path $artifactDir "release-gate-summary.md"
$gitStatusPath = Join-Path $artifactDir "git-status.txt"
New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null
$checks = New-Object System.Collections.Generic.List[object]
function Add-Check {
param(
[string]$Key,
[string]$Label,
[ValidateSet("pass", "fail", "warn", "skip")]
[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 12 | Set-Content -Encoding UTF8 -Path $Path
}
function Invoke-GateStep {
param(
[string]$Key,
[string]$Label,
[string]$ScriptName,
[string[]]$Arguments
)
$scriptPath = Join-Path $repoRoot "scripts\$ScriptName"
$logPath = Join-Path $artifactDir "$Key.log"
$previousErrorActionPreference = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try {
$output = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $scriptPath @Arguments 2>&1
$exitCode = $LASTEXITCODE
} finally {
$ErrorActionPreference = $previousErrorActionPreference
}
$outputText = ($output -join [Environment]::NewLine)
$outputText | Set-Content -Encoding UTF8 -Path $logPath
$childArtifact = $null
$artifactMatch = [regex]::Match($outputText, '([A-Z]:\\[^\r\n]+)$', [System.Text.RegularExpressions.RegexOptions]::Multiline)
if ($artifactMatch.Success) {
$childArtifact = $artifactMatch.Groups[1].Value.Trim()
}
if ($exitCode -eq 0 -and $outputText -match "_WARN") {
Add-Check -Key $Key -Label $Label -Status "warn" -Required $false -Detail "$Label completed with warnings" -Meta @{
log = $logPath
artifact_dir = $childArtifact
}
} elseif ($exitCode -eq 0) {
Add-Check -Key $Key -Label $Label -Status "pass" -Detail "$Label passed" -Meta @{
log = $logPath
artifact_dir = $childArtifact
}
} else {
Add-Check -Key $Key -Label $Label -Status "fail" -Detail "$Label failed" -Meta @{
exit_code = $exitCode
log = $logPath
artifact_dir = $childArtifact
}
}
}
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()
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
}
}
$gitStatus = git -C $repoRoot status --short
($gitStatus -join [Environment]::NewLine) | Set-Content -Encoding UTF8 -Path $gitStatusPath
if ($gitStatus) {
if ($RequireCleanWorktree) {
Add-Check -Key "working_tree" -Label "working tree" -Status "fail" -Detail "Working tree has uncommitted changes and -RequireCleanWorktree is set" -Meta @{ artifact = $gitStatusPath }
} else {
Add-Check -Key "working_tree" -Label "working tree" -Status "warn" -Required $false -Detail "Working tree has uncommitted changes; release gate is running in advisory mode" -Meta @{ artifact = $gitStatusPath }
}
} else {
Add-Check -Key "working_tree" -Label "working tree" -Status "pass" -Detail "Working tree is clean" -Meta @{ artifact = $gitStatusPath }
}
$dependencyArgs = @("-ArtifactRoot", (Join-Path $artifactDir "dependency-governance"))
if ($SkipBuild) {
$dependencyArgs += "-SkipBuild"
}
Invoke-GateStep -Key "dependency_governance" -Label "dependency governance" -ScriptName "run-dependency-governance.ps1" -Arguments $dependencyArgs
Invoke-GateStep -Key "production_health" -Label "production health" -ScriptName "run-production-health-check.ps1" -Arguments @(
"-ServerHost", $ServerHost,
"-BackendBaseUrl", $BackendBaseUrl,
"-FrontendBaseUrl", $FrontendBaseUrl,
"-ArtifactRoot", (Join-Path $artifactDir "production-health")
)
Invoke-GateStep -Key "security_baseline" -Label "security baseline" -ScriptName "run-security-baseline.ps1" -Arguments @(
"-ServerHost", $ServerHost,
"-BackendBaseUrl", $BackendBaseUrl,
"-FrontendBaseUrl", $FrontendBaseUrl,
"-ArtifactRoot", (Join-Path $artifactDir "security-baseline")
)
if ($SkipProductionRegression) {
Add-Check -Key "production_regression" -Label "production regression" -Status "skip" -Required $false -Detail "Skipped by -SkipProductionRegression"
} elseif ([string]::IsNullOrWhiteSpace($env:AI_CONTRACT_ADMIN_PASSWORD)) {
Add-Check -Key "production_regression" -Label "production regression" -Status "fail" -Detail "AI_CONTRACT_ADMIN_PASSWORD must be set in the current process"
} else {
Invoke-GateStep -Key "production_regression" -Label "production regression" -ScriptName "run-production-regression.ps1" -Arguments @(
"-WebUrl", $FrontendBaseUrl,
"-ApiUrl", $ApiUrl,
"-HealthUrl", $HealthUrl,
"-ReadinessUrl", $ReadinessUrl,
"-ArtifactRoot", (Join-Path $artifactDir "production-regression")
)
}
if ($IncludeBackupDrill) {
Invoke-GateStep -Key "backup_drill" -Label "backup drill" -ScriptName "run-backup-drill.ps1" -Arguments @(
"-ServerHost", $ServerHost,
"-ArtifactRoot", (Join-Path $artifactDir "backup-drill"),
"-RemoteBackupRoot", "/opt/ai-contract-platform/backups/phase6-release-gate"
)
} else {
Add-Check -Key "backup_drill" -Label "backup drill" -Status "skip" -Required $false -Detail "Skipped by default; pass -IncludeBackupDrill for weekly/full release gate"
}
$failedRequired = @($checks | Where-Object { $_.required -and $_.status -eq "fail" })
$warningsList = @($checks | Where-Object { $_.status -eq "warn" })
$skippedList = @($checks | Where-Object { $_.status -eq "skip" })
$overallStatus = if ($failedRequired.Count -gt 0) { "FAIL" } elseif ($warningsList.Count -gt 0) { "WARN" } else { "PASS" }
$summary = [ordered]@{
name = "phase6-release-gate"
status = $overallStatus
generated_at = (Get-Date).ToUniversalTime().ToString("o")
artifact_dir = $artifactDir
branch = $branch
commit = $commit
remote = $remoteUrl
targets = [ordered]@{
server_host = $ServerHost
frontend_base_url = $FrontendBaseUrl
backend_base_url = $BackendBaseUrl
api_url = $ApiUrl
health_url = $HealthUrl
readiness_url = $ReadinessUrl
}
options = [ordered]@{
require_clean_worktree = [bool]$RequireCleanWorktree
include_backup_drill = [bool]$IncludeBackupDrill
skip_build = [bool]$SkipBuild
skip_production_regression = [bool]$SkipProductionRegression
}
checks = $checks
totals = [ordered]@{
total = $checks.Count
failed_required = $failedRequired.Count
warnings = $warningsList.Count
skipped = $skippedList.Count
}
}
Save-Json -Path $summaryJson -Value $summary
$markdown = New-Object System.Collections.Generic.List[string]
$markdown.Add("# Phase 6 Release Gate") | Out-Null
$markdown.Add("") | Out-Null
$markdown.Add("- Status: $overallStatus") | 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("") | 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("Child artifacts:") | Out-Null
$markdown.Add("") | Out-Null
foreach ($check in $checks) {
if ($check.meta.artifact_dir) {
$markdown.Add(('- {0}: `{1}`' -f $check.label, $check.meta.artifact_dir)) | Out-Null
}
}
$markdown.Add("") | Out-Null
$markdown.Add("Local artifacts:") | Out-Null
$markdown.Add("") | Out-Null
$markdown.Add('- `release-gate-summary.json`') | Out-Null
$markdown.Add('- `release-gate-summary.md`') | Out-Null
$markdown.Add('- `git-status.txt`') | Out-Null
$markdown.Add('- `*.log`') | Out-Null
$markdown | Set-Content -Encoding UTF8 -Path $summaryMd
Write-Host "PHASE6_RELEASE_GATE_$overallStatus $artifactDir"
if ($overallStatus -eq "FAIL") {
throw "Phase 6 release gate failed. See $summaryMd"
}