All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 4m7s
AWOOOI Harbor 110 Local Repair / workflow-shape (push) Successful in 0s
AWOOOI Harbor 110 Local Repair / harbor-110-local-repair (push) Successful in 1m21s
CD Pipeline / build-and-deploy (push) Successful in 4m46s
CD Pipeline / post-deploy-checks (push) Successful in 2m39s
282 lines
12 KiB
PowerShell
282 lines
12 KiB
PowerShell
param(
|
|
[string]$SourceRoot = (Split-Path -Parent $MyInvocation.MyCommand.Path),
|
|
[string]$AgentRoot = "C:\Wooo\Agent99",
|
|
[string]$SourceRevision = "",
|
|
[switch]$RegisterTasks,
|
|
[switch]$ValidateOnly
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
|
$BinDir = Join-Path $AgentRoot "bin"
|
|
$ConfigPath = Join-Path $AgentRoot "config\agent99.config.json"
|
|
$EvidenceDir = Join-Path $AgentRoot "evidence"
|
|
$StateDir = Join-Path $AgentRoot "state"
|
|
$DeployRoot = Join-Path $AgentRoot "deploy"
|
|
$StageDir = Join-Path $DeployRoot "staging-$stamp"
|
|
$BackupDir = Join-Path $DeployRoot "backup-$stamp"
|
|
$ManifestPath = Join-Path $StateDir "runtime-manifest.json"
|
|
$EvidencePath = Join-Path $EvidenceDir "agent99-deploy-$stamp.json"
|
|
$script:ConfigMigrations = @()
|
|
|
|
$runtimeFiles = @(
|
|
"agent99-bootstrap.ps1",
|
|
"agent99-contract-check.ps1",
|
|
"agent99-control-plane.ps1",
|
|
"agent99-deploy.ps1",
|
|
"agent99-register-tasks.ps1",
|
|
"agent99-sre-alert-inbox.ps1",
|
|
"agent99-sre-alert-relay.ps1",
|
|
"agent99-submit-request.ps1",
|
|
"agent99-synthetic-tests.ps1",
|
|
"agent99-telegram-inbox.ps1",
|
|
"agent99-windows-update-policy.ps1",
|
|
"host-reboot-scorecard.ps1",
|
|
"agent99.config.example.json",
|
|
"agent99.config.99.example.json"
|
|
)
|
|
|
|
function Ensure-Utf8Bom {
|
|
param([string]$Path)
|
|
$bytes = [IO.File]::ReadAllBytes($Path)
|
|
$text = [Text.Encoding]::UTF8.GetString($bytes)
|
|
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
|
[IO.File]::WriteAllText($Path, $text, (New-Object System.Text.UTF8Encoding($true)))
|
|
}
|
|
|
|
function Add-DefaultProperty {
|
|
param([object]$Object, [string]$Name, [object]$Value)
|
|
if (-not $Object.PSObject.Properties[$Name] -or $null -eq $Object.PSObject.Properties[$Name].Value) {
|
|
if ($Object.PSObject.Properties[$Name]) {
|
|
$Object.PSObject.Properties[$Name].Value = $Value
|
|
} else {
|
|
$Object | Add-Member -MemberType NoteProperty -Name $Name -Value $Value
|
|
}
|
|
}
|
|
}
|
|
|
|
function Set-AgentProperty {
|
|
param([object]$Object, [string]$Name, [object]$Value)
|
|
if ($Object.PSObject.Properties[$Name]) {
|
|
$Object.PSObject.Properties[$Name].Value = $Value
|
|
} else {
|
|
$Object | Add-Member -MemberType NoteProperty -Name $Name -Value $Value
|
|
}
|
|
}
|
|
|
|
function Write-DeployEvidence {
|
|
param([bool]$Ok, [string]$Status, [string]$ErrorText, [object[]]$Files, [object]$TaskResult)
|
|
New-Item -ItemType Directory -Force -Path $EvidenceDir | Out-Null
|
|
[pscustomobject]@{
|
|
schemaVersion = "agent99_deploy_receipt_v2"
|
|
timestamp = (Get-Date -Format o)
|
|
ok = $Ok
|
|
status = $Status
|
|
sourceRoot = $SourceRoot
|
|
sourceRevision = $SourceRevision
|
|
agentRoot = $AgentRoot
|
|
stageDir = $StageDir
|
|
backupDir = $BackupDir
|
|
runtimeManifest = $ManifestPath
|
|
validateOnly = [bool]$ValidateOnly
|
|
registerTasks = [bool]$RegisterTasks
|
|
error = $ErrorText
|
|
configMigrations = @($script:ConfigMigrations)
|
|
files = $Files
|
|
taskRegistration = $TaskResult
|
|
} | ConvertTo-Json -Depth 10 | Set-Content -Path $EvidencePath -Encoding UTF8
|
|
}
|
|
|
|
function Restore-Agent99Deployment {
|
|
param([object[]]$Rows, [bool]$ConfigBackedUp, [bool]$ManifestBackedUp)
|
|
foreach ($item in $Rows) {
|
|
$backupPath = Join-Path $BackupDir $item.name
|
|
if ($item.existed -and (Test-Path $backupPath)) {
|
|
Copy-Item -Force $backupPath $item.livePath
|
|
} elseif (-not $item.existed -and (Test-Path $item.livePath)) {
|
|
Remove-Item -Force $item.livePath
|
|
}
|
|
}
|
|
if ($ConfigBackedUp) { Copy-Item -Force (Join-Path $BackupDir "agent99.config.json") $ConfigPath }
|
|
if ($ManifestBackedUp) {
|
|
Copy-Item -Force (Join-Path $BackupDir "runtime-manifest.json") $ManifestPath
|
|
} elseif (Test-Path $ManifestPath) {
|
|
Remove-Item -Force $ManifestPath
|
|
}
|
|
}
|
|
|
|
if (-not $SourceRevision) {
|
|
try {
|
|
$revision = & git -C $SourceRoot rev-parse HEAD 2>$null
|
|
if ($LASTEXITCODE -eq 0 -and $revision) { $SourceRevision = ([string]$revision).Trim() }
|
|
} catch {
|
|
$SourceRevision = ""
|
|
}
|
|
}
|
|
if (-not $SourceRevision) { $SourceRevision = "unknown" }
|
|
|
|
New-Item -ItemType Directory -Force -Path $StageDir, $EvidenceDir, $StateDir | Out-Null
|
|
foreach ($name in $runtimeFiles) {
|
|
$source = Join-Path $SourceRoot $name
|
|
if (-not (Test-Path $source)) { throw "Required deployment file missing: $source" }
|
|
$target = Join-Path $StageDir $name
|
|
Copy-Item -Force $source $target
|
|
if ($name.EndsWith(".ps1")) { Ensure-Utf8Bom $target }
|
|
}
|
|
|
|
& (Join-Path $StageDir "agent99-contract-check.ps1") -SourceRoot $StageDir | Out-Null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-DeployEvidence $false "preflight_failed" "contract_check_failed" @() $null
|
|
throw "Agent99 staged contract check failed."
|
|
}
|
|
& (Join-Path $StageDir "agent99-synthetic-tests.ps1") -SourceRoot $StageDir -WorkRoot (Join-Path $StageDir "synthetic-work") | Out-Null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-DeployEvidence $false "preflight_failed" "synthetic_tests_failed" @() $null
|
|
throw "Agent99 staged synthetic tests failed."
|
|
}
|
|
|
|
$stagedFiles = @($runtimeFiles | ForEach-Object {
|
|
$path = Join-Path $StageDir $_
|
|
[pscustomobject]@{ name = $_; sha256 = (Get-FileHash -Algorithm SHA256 -Path $path).Hash.ToLowerInvariant() }
|
|
})
|
|
|
|
if ($ValidateOnly) {
|
|
Write-DeployEvidence $true "validated" $null $stagedFiles $null
|
|
[pscustomobject]@{ ok = $true; status = "validated"; sourceRevision = $SourceRevision; evidence = $EvidencePath; files = $stagedFiles } | ConvertTo-Json -Depth 8
|
|
exit 0
|
|
}
|
|
|
|
New-Item -ItemType Directory -Force -Path $BinDir, $BackupDir | Out-Null
|
|
$promotionRows = @()
|
|
$configBackedUp = $false
|
|
$manifestBackedUp = $false
|
|
try {
|
|
foreach ($name in $runtimeFiles) {
|
|
$livePath = Join-Path $BinDir $name
|
|
$existed = Test-Path $livePath
|
|
if ($existed) { Copy-Item -Force $livePath (Join-Path $BackupDir $name) }
|
|
$promotionRows += [pscustomobject]@{ name = $name; livePath = $livePath; existed = [bool]$existed }
|
|
Copy-Item -Force (Join-Path $StageDir $name) $livePath
|
|
}
|
|
|
|
if (-not (Test-Path $ConfigPath)) { throw "Live Agent99 config not found: $ConfigPath" }
|
|
Copy-Item -Force $ConfigPath (Join-Path $BackupDir "agent99.config.json")
|
|
$configBackedUp = $true
|
|
if (Test-Path $ManifestPath) {
|
|
Copy-Item -Force $ManifestPath (Join-Path $BackupDir "runtime-manifest.json")
|
|
$manifestBackedUp = $true
|
|
}
|
|
|
|
$config = Get-Content $ConfigPath -Raw | ConvertFrom-Json
|
|
Add-DefaultProperty $config "sshIdentityFile" (Join-Path $AgentRoot "keys\agent99_ed25519")
|
|
Add-DefaultProperty $config "sshTransport" ([pscustomobject]@{})
|
|
Add-DefaultProperty $config.sshTransport "serialized" $true
|
|
Add-DefaultProperty $config.sshTransport "lockTimeoutSeconds" 90
|
|
Add-DefaultProperty $config.sshTransport "lockPollMilliseconds" 200
|
|
Add-DefaultProperty $config "sshProcessGuard" ([pscustomobject]@{})
|
|
Add-DefaultProperty $config.sshProcessGuard "enabled" $true
|
|
Add-DefaultProperty $config.sshProcessGuard "staleMinutes" 15
|
|
Add-DefaultProperty $config.sshProcessGuard "hardStaleMinutes" 60
|
|
Add-DefaultProperty $config "k3s" ([pscustomobject]@{})
|
|
Add-DefaultProperty $config.k3s "jumpHost" "192.168.0.110"
|
|
Add-DefaultProperty $config.k3s "preferJumpHost" $true
|
|
Add-DefaultProperty $config.k3s "directHosts" @("192.168.0.110", "192.168.0.112")
|
|
Add-DefaultProperty $config "autoRecovery" ([pscustomobject]@{})
|
|
Add-DefaultProperty $config.autoRecovery "enabled" $true
|
|
Add-DefaultProperty $config.autoRecovery "triggerOnHostUnreachable" $true
|
|
Add-DefaultProperty $config.autoRecovery "minimumUnreachableHosts" 1
|
|
Add-DefaultProperty $config.autoRecovery "cooldownMinutes" 10
|
|
Add-DefaultProperty $config "recoverySlo" ([pscustomobject]@{})
|
|
Add-DefaultProperty $config.recoverySlo "targetMinutes" 10
|
|
Add-DefaultProperty $config.recoverySlo "hostTimeoutSeconds" 90
|
|
Add-DefaultProperty $config.recoverySlo "harborTimeoutSeconds" 120
|
|
Add-DefaultProperty $config.recoverySlo "workloadTimeoutSeconds" 180
|
|
Add-DefaultProperty $config "telegram" ([pscustomobject]@{})
|
|
Add-DefaultProperty $config.telegram "repeatAfterMinutes" 120
|
|
Add-DefaultProperty $config "performance" ([pscustomobject]@{})
|
|
Add-DefaultProperty $config.performance "processCpuHostPercentWarning" 25
|
|
Add-DefaultProperty $config.performance "processCpuHostPercentCritical" 50
|
|
|
|
$policyConfig = Get-Content (Join-Path $StageDir "agent99.config.99.example.json") -Raw | ConvertFrom-Json
|
|
foreach ($sourceAction in @($policyConfig.performance.loadShedding.actions | Where-Object { $_.PSObject.Properties["policyVersion"] })) {
|
|
$liveAction = $config.performance.loadShedding.actions | Where-Object { $_.name -eq $sourceAction.name } | Select-Object -First 1
|
|
if (-not $liveAction) { continue }
|
|
$sourceVersion = [int]$sourceAction.policyVersion
|
|
$liveVersion = if ($liveAction.PSObject.Properties["policyVersion"]) { [int]$liveAction.policyVersion } else { 0 }
|
|
if ($sourceVersion -le $liveVersion) { continue }
|
|
foreach ($propertyName in @("policyVersion", "host", "command", "timeoutSeconds", "postVerifyCommand", "postVerifyMaxDiskUsedPercent", "postVerifyOkRegex", "rollback", "cooldownMinutes", "failureBackoffMinutes", "minSeverity", "reasonMatches", "enabled")) {
|
|
if ($sourceAction.PSObject.Properties[$propertyName]) {
|
|
Set-AgentProperty $liveAction $propertyName $sourceAction.PSObject.Properties[$propertyName].Value
|
|
}
|
|
}
|
|
$script:ConfigMigrations += [pscustomobject]@{
|
|
name = [string]$sourceAction.name
|
|
fromPolicyVersion = $liveVersion
|
|
toPolicyVersion = $sourceVersion
|
|
}
|
|
}
|
|
$config | ConvertTo-Json -Depth 20 | Set-Content -Path $ConfigPath -Encoding UTF8
|
|
|
|
$manifestRows = @()
|
|
foreach ($staged in $stagedFiles) {
|
|
$livePath = Join-Path $BinDir $staged.name
|
|
$runtimeSha = if (Test-Path $livePath) { (Get-FileHash -Algorithm SHA256 -Path $livePath).Hash.ToLowerInvariant() } else { "" }
|
|
$matched = [bool]($runtimeSha -and $runtimeSha -eq $staged.sha256)
|
|
$manifestRows += [pscustomobject]@{
|
|
name = $staged.name
|
|
sourceExists = $true
|
|
runtimeExists = [bool](Test-Path $livePath)
|
|
sourceSha256 = $staged.sha256
|
|
runtimeSha256 = $runtimeSha
|
|
contentMatched = $matched
|
|
matched = $matched
|
|
}
|
|
}
|
|
$runtimeMatched = [bool](@($manifestRows | Where-Object { -not $_.matched }).Count -eq 0)
|
|
[pscustomobject]@{
|
|
schemaVersion = "agent99_runtime_manifest_v1"
|
|
generatedAt = (Get-Date -Format o)
|
|
sourceRevision = $SourceRevision
|
|
runtimeMatched = $runtimeMatched
|
|
fileCount = $manifestRows.Count
|
|
mismatchCount = @($manifestRows | Where-Object { -not $_.matched }).Count
|
|
files = $manifestRows
|
|
} | ConvertTo-Json -Depth 10 | Set-Content -Path $ManifestPath -Encoding UTF8
|
|
if (-not $runtimeMatched) { throw "Promoted Agent99 runtime does not match staged source." }
|
|
|
|
& (Join-Path $BinDir "agent99-contract-check.ps1") -SourceRoot $BinDir | Out-Null
|
|
if ($LASTEXITCODE -ne 0) { throw "Post-promotion Agent99 contract check failed." }
|
|
& (Join-Path $BinDir "agent99-synthetic-tests.ps1") -SourceRoot $BinDir -WorkRoot (Join-Path $StageDir "live-synthetic-work") | Out-Null
|
|
if ($LASTEXITCODE -ne 0) { throw "Post-promotion Agent99 synthetic tests failed." }
|
|
} catch {
|
|
Restore-Agent99Deployment $promotionRows $configBackedUp $manifestBackedUp
|
|
Write-DeployEvidence $false "rolled_back" $_.Exception.Message $stagedFiles $null
|
|
throw
|
|
}
|
|
|
|
$taskResult = $null
|
|
if ($RegisterTasks) {
|
|
try {
|
|
& (Join-Path $BinDir "agent99-register-tasks.ps1") -AgentRoot $AgentRoot | Out-Null
|
|
$taskResult = [pscustomobject]@{ ok = $true; status = "registered" }
|
|
} catch {
|
|
$taskResult = [pscustomobject]@{ ok = $false; status = "registration_failed"; error = $_.Exception.Message }
|
|
}
|
|
}
|
|
|
|
$deployOk = [bool](-not $taskResult -or $taskResult.ok)
|
|
$deployStatus = if ($deployOk) { "deployed" } else { "deployed_task_registration_failed" }
|
|
Write-DeployEvidence $deployOk $deployStatus $(if ($taskResult -and -not $taskResult.ok) { $taskResult.error } else { $null }) $stagedFiles $taskResult
|
|
[pscustomobject]@{
|
|
ok = $deployOk
|
|
status = $deployStatus
|
|
sourceRevision = $SourceRevision
|
|
evidence = $EvidencePath
|
|
backupDir = $BackupDir
|
|
runtimeManifest = $ManifestPath
|
|
taskRegistration = $taskResult
|
|
files = $stagedFiles
|
|
} | ConvertTo-Json -Depth 10
|
|
if (-not $deployOk) { exit 2 }
|
|
exit 0
|