chore: add capacity governance automation

This commit is contained in:
Codex
2026-05-01 12:35:53 +08:00
parent a94fb7283b
commit a13931f720
5 changed files with 610 additions and 1 deletions
+19
View File
@@ -269,6 +269,25 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\run-production-o
当前容量基线:阶段 7.2 巡检显示服务器磁盘约 `87%-88%`,已触发 WARN。若继续增长到 `90%`,巡检门禁将转为 FAIL;应优先清理过期备份、Docker build cache 和未使用镜像。
容量治理 dry run:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\run-capacity-governance.ps1
```
容量治理正式执行:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\run-capacity-governance.ps1 -Apply
```
默认保留策略:
1. 磁盘 `80%` WARN,`90%` FAIL。
2. 发布备份保留最近 `10` 个,并且只删除超过 `14` 天的旧文件。
3. 回滚备份保留最近 `10` 个,并且只删除超过 `14` 天的旧文件。
4. Docker 清理只处理 builder cache 和 dangling images,不执行 `docker system prune -a`,不删除 volumes。
列出可回滚的 frontend 备份:
```powershell
@@ -20,7 +20,7 @@
阶段 1 已完成源码闭环与生产入口闭环,生产无人 E2E `18/18` 通过。阶段 2 已完成 Review 重试、Knowledge Base 文档重试、Review -> Chat -> Contract 上下文联动、首页交付工作台和阶段 2 自动化无人闭环,本地与生产 E2E 均 `7/7` 通过。阶段 3 已完成系统 readiness、试点交付准备页、默认账号说明、演示样本数据、依赖健康门禁和阶段 3 自动化无人闭环,本地与生产 E2E 均 `5/5` 通过。
当前后续重点已从“生产运营自动化与发布门禁固化”推进到“生产运营常态化与一键发布”:阶段 7 首批一键发布闭环已完成,阶段 7.2 一键回滚与生产巡检门禁也已完成首批闭环。当前唯一持续 WARN 是服务器磁盘使用率约 `87%-88%`,下一步应进入备份保留、历史 Docker 资源清理和容量治理。
当前后续重点已从“生产运营自动化与发布门禁固化”推进到“生产运营常态化与一键发布”:阶段 7 首批一键发布闭环已完成,阶段 7.2 一键回滚与生产巡检门禁也已完成首批闭环。当前正在进入阶段 7.3 容量治理:固化备份保留策略、Docker build cache/旧镜像清理和磁盘告警阈值。
---
@@ -0,0 +1,72 @@
# 阶段 7.3:容量治理与备份保留策略执行记录(2026-05-01)
> 当前目标:处理阶段 7.2 暴露的磁盘容量 WARN,将备份保留策略、Docker build cache/旧镜像清理和磁盘告警阈值固化为自动化流程。
---
## 一、当前执行结论
| 主线 | 状态 | 备注 |
|---|---|---|
| gstack 安装校验 | PASS | 已返回 `GSTACK_OK` |
| 阶段 7.3 方案 | READY | 已新增容量治理方案文档 |
| 容量治理脚本 | READY | 已新增 `scripts/run-capacity-governance.ps1` |
| 初始容量状态 | WARN | 远端 `/` 磁盘 `88%` |
| dry run | PASS_WITH_CAPACITY_WARN | 策略计算通过,备份无删除候选,Docker 可回收空间明显 |
| 正式清理 | PENDING | 待执行 |
| 清理后巡检 | PENDING | 待执行 |
| 清理后 release gate | PENDING | 待执行 |
---
## 二、初始现场状态
轻量盘点结果:
```text
/dev/mapper/ol-root 70G total, 62G used, 8.5G available, 88% used
```
frontend 备份包当前很小,不是主要容量压力来源:
```text
/opt/ai-contract-platform/backups/phase7-deploy/frontend-before-2026-05-01T11-21-50.tgz
/opt/ai-contract-platform/backups/phase7-deploy/frontend-before-2026-05-01T11-26-02.tgz
/opt/ai-contract-platform/backups/phase7-rollback/frontend-current-before-rollback-2026-05-01T12-11-51-035.tgz
```
`docker system df` 在服务器上会超时,因此容量治理脚本对 Docker 诊断和清理都加入超时保护。
---
## 三、dry run 验证结果
验证命令:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\run-capacity-governance.ps1 -AllowDirtyWorktree
```
验证产物:
```text
project-docs/qa-artifacts/phase7-capacity-governance/2026-05-01T12-34-41-578/
```
结论:
1. remote capacity run PASS。
2. disk threshold WARN:当前磁盘 `89%`,超过 `80%` 预警阈值,低于 `90%` 失败阈值。
3. backup retention PASS:发布备份 `2` 个、回滚备份 `1` 个,均无删除候选。
4. Docker prune SKIP:dry run 不执行清理。
5. Docker 盘点显示 images 可回收约 `9.7GB`,build cache 可回收约 `17.6GB`,容量压力主要来自 Docker 历史资源。
---
## 四、待执行验证
正式清理将在脚本提交后、干净工作区中执行:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\run-capacity-governance.ps1 -Apply
```
@@ -0,0 +1,90 @@
# 阶段 7.3:容量治理与备份保留策略方案(2026-05-01)
> 前置状态:阶段 7.2 已完成一键回滚与生产巡检门禁。生产巡检持续报告服务器磁盘 `87%-88%` WARN,因此阶段 7.3 的目标是把容量治理做成可审计、可复跑、可控清理的运维闭环。
---
## 一、阶段目标
1. 固化磁盘告警阈值:`80% WARN`,`90% FAIL`。
2. 固化 frontend 发布备份保留策略。
3. 固化 frontend 回滚前备份保留策略。
4. 清理 Docker build cache 和 dangling images。
5. 所有清理先 dry run,正式执行必须留下 JSON/Markdown 证据。
6. 清理后复跑生产巡检门禁和 release gate。
---
## 二、交付范围
| 主线 | 目标 | 交付物 |
|---|---|---|
| A. 容量治理脚本 | 统一盘点、dry run、正式清理 | `scripts/run-capacity-governance.ps1` |
| B. 保留策略 | 备份保留数量、年龄和删除边界 | 本文档、运维手册 |
| C. Docker 清理 | builder cache、dangling images | 容量治理脚本 |
| D. 验证证据 | dry run、apply、ops check、release gate | `project-docs/qa-artifacts/phase7-capacity-governance/<timestamp>/` |
---
## 三、默认策略
| 项目 | 默认值 | 说明 |
|---|---:|---|
| 磁盘 WARN | `80%` | 生产巡检和容量治理均记录 WARN |
| 磁盘 FAIL | `90%` | 达到后阻断门禁 |
| 发布备份保留数量 | `10` | 始终保留最近 10 个 |
| 发布备份最大年龄 | `14 天` | 只有超过数量且超过年龄才删除 |
| 回滚备份保留数量 | `10` | 始终保留最近 10 个 |
| 回滚备份最大年龄 | `14 天` | 只有超过数量且超过年龄才删除 |
| Docker builder prune | `24h` | 清理 24 小时以前的 builder cache |
| Docker prune 超时 | `180 秒` | 避免 Docker 诊断或清理卡死 |
---
## 四、使用方式
dry run:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\run-capacity-governance.ps1
```
正式执行:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\run-capacity-governance.ps1 -Apply
```
只执行备份保留,不清理 Docker:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\run-capacity-governance.ps1 -Apply -SkipDockerPrune
```
只清理 Docker,不删除备份:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\run-capacity-governance.ps1 -Apply -SkipBackupCleanup
```
---
## 五、安全边界
1. 默认不删除任何远端文件。
2. 备份删除只使用 `rm -f`,不使用递归删除。
3. 备份删除路径必须位于配置的备份根目录内。
4. 备份必须同时满足“超过保留数量”和“超过保留年龄”才会删除。
5. Docker 清理只执行 builder cache prune 和 dangling image prune,不执行 `docker system prune -a`,不删除 volumes。
6. 所有 Docker 命令带超时,避免容量治理脚本挂死。
---
## 六、验收标准
1. dry run 能输出当前磁盘、备份候选、Docker 诊断结果。
2. 正式执行不会删除最近备份。
3. 正式执行会尝试清理 Docker builder cache 和 dangling images。
4. 执行后生产巡检 required 项 PASS。
5. 执行后 release gate PASS。
6. 若磁盘仍高于 `80%`,必须保留 WARN 结论和下一步容量治理建议。
+428
View File
@@ -0,0 +1,428 @@
param(
[string]$ServerHost = "192.168.3.90",
[string]$SshUser = "root",
[string]$SshKeyPath = "$HOME\.ssh\water_audit_ed25519",
[string]$RemoteProjectPath = "/opt/ai-contract-platform",
[string]$RemoteDeployBackupRoot = "/opt/ai-contract-platform/backups/phase7-deploy",
[string]$RemoteRollbackBackupRoot = "/opt/ai-contract-platform/backups/phase7-rollback",
[string]$ArtifactRoot = "",
[int]$DiskWarnPercent = 80,
[int]$DiskFailPercent = 90,
[int]$DeployBackupKeep = 10,
[int]$DeployBackupMaxAgeDays = 14,
[int]$RollbackBackupKeep = 10,
[int]$RollbackBackupMaxAgeDays = 14,
[int]$DockerBuilderPruneUntilHours = 24,
[int]$DockerCommandTimeoutSeconds = 180,
[switch]$Apply,
[switch]$SkipBackupCleanup,
[switch]$SkipDockerPrune,
[switch]$AllowDirtyWorktree
)
$ErrorActionPreference = "Stop"
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
$timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss-fff"
if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) {
$ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase7-capacity-governance"
}
if (-not [System.IO.Path]::IsPathRooted($ArtifactRoot)) {
$ArtifactRoot = Join-Path $repoRoot $ArtifactRoot
}
$artifactDir = Join-Path $ArtifactRoot $timestamp
$summaryJson = Join-Path $artifactDir "capacity-governance-summary.json"
$summaryMd = Join-Path $artifactDir "capacity-governance-summary.md"
$gitStatusPath = Join-Path $artifactDir "git-status.txt"
$remoteCapacityPath = Join-Path $artifactDir "remote-capacity-output.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 Convert-KeyValueOutput {
param([string]$Text)
$data = @{}
foreach ($line in ($Text -split "`r?`n")) {
if ($line -match '^([A-Z0-9_]+)=(.*)$') {
$data[$matches[1]] = $matches[2]
}
}
return $data
}
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
}
}
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; capacity governance 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 development validation" -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) {
$applyFlag = if ($Apply) { "true" } else { "false" }
$skipBackupFlag = if ($SkipBackupCleanup) { "true" } else { "false" }
$skipDockerFlag = if ($SkipDockerPrune) { "true" } else { "false" }
$remoteScript = @"
set -euo pipefail
project='$RemoteProjectPath'
deploy_root='$RemoteDeployBackupRoot'
rollback_root='$RemoteRollbackBackupRoot'
apply='$applyFlag'
skip_backup='$skipBackupFlag'
skip_docker='$skipDockerFlag'
deploy_keep='$DeployBackupKeep'
deploy_days='$DeployBackupMaxAgeDays'
rollback_keep='$RollbackBackupKeep'
rollback_days='$RollbackBackupMaxAgeDays'
builder_until_hours='$DockerBuilderPruneUntilHours'
docker_timeout='$DockerCommandTimeoutSeconds'
emit_disk() {
label="`$1"
used="`$(df -P "`$project" | awk 'NR==2 {print `$5}' | tr -d %)"
avail="`$(df -P "`$project" | awk 'NR==2 {print `$4}')"
used_kb="`$(df -P "`$project" | awk 'NR==2 {print `$3}')"
echo "DISK_`${label}_USED_PERCENT=`$used"
echo "DISK_`${label}_AVAILABLE_KB=`$avail"
echo "DISK_`${label}_USED_KB=`$used_kb"
}
scan_backups() {
prefix="`$1"
root="`$2"
pattern="`$3"
keep="`$4"
max_age_days="`$5"
now="`$(date +%s)"
count=0
candidate_count=0
candidate_bytes=0
deleted_count=0
deleted_bytes=0
mkdir -p "`$root"
while read -r epoch_float size path; do
[ -n "`$path" ] || continue
epoch="`$(printf '%s\n' "`$epoch_float" | cut -d. -f1)"
count="`$((count + 1))"
age_days="`$(((now - epoch) / 86400))"
should_delete=false
if [ "`$count" -gt "`$keep" ] && [ "`$age_days" -gt "`$max_age_days" ]; then
should_delete=true
candidate_count="`$((candidate_count + 1))"
candidate_bytes="`$((candidate_bytes + size))"
echo "`${prefix}_DELETE_CANDIDATE=`$path|`$size|`$age_days"
fi
if [ "`$apply" = "true" ] && [ "`$skip_backup" != "true" ] && [ "`$should_delete" = "true" ]; then
case "`$path" in
"`$root"/*) rm -f -- "`$path" ;;
*) echo "`${prefix}_DELETE_REFUSED=`$path" ;;
esac
if [ ! -e "`$path" ]; then
deleted_count="`$((deleted_count + 1))"
deleted_bytes="`$((deleted_bytes + size))"
fi
fi
done < <(find "`$root" -maxdepth 1 -type f -name "`$pattern" -printf '%T@ %s %p\n' 2>/dev/null | sort -nr)
echo "`${prefix}_TOTAL_COUNT=`$count"
echo "`${prefix}_DELETE_CANDIDATE_COUNT=`$candidate_count"
echo "`${prefix}_DELETE_CANDIDATE_BYTES=`$candidate_bytes"
echo "`${prefix}_DELETED_COUNT=`$deleted_count"
echo "`${prefix}_DELETED_BYTES=`$deleted_bytes"
}
cd "`$project"
test "`$PWD" = "`$project"
echo "REMOTE_PROJECT_OK=1"
echo "APPLY_MODE=`$apply"
emit_disk BEFORE
echo "DOCKER_SYSTEM_DF_BEFORE_BEGIN"
timeout 20 docker system df || true
echo "DOCKER_SYSTEM_DF_BEFORE_END"
scan_backups DEPLOY_BACKUP "`$deploy_root" 'frontend-before-*.tgz' "`$deploy_keep" "`$deploy_days"
scan_backups ROLLBACK_BACKUP "`$rollback_root" 'frontend-current-before-rollback-*.tgz' "`$rollback_keep" "`$rollback_days"
if [ "`$apply" = "true" ] && [ "`$skip_docker" != "true" ]; then
echo "DOCKER_BUILDER_PRUNE_BEGIN"
if timeout "`$docker_timeout" docker builder prune -f --filter "until=`${builder_until_hours}h"; then
echo "DOCKER_BUILDER_PRUNE_EXIT=0"
else
echo "DOCKER_BUILDER_PRUNE_EXIT=`$?"
fi
echo "DOCKER_BUILDER_PRUNE_END"
echo "DOCKER_IMAGE_PRUNE_BEGIN"
if timeout "`$docker_timeout" docker image prune -f; then
echo "DOCKER_IMAGE_PRUNE_EXIT=0"
else
echo "DOCKER_IMAGE_PRUNE_EXIT=`$?"
fi
echo "DOCKER_IMAGE_PRUNE_END"
else
echo "DOCKER_PRUNE_SKIPPED=1"
fi
emit_disk AFTER
echo "DOCKER_SYSTEM_DF_AFTER_BEGIN"
timeout 20 docker system df || true
echo "DOCKER_SYSTEM_DF_AFTER_END"
"@
$capacityRun = Invoke-Remote -Name "01-remote-capacity-governance" -Command $remoteScript
$capacityRun.output | Set-Content -Encoding UTF8 -Path $remoteCapacityPath
$remoteData = Convert-KeyValueOutput -Text $capacityRun.output
if ($capacityRun.exit_code -eq 0 -and $remoteData.REMOTE_PROJECT_OK -eq "1") {
Add-Check -Key "remote_capacity_run" -Label "remote capacity run" -Status "pass" -Detail "Remote capacity governance script completed" -Meta @{ artifact = $remoteCapacityPath }
} else {
Add-Check -Key "remote_capacity_run" -Label "remote capacity run" -Status "fail" -Detail "Remote capacity governance script failed" -Meta @{
exit_code = $capacityRun.exit_code
artifact = $remoteCapacityPath
}
}
$beforeDisk = if ($remoteData.DISK_BEFORE_USED_PERCENT) { [int]$remoteData.DISK_BEFORE_USED_PERCENT } else { 100 }
$afterDisk = if ($remoteData.DISK_AFTER_USED_PERCENT) { [int]$remoteData.DISK_AFTER_USED_PERCENT } else { 100 }
if ($afterDisk -ge $DiskFailPercent) {
Add-Check -Key "disk_threshold" -Label "disk threshold" -Status "fail" -Detail "Disk usage is $afterDisk%, fail threshold is $DiskFailPercent%" -Meta @{
before = $beforeDisk
after = $afterDisk
artifact = $remoteCapacityPath
}
} elseif ($afterDisk -ge $DiskWarnPercent) {
Add-Check -Key "disk_threshold" -Label "disk threshold" -Status "warn" -Required $false -Detail "Disk usage is $afterDisk%, warn threshold is $DiskWarnPercent%" -Meta @{
before = $beforeDisk
after = $afterDisk
artifact = $remoteCapacityPath
}
} else {
Add-Check -Key "disk_threshold" -Label "disk threshold" -Status "pass" -Detail "Disk usage is $afterDisk%" -Meta @{
before = $beforeDisk
after = $afterDisk
artifact = $remoteCapacityPath
}
}
$deployCandidates = if ($remoteData.DEPLOY_BACKUP_DELETE_CANDIDATE_COUNT) { [int]$remoteData.DEPLOY_BACKUP_DELETE_CANDIDATE_COUNT } else { 0 }
$rollbackCandidates = if ($remoteData.ROLLBACK_BACKUP_DELETE_CANDIDATE_COUNT) { [int]$remoteData.ROLLBACK_BACKUP_DELETE_CANDIDATE_COUNT } else { 0 }
$deployDeleted = if ($remoteData.DEPLOY_BACKUP_DELETED_COUNT) { [int]$remoteData.DEPLOY_BACKUP_DELETED_COUNT } else { 0 }
$rollbackDeleted = if ($remoteData.ROLLBACK_BACKUP_DELETED_COUNT) { [int]$remoteData.ROLLBACK_BACKUP_DELETED_COUNT } else { 0 }
if ($SkipBackupCleanup) {
Add-Check -Key "backup_retention" -Label "backup retention" -Status "skip" -Required $false -Detail "Skipped by -SkipBackupCleanup"
} elseif ($Apply) {
Add-Check -Key "backup_retention" -Label "backup retention" -Status "pass" -Detail "Backup retention applied; deleted deploy=$deployDeleted rollback=$rollbackDeleted" -Meta @{
deploy_candidates = $deployCandidates
rollback_candidates = $rollbackCandidates
artifact = $remoteCapacityPath
}
} elseif (($deployCandidates + $rollbackCandidates) -gt 0) {
Add-Check -Key "backup_retention" -Label "backup retention" -Status "warn" -Required $false -Detail "Dry run found backup deletion candidates" -Meta @{
deploy_candidates = $deployCandidates
rollback_candidates = $rollbackCandidates
artifact = $remoteCapacityPath
}
} else {
Add-Check -Key "backup_retention" -Label "backup retention" -Status "pass" -Detail "No backup deletion candidates under current retention policy" -Meta @{
deploy_total = $remoteData.DEPLOY_BACKUP_TOTAL_COUNT
rollback_total = $remoteData.ROLLBACK_BACKUP_TOTAL_COUNT
artifact = $remoteCapacityPath
}
}
if ($SkipDockerPrune) {
Add-Check -Key "docker_prune" -Label "Docker prune" -Status "skip" -Required $false -Detail "Skipped by -SkipDockerPrune"
} elseif ($Apply) {
$builderExit = if ($remoteData.DOCKER_BUILDER_PRUNE_EXIT) { [int]$remoteData.DOCKER_BUILDER_PRUNE_EXIT } else { -1 }
$imageExit = if ($remoteData.DOCKER_IMAGE_PRUNE_EXIT) { [int]$remoteData.DOCKER_IMAGE_PRUNE_EXIT } else { -1 }
if ($builderExit -eq 0 -and $imageExit -eq 0) {
Add-Check -Key "docker_prune" -Label "Docker prune" -Status "pass" -Detail "Docker builder prune and dangling image prune completed" -Meta @{ artifact = $remoteCapacityPath }
} else {
Add-Check -Key "docker_prune" -Label "Docker prune" -Status "warn" -Required $false -Detail "Docker prune completed with non-zero exit or timeout" -Meta @{
builder_exit = $builderExit
image_exit = $imageExit
artifact = $remoteCapacityPath
}
}
} else {
Add-Check -Key "docker_prune" -Label "Docker prune" -Status "skip" -Required $false -Detail "Dry run only; pass -Apply to prune Docker builder cache and dangling images"
}
}
$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-capacity-governance"
status = $overallStatus
generated_at = (Get-Date).ToUniversalTime().ToString("o")
artifact_dir = $artifactDir
branch = $branch
commit = $commit
remote = $remoteUrl
targets = [ordered]@{
server_host = $ServerHost
remote_project_path = $RemoteProjectPath
deploy_backup_root = $RemoteDeployBackupRoot
rollback_backup_root = $RemoteRollbackBackupRoot
}
policy = [ordered]@{
disk_warn_percent = $DiskWarnPercent
disk_fail_percent = $DiskFailPercent
deploy_backup_keep = $DeployBackupKeep
deploy_backup_max_age_days = $DeployBackupMaxAgeDays
rollback_backup_keep = $RollbackBackupKeep
rollback_backup_max_age_days = $RollbackBackupMaxAgeDays
docker_builder_prune_until_hours = $DockerBuilderPruneUntilHours
docker_command_timeout_seconds = $DockerCommandTimeoutSeconds
}
options = [ordered]@{
apply = [bool]$Apply
skip_backup_cleanup = [bool]$SkipBackupCleanup
skip_docker_prune = [bool]$SkipDockerPrune
allow_dirty_worktree = [bool]$AllowDirtyWorktree
}
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 Capacity Governance") | 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("- Apply mode: $([bool]$Apply)") | 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("Policy:") | Out-Null
$markdown.Add("") | Out-Null
$markdown.Add("- Disk warn/fail: $DiskWarnPercent% / $DiskFailPercent%") | Out-Null
$markdown.Add("- Deploy backup retention: keep $DeployBackupKeep and only delete files older than $DeployBackupMaxAgeDays days") | Out-Null
$markdown.Add("- Rollback backup retention: keep $RollbackBackupKeep and only delete files older than $RollbackBackupMaxAgeDays days") | Out-Null
$markdown.Add("- Docker builder prune: until ${DockerBuilderPruneUntilHours}h") | Out-Null
$markdown.Add("") | Out-Null
$markdown.Add("Local artifacts:") | Out-Null
$markdown.Add("") | Out-Null
$markdown.Add('- `capacity-governance-summary.json`') | Out-Null
$markdown.Add('- `capacity-governance-summary.md`') | Out-Null
$markdown.Add('- `git-status.txt`') | Out-Null
$markdown.Add('- `remote-capacity-output.txt`') | Out-Null
$markdown.Add('- `*.log`') | Out-Null
$markdown | Set-Content -Encoding UTF8 -Path $summaryMd
Write-Host "PHASE7_CAPACITY_GOVERNANCE_$overallStatus $artifactDir"
if ($overallStatus -eq "FAIL") {
throw "Phase 7 capacity governance failed. See $summaryMd"
}