From 70d83f33171f2bdb74a33dd71171b39c8010d3a3 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 1 May 2026 11:25:53 +0800 Subject: [PATCH] chore: add frontend docker deploy automation --- project-docs/运维手册-2026-05-01.md | 10 + .../闭环交付与深度开发方案-2026-05-01.md | 3 +- ...®µ7-一键前端Docker发布执行记录-2026-05-01.md | 81 ++++ ...7-生产运营常态化与一键发布方案-2026-05-01.md | 95 ++++ scripts/deploy-frontend-docker.ps1 | 415 ++++++++++++++++++ 5 files changed, 603 insertions(+), 1 deletion(-) create mode 100644 project-docs/阶段7-一键前端Docker发布执行记录-2026-05-01.md create mode 100644 project-docs/阶段7-生产运营常态化与一键发布方案-2026-05-01.md create mode 100644 scripts/deploy-frontend-docker.ps1 diff --git a/project-docs/运维手册-2026-05-01.md b/project-docs/运维手册-2026-05-01.md index bf275262..259b26dd 100644 --- a/project-docs/运维手册-2026-05-01.md +++ b/project-docs/运维手册-2026-05-01.md @@ -208,6 +208,16 @@ project-docs/qa-artifacts/phase5-production-regression// ## 七、发布门禁口径 +一键前端 Docker 发布: + +```powershell +$env:AI_CONTRACT_ADMIN_PASSWORD = '<由管理员输入的实际密码>' +powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\deploy-frontend-docker.ps1 +Remove-Item Env:\AI_CONTRACT_ADMIN_PASSWORD +``` + +该脚本会自动打包本地 `frontend`、上传到 `192.168.3.90`、备份远端旧 frontend、重建并重启 frontend Docker 容器,并执行 post-deploy release gate。 + 默认发布门禁: ```powershell diff --git a/project-docs/闭环交付与深度开发方案-2026-05-01.md b/project-docs/闭环交付与深度开发方案-2026-05-01.md index b3fa3c2c..4ee8b0a9 100644 --- a/project-docs/闭环交付与深度开发方案-2026-05-01.md +++ b/project-docs/闭环交付与深度开发方案-2026-05-01.md @@ -16,10 +16,11 @@ | 阶段 4:试点运行监控与安全加固 | PASS | `project-docs/阶段4-试点运行监控与安全加固执行记录-2026-05-01.md` | | 阶段 5:生产运行、安全、监控、备份与依赖治理 | PASS | `project-docs/阶段5-生产运行安全监控备份与依赖治理执行记录-2026-05-01.md` | | 阶段 6:生产运营自动化与发布门禁固化 | PASS | `project-docs/阶段6-生产运营自动化与发布门禁固化执行记录-2026-05-01.md` | +| 阶段 7:生产运营常态化与一键发布 | IN PROGRESS | `project-docs/阶段7-一键前端Docker发布执行记录-2026-05-01.md` | 阶段 1 已完成源码闭环与生产入口闭环,生产无人 E2E `18/18` 通过。阶段 2 已完成 Review 重试、Knowledge Base 文档重试、Review -> Chat -> Contract 上下文联动、首页交付工作台和阶段 2 自动化无人闭环,本地与生产 E2E 均 `7/7` 通过。阶段 3 已完成系统 readiness、试点交付准备页、默认账号说明、演示样本数据、依赖健康门禁和阶段 3 自动化无人闭环,本地与生产 E2E 均 `5/5` 通过。 -当前后续重点已从“生产运行治理”推进到“生产运营自动化与发布门禁固化”:阶段 6 已完成前端视觉改动梳理、发布门禁聚合脚本、默认发布门禁实跑、严格门禁探针、前端视觉刷新验收、视觉刷新提交、严格发布门禁复跑,以及 `192.168.3.90` Docker 发布后上线验证。 +当前后续重点已从“生产运营自动化与发布门禁固化”推进到“生产运营常态化与一键发布”:阶段 7 已启动,首批目标是把阶段 6 手动 Docker 前端发布流程沉淀为 `scripts/deploy-frontend-docker.ps1`。 --- diff --git a/project-docs/阶段7-一键前端Docker发布执行记录-2026-05-01.md b/project-docs/阶段7-一键前端Docker发布执行记录-2026-05-01.md new file mode 100644 index 00000000..e46c5ad6 --- /dev/null +++ b/project-docs/阶段7-一键前端Docker发布执行记录-2026-05-01.md @@ -0,0 +1,81 @@ +# 阶段 7:一键前端 Docker 发布执行记录(2026-05-01) + +> 当前目标:把阶段 6 手动完成的前端 Docker 发布流程沉淀成 `scripts/deploy-frontend-docker.ps1`。 + +--- + +## 一、当前执行结论 + +| 主线 | 状态 | 备注 | +|---|---|---| +| gstack 安装校验 | PASS | 已返回 `GSTACK_OK` | +| 阶段 7 方案 | READY | 已新增阶段 7 方案文档 | +| 一键发布脚本 | READY | 已新增 `scripts/deploy-frontend-docker.ps1` | +| 开发期脚本验证 | PASS_WITH_EXPECTED_WARN | 已执行 `-AllowDirtyWorktree -SkipReleaseGate`,远端备份、替换、Docker build/up 成功 | +| 正式脚本发布验证 | PENDING | 提交脚本后执行干净工作区正式发布 | + +--- + +## 二、脚本能力 + +新增脚本: + +```powershell +scripts\deploy-frontend-docker.ps1 +``` + +默认发布目标: + +| 项目 | 默认值 | +|---|---| +| 服务器 | `192.168.3.90` | +| 远端项目目录 | `/opt/ai-contract-platform` | +| Compose 文件 | `docker-compose.server.yml` | +| 前端入口 | `http://192.168.3.90:3003` | +| 后端入口 | `http://192.168.3.90:8001` | +| 远端备份目录 | `/opt/ai-contract-platform/backups/phase7-deploy` | + +--- + +## 三、开发期验证结果 + +验证命令: + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\deploy-frontend-docker.ps1 -AllowDirtyWorktree -SkipReleaseGate +``` + +验证产物: + +```text +project-docs/qa-artifacts/phase7-frontend-docker-deploy/2026-05-01T11-21-50/ +``` + +结论: + +1. `gstack preflight` PASS。 +2. SSH key PASS。 +3. working tree WARN,原因为阶段 7 脚本与文档仍处于待提交状态,符合开发期验证预期。 +4. frontend archive PASS。 +5. archive upload PASS。 +6. remote Docker deploy PASS。 +7. post-deploy release gate SKIP,原因为本次仅验证发布动作,符合开发期验证参数预期。 +8. 远端 frontend 容器已重新启动,远端备份已生成:`/opt/ai-contract-platform/backups/phase7-deploy/frontend-before-2026-05-01T11-21-50.tgz`。 + +--- + +## 四、待执行验证 + +正式发布验证: + +```powershell +$env:AI_CONTRACT_ADMIN_PASSWORD = '<由管理员输入的实际密码>' +powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\deploy-frontend-docker.ps1 +Remove-Item Env:\AI_CONTRACT_ADMIN_PASSWORD +``` + +预期: + +1. 开发期验证允许当前新增脚本/文档造成的 dirty worktree。 +2. 正式发布验证必须在干净工作区执行。 +3. 正式发布验证必须通过 post-deploy release gate。 diff --git a/project-docs/阶段7-生产运营常态化与一键发布方案-2026-05-01.md b/project-docs/阶段7-生产运营常态化与一键发布方案-2026-05-01.md new file mode 100644 index 00000000..ed797432 --- /dev/null +++ b/project-docs/阶段7-生产运营常态化与一键发布方案-2026-05-01.md @@ -0,0 +1,95 @@ +# 阶段 7:生产运营常态化与一键发布方案(2026-05-01) + +> 前置状态:阶段 1-6 已完成业务闭环、生产运行治理、前端视觉刷新、Docker 部署和发布后 release gate。阶段 7 的目标是把“人工可执行的发布动作”沉淀成“可复跑、可审计、可门禁的一键流程”。 + +--- + +## 一、阶段目标 + +阶段 7 首批目标: + +1. 将前端 Docker 发布流程脚本化。 +2. 每次发布前自动检查本地工作区和远端环境。 +3. 自动备份远端旧 frontend。 +4. 自动上传、替换、Docker build/up frontend。 +5. 发布后自动执行 release gate。 +6. 生成统一部署报告,记录本地包、远端备份、Docker 日志和 release gate 产物。 + +--- + +## 二、交付范围 + +| 主线 | 目标 | 交付物 | +|---|---|---| +| A. 一键前端发布 | 复刻并固化阶段 6 手动 Docker 发布流程 | `scripts/deploy-frontend-docker.ps1` | +| B. 发布证据 | 记录本地归档包、远端备份、Docker build/up 日志和 release gate | `project-docs/qa-artifacts/phase7-frontend-docker-deploy//` | +| C. 运维文档 | 补充阶段 7 执行记录和后续常态化方向 | 阶段 7 方案与执行记录 | + +--- + +## 三、脚本默认行为 + +`scripts/deploy-frontend-docker.ps1` 默认执行: + +1. 校验 `GSTACK_OK`。 +2. 校验 SSH key。 +3. 校验本地 Git 工作区干净。 +4. 打包本地 `frontend`,排除 `node_modules` 和 `.next`。 +5. 上传压缩包到远端 `/tmp`。 +6. 远端备份 `/opt/ai-contract-platform/frontend`。 +7. 替换远端 frontend 源码。 +8. 远端执行 `docker compose -f docker-compose.server.yml build --no-cache frontend`。 +9. 远端执行 `docker compose -f docker-compose.server.yml up -d frontend`。 +10. 发布后执行 `run-production-release-gate.ps1 -RequireCleanWorktree`。 + +--- + +## 四、使用方式 + +正式发布: + +```powershell +$env:AI_CONTRACT_ADMIN_PASSWORD = '<由管理员输入的实际密码>' +powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\deploy-frontend-docker.ps1 +Remove-Item Env:\AI_CONTRACT_ADMIN_PASSWORD +``` + +开发期验证脚本本身: + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\deploy-frontend-docker.ps1 -AllowDirtyWorktree -SkipReleaseGate +``` + +完整发布窗口: + +```powershell +$env:AI_CONTRACT_ADMIN_PASSWORD = '<由管理员输入的实际密码>' +powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\deploy-frontend-docker.ps1 -IncludeBackupDrill +Remove-Item Env:\AI_CONTRACT_ADMIN_PASSWORD +``` + +--- + +## 五、验收标准 + +阶段 7 首批闭环必须满足: + +1. 脚本可在干净工作区正式执行。 +2. 脚本可在开发期通过 `-AllowDirtyWorktree -SkipReleaseGate` 验证 Docker 发布动作。 +3. 远端旧 frontend 有备份。 +4. Docker frontend build/up 成功。 +5. 发布后 release gate PASS。 +6. 本地工作区最终干净。 +7. 方案、执行记录和脚本提交并推送到 `192.168.3.90` 仓库。 + +--- + +## 六、阶段 7 后续方向 + +一键发布脚本完成后,阶段 7 后续进入: + +1. 定时巡检和告警。 +2. 备份保留策略和清理策略。 +3. 远端部署目录 Git 化或制品化治理。 +4. 试点运营观察指标。 +5. 发布后人工验收截图归档。 diff --git a/scripts/deploy-frontend-docker.ps1 b/scripts/deploy-frontend-docker.ps1 new file mode 100644 index 00000000..557d15d9 --- /dev/null +++ b/scripts/deploy-frontend-docker.ps1 @@ -0,0 +1,415 @@ +param( + [string]$ServerHost = "192.168.3.90", + [string]$SshUser = "root", + [string]$SshKeyPath = "$HOME\.ssh\water_audit_ed25519", + [string]$RemoteProjectPath = "/opt/ai-contract-platform", + [string]$ComposeFile = "docker-compose.server.yml", + [string]$RemoteBackupRoot = "/opt/ai-contract-platform/backups/phase7-deploy", + [string]$FrontendBaseUrl = "http://192.168.3.90:3003", + [string]$BackendBaseUrl = "http://192.168.3.90:8001", + [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]$AllowDirtyWorktree, + [switch]$SkipReleaseGate, + [switch]$IncludeBackupDrill, + [switch]$KeepRemoteArchive +) + +$ErrorActionPreference = "Stop" + +$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") +$timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss" +$archiveName = "ai-contract-frontend-$timestamp.tgz" +if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) { + $ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase7-frontend-docker-deploy" +} +$artifactDir = Join-Path $ArtifactRoot $timestamp +$summaryJson = Join-Path $artifactDir "frontend-docker-deploy-summary.json" +$summaryMd = Join-Path $artifactDir "frontend-docker-deploy-summary.md" +$gitStatusPath = Join-Path $artifactDir "git-status.txt" +$localArchive = Join-Path $artifactDir $archiveName +$remoteArchive = "/tmp/$archiveName" + +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-LocalStep { + param( + [string]$Name, + [scriptblock]$Script + ) + + $logPath = Join-Path $artifactDir "$Name.log" + $previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = "Continue" + try { + $output = & $Script 2>&1 + $exitCode = if ($LASTEXITCODE -is [int]) { $LASTEXITCODE } else { 0 } + } catch { + $output = @($_.Exception.Message) + $exitCode = 1 + } finally { + $ErrorActionPreference = $previousErrorActionPreference + } + ($output -join [Environment]::NewLine) | Set-Content -Encoding UTF8 -Path $logPath + return [ordered]@{ + exit_code = $exitCode + log = $logPath + output = ($output -join [Environment]::NewLine) + } +} + +function Invoke-Remote { + param( + [string]$Name, + [string]$Command + ) + + $target = "${SshUser}@${ServerHost}" + $logPath = Join-Path $artifactDir "$Name.log" + $args = @( + "-i", $SshKeyPath, + "-o", "BatchMode=yes", + "-o", "StrictHostKeyChecking=accept-new", + "-o", "ConnectTimeout=10", + $target, + $Command + ) + $previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = "Continue" + try { + $output = & ssh @args 2>&1 + $exitCode = $LASTEXITCODE + } finally { + $ErrorActionPreference = $previousErrorActionPreference + } + $outputText = ($output -join [Environment]::NewLine) + $outputText | Set-Content -Encoding UTF8 -Path $logPath + return [ordered]@{ + exit_code = $exitCode + output = $outputText + log = $logPath + } +} + +function Invoke-ScpToRemote { + param( + [string]$LocalPath, + [string]$RemotePath + ) + + $target = "${SshUser}@${ServerHost}:$RemotePath" + $logPath = Join-Path $artifactDir "upload-frontend-archive.log" + $args = @( + "-i", $SshKeyPath, + "-o", "BatchMode=yes", + "-o", "StrictHostKeyChecking=accept-new", + "-o", "ConnectTimeout=10", + $LocalPath, + $target + ) + $previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = "Continue" + try { + $output = & scp @args 2>&1 + $exitCode = $LASTEXITCODE + } finally { + $ErrorActionPreference = $previousErrorActionPreference + } + ($output -join [Environment]::NewLine) | Set-Content -Encoding UTF8 -Path $logPath + return [ordered]@{ + exit_code = $exitCode + log = $logPath + } +} + +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" +} + +if (-not (Test-Path $SshKeyPath)) { + Add-Check -Key "ssh_key" -Label "SSH key" -Status "fail" -Detail "SSH key not found: $SshKeyPath" +} else { + Add-Check -Key "ssh_key" -Label "SSH key" -Status "pass" -Detail "SSH key exists" +} + +$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 ($gitStatus) { + if ($AllowDirtyWorktree) { + Add-Check -Key "working_tree" -Label "working tree" -Status "warn" -Required $false -Detail "Working tree has uncommitted changes; deployment allowed by -AllowDirtyWorktree" -Meta @{ artifact = $gitStatusPath } + } else { + Add-Check -Key "working_tree" -Label "working tree" -Status "fail" -Detail "Working tree has uncommitted changes; pass -AllowDirtyWorktree for a development-only deploy" -Meta @{ artifact = $gitStatusPath } + } +} else { + Add-Check -Key "working_tree" -Label "working tree" -Status "pass" -Detail "Working tree is clean" -Meta @{ artifact = $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 + } +} + +$failedPreflight = @($checks | Where-Object { $_.required -and $_.status -eq "fail" }) +if ($failedPreflight.Count -eq 0) { + $archive = Invoke-LocalStep -Name "01-create-frontend-archive" -Script { + Push-Location $repoRoot + try { + & tar -czf $localArchive --exclude=frontend/node_modules --exclude=frontend/.next frontend + if ($LASTEXITCODE -eq 0 -and (Test-Path $localArchive)) { + Get-Item $localArchive | Select-Object FullName, Length + } + } finally { + Pop-Location + } + } + if ($archive.exit_code -eq 0 -and (Test-Path $localArchive)) { + Add-Check -Key "archive" -Label "frontend archive" -Status "pass" -Detail "Frontend archive created" -Meta @{ + archive = $localArchive + bytes = (Get-Item $localArchive).Length + log = $archive.log + } + } else { + Add-Check -Key "archive" -Label "frontend archive" -Status "fail" -Detail "Frontend archive creation failed" -Meta @{ log = $archive.log } + } +} + +$failedBeforeUpload = @($checks | Where-Object { $_.required -and $_.status -eq "fail" }) +if ($failedBeforeUpload.Count -eq 0) { + $upload = Invoke-ScpToRemote -LocalPath $localArchive -RemotePath $remoteArchive + if ($upload.exit_code -eq 0) { + Add-Check -Key "upload" -Label "upload archive" -Status "pass" -Detail "Frontend archive uploaded to remote host" -Meta @{ + remote_archive = $remoteArchive + log = $upload.log + } + } else { + Add-Check -Key "upload" -Label "upload archive" -Status "fail" -Detail "Frontend archive upload failed" -Meta @{ + remote_archive = $remoteArchive + log = $upload.log + } + } +} + +$remoteBackupPath = "$RemoteBackupRoot/frontend-before-$timestamp.tgz" +$failedBeforeRemote = @($checks | Where-Object { $_.required -and $_.status -eq "fail" }) +if ($failedBeforeRemote.Count -eq 0) { + $keepArchiveFlag = if ($KeepRemoteArchive) { "true" } else { "false" } + $remoteScript = @" +set -euo pipefail +project='$RemoteProjectPath' +compose='$ComposeFile' +archive='$remoteArchive' +backup_root='$RemoteBackupRoot' +stamp='$timestamp' +keep_archive='$keepArchiveFlag' +cd "`$project" +test "`$PWD" = "`$project" +test -f "`$compose" +test -f ".env" +test -d "frontend" +test -s "`$archive" +mkdir -p "`$backup_root" +backup="`$backup_root/frontend-before-`$stamp.tgz" +tar -czf "`$backup" frontend +test -s "`$backup" +rm -rf frontend +tar -xzf "`$archive" +test -f frontend/package.json +docker compose -f "`$compose" build --no-cache frontend +docker compose -f "`$compose" up -d frontend +docker compose -f "`$compose" ps backend frontend +if [ "`$keep_archive" != "true" ]; then rm -f "`$archive"; fi +echo "REMOTE_FRONTEND_BACKUP=`$backup" +"@ -replace "`r?`n", "; " + $remoteDeploy = Invoke-Remote -Name "02-remote-frontend-docker-deploy" -Command $remoteScript + if ($remoteDeploy.exit_code -eq 0) { + Add-Check -Key "remote_deploy" -Label "remote Docker deploy" -Status "pass" -Detail "Remote frontend backup, replacement, build and restart completed" -Meta @{ + remote_backup = $remoteBackupPath + log = $remoteDeploy.log + } + } else { + Add-Check -Key "remote_deploy" -Label "remote Docker deploy" -Status "fail" -Detail "Remote frontend Docker deploy failed" -Meta @{ + remote_backup = $remoteBackupPath + log = $remoteDeploy.log + } + } +} + +$failedBeforeGate = @($checks | Where-Object { $_.required -and $_.status -eq "fail" }) +if ($SkipReleaseGate) { + Add-Check -Key "release_gate" -Label "post-deploy release gate" -Status "skip" -Required $false -Detail "Skipped by -SkipReleaseGate" +} elseif ($failedBeforeGate.Count -eq 0) { + if ([string]::IsNullOrWhiteSpace($env:AI_CONTRACT_ADMIN_PASSWORD)) { + Add-Check -Key "release_gate" -Label "post-deploy release gate" -Status "fail" -Detail "AI_CONTRACT_ADMIN_PASSWORD must be set in the current process" + } else { + $gateArgs = @( + "-ServerHost", $ServerHost, + "-FrontendBaseUrl", $FrontendBaseUrl, + "-BackendBaseUrl", $BackendBaseUrl, + "-ApiUrl", $ApiUrl, + "-HealthUrl", $HealthUrl, + "-ReadinessUrl", $ReadinessUrl, + "-ArtifactRoot", (Join-Path $artifactDir "release-gate") + ) + if (-not $AllowDirtyWorktree) { + $gateArgs += "-RequireCleanWorktree" + } + if ($IncludeBackupDrill) { + $gateArgs += "-IncludeBackupDrill" + } + $gateScript = Join-Path $repoRoot "scripts\run-production-release-gate.ps1" + $gateLog = Join-Path $artifactDir "03-post-deploy-release-gate.log" + $previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = "Continue" + try { + $gateOutput = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $gateScript @gateArgs 2>&1 + $gateExit = $LASTEXITCODE + } finally { + $ErrorActionPreference = $previousErrorActionPreference + } + $gateText = ($gateOutput -join [Environment]::NewLine) + $gateText | Set-Content -Encoding UTF8 -Path $gateLog + $gateArtifact = $null + $gateMatch = [regex]::Match($gateText, '([A-Z]:\\[^\r\n]+)$', [System.Text.RegularExpressions.RegexOptions]::Multiline) + if ($gateMatch.Success) { + $gateArtifact = $gateMatch.Groups[1].Value.Trim() + } + if ($gateExit -eq 0 -and $gateText -match "_WARN") { + Add-Check -Key "release_gate" -Label "post-deploy release gate" -Status "warn" -Required $false -Detail "Release gate completed with warnings" -Meta @{ + artifact_dir = $gateArtifact + log = $gateLog + } + } elseif ($gateExit -eq 0) { + Add-Check -Key "release_gate" -Label "post-deploy release gate" -Status "pass" -Detail "Release gate passed" -Meta @{ + artifact_dir = $gateArtifact + log = $gateLog + } + } else { + Add-Check -Key "release_gate" -Label "post-deploy release gate" -Status "fail" -Detail "Release gate failed" -Meta @{ + artifact_dir = $gateArtifact + log = $gateLog + } + } + } +} + +$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 = "phase7-frontend-docker-deploy" + status = $overallStatus + generated_at = (Get-Date).ToUniversalTime().ToString("o") + artifact_dir = $artifactDir + branch = $branch + commit = $commit + remote = $remoteUrl + local_archive = $localArchive + remote_archive = $remoteArchive + remote_backup = $remoteBackupPath + targets = [ordered]@{ + server_host = $ServerHost + remote_project_path = $RemoteProjectPath + compose_file = $ComposeFile + frontend_base_url = $FrontendBaseUrl + backend_base_url = $BackendBaseUrl + } + options = [ordered]@{ + allow_dirty_worktree = [bool]$AllowDirtyWorktree + skip_release_gate = [bool]$SkipReleaseGate + include_backup_drill = [bool]$IncludeBackupDrill + keep_remote_archive = [bool]$KeepRemoteArchive + } + 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 7 Frontend Docker Deploy") | 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("- Remote backup: $remoteBackupPath") | 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("Artifacts:") | Out-Null +$markdown.Add("") | Out-Null +$markdown.Add(('- Local archive: `{0}`' -f $localArchive)) | Out-Null +$markdown.Add('- Summary JSON: `frontend-docker-deploy-summary.json`') | Out-Null +$markdown.Add('- Summary Markdown: `frontend-docker-deploy-summary.md`') | Out-Null +foreach ($check in $checks) { + if ($check.meta.log) { + $markdown.Add(('- {0} log: `{1}`' -f $check.label, $check.meta.log)) | Out-Null + } + if ($check.meta.artifact_dir) { + $markdown.Add(('- {0} artifact: `{1}`' -f $check.label, $check.meta.artifact_dir)) | Out-Null + } +} +$markdown | Set-Content -Encoding UTF8 -Path $summaryMd + +Write-Host "PHASE7_FRONTEND_DOCKER_DEPLOY_$overallStatus $artifactDir" +if ($overallStatus -eq "FAIL") { + throw "Phase 7 frontend Docker deploy failed. See $summaryMd" +}