chore: add frontend docker deploy automation

This commit is contained in:
Codex
2026-05-01 11:25:53 +08:00
parent 229e2e5127
commit 70d83f3317
5 changed files with 603 additions and 1 deletions
+10
View File
@@ -208,6 +208,16 @@ project-docs/qa-artifacts/phase5-production-regression/<timestamp>/
## 七、发布门禁口径
一键前端 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
@@ -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`。
---
@@ -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。
@@ -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/<timestamp>/` |
| 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. 发布后人工验收截图归档。
+415
View File
@@ -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"
}