feat(deploy): 为更新组件增加定时维护与双保险
Test deployment public URL (Linux) / test-linux (push) Canceled after 0s
Test start.ps1 (Windows) / test-windows (push) Canceled after 0s

This commit is contained in:
2026-10-08 09:23:25 +08:00
parent e3d760c5ab
commit 89a4ccd7e7
30 changed files with 796 additions and 7 deletions
+74
View File
@@ -0,0 +1,74 @@
param([string]$ProjectRoot = (Split-Path $PSScriptRoot -Parent))
$ErrorActionPreference = 'Stop'
Set-Location $ProjectRoot
. (Join-Path $ProjectRoot 'scripts/Initialize-SecurityEnv.ps1')
$content = [System.IO.File]::ReadAllText((Join-Path $ProjectRoot '.env'))
if ((Get-EnvValue $content 'INFRASTRUCTURE_AUTO_UPDATE_ENABLED') -eq 'false') { return }
$state = Join-Path $ProjectRoot 'data/infrastructure-update'
[System.IO.Directory]::CreateDirectory($state) | Out-Null
try { $lock = [System.IO.File]::Open((Join-Path $state 'task.lock'), 'OpenOrCreate', 'ReadWrite', 'None') }
catch [System.IO.IOException] { return }
$log = Join-Path $state 'last-run.log'
$previousConfig = $env:DOCKER_CONFIG
try {
[System.IO.File]::WriteAllText($log, "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') 检查 Watchtower 和 updater 更新`n")
$configFile = Get-EnvValue $content 'DOCKER_CONFIG_FILE'
if (-not [string]::IsNullOrWhiteSpace($configFile) -and (Test-Path $configFile -PathType Leaf)) {
$env:DOCKER_CONFIG = Split-Path $configFile -Parent
}
& docker compose version *> $null
$v2 = $LASTEXITCODE -eq 0
function Invoke-InfrastructureCompose {
param([string[]]$ComposeArguments)
if ($v2) { $output = & docker compose @ComposeArguments 2>&1 }
else { $output = & docker-compose @ComposeArguments 2>&1 }
$code = $LASTEXITCODE
$output | Out-File $log -Append -Encoding utf8
if ($code -ne 0) { throw '组件更新命令未成功,现有容器状态需查看日志' }
}
if ($v2) { $available = @(& docker compose config --services) }
else { $available = @(& docker-compose config --services) }
if ($LASTEXITCODE -ne 0 -or $available -notcontains 'watchtower') { throw '无法读取 Watchtower 部署配置' }
$failed = @()
foreach ($service in @('watchtower', 'easyai-updater')) {
# Do not enable updater in profile-disabled legacy deployments.
if ($available -notcontains $service) {
"$service 未启用,跳过维护" | Out-File $log -Append -Encoding utf8
continue
}
try {
"检查 $service" | Out-File $log -Append -Encoding utf8
Invoke-InfrastructureCompose -ComposeArguments @('pull', $service)
Invoke-InfrastructureCompose -ComposeArguments @('up', '-d', '--no-deps', $service)
$healthy = $false
for ($attempt=0; $attempt -lt 12; $attempt++) {
if ($v2) { $containerId = & docker compose ps -q $service }
else { $containerId = & docker-compose ps -q $service }
if ($LASTEXITCODE -ne 0) { throw "无法读取 $service 状态" }
if ($containerId) {
$status = & docker inspect -f '{{.State.Running}} {{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' $containerId
if ($LASTEXITCODE -ne 0) { throw "无法读取 $service 状态" }
if ($status -eq 'true healthy') { $healthy = $true; break }
if ($status -eq 'true none') {
if ($service -eq 'watchtower') {
& docker exec $containerId /watchtower --health-check *> $null
} else {
& docker exec $containerId node -e "require('http').get('http://127.0.0.1:3015/health',r=>{r.resume();process.exit(r.statusCode===200?0:1)}).on('error',()=>process.exit(1))" *> $null
}
if ($LASTEXITCODE -eq 0) { $healthy = $true; break }
}
}
Start-Sleep -Seconds 5
}
if (-not $healthy) { throw "$service 暂未恢复可用" }
"$service 已恢复可用" | Out-File $log -Append -Encoding utf8
} catch {
"$service 本次维护未成功,下次定时任务将继续检查:$($_.Exception.Message)" | Out-File $log -Append -Encoding utf8
$failed += $service
}
}
if ($failed.Count -gt 0) { throw "更新组件维护未全部成功:$($failed -join ', ')" }
} finally {
$env:DOCKER_CONFIG = $previousConfig
$lock.Dispose()
}