445 lines
19 KiB
PowerShell
445 lines
19 KiB
PowerShell
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]$SharedAiSourcePath = "D:\shared-ai-platform",
|
|
[string]$BackendSourcePath = "",
|
|
[string]$RemoteServicePath = "/opt/ai-contract-platform/shared-ai-platform",
|
|
[string]$ArtifactRoot = "",
|
|
[switch]$AllowDirtyWorktree
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
|
$timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss"
|
|
if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) {
|
|
$ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\shared-ai-platform-docker-deploy"
|
|
}
|
|
$artifactDir = Join-Path $ArtifactRoot $timestamp
|
|
$summaryJson = Join-Path $artifactDir "shared-ai-platform-docker-deploy-summary.json"
|
|
$summaryMd = Join-Path $artifactDir "shared-ai-platform-docker-deploy-summary.md"
|
|
$gitStatusPath = Join-Path $artifactDir "git-status.txt"
|
|
$archiveName = "shared-ai-platform-$timestamp.tgz"
|
|
$backendArchiveName = "ai-contract-backend-$timestamp.tgz"
|
|
$composeArchiveName = "docker-compose.server-$timestamp.yml"
|
|
$localArchive = Join-Path $artifactDir $archiveName
|
|
$localBackendArchive = Join-Path $artifactDir $backendArchiveName
|
|
$localComposeCopy = Join-Path $artifactDir $composeArchiveName
|
|
$remoteArchive = "/tmp/$archiveName"
|
|
$remoteBackendArchive = "/tmp/$backendArchiveName"
|
|
$remoteComposeCopy = "/tmp/$composeArchiveName"
|
|
$localRemoteEnv = Join-Path $artifactDir "shared-ai-platform.remote.env"
|
|
$remoteEnv = "/tmp/shared-ai-platform.env"
|
|
|
|
if ([string]::IsNullOrWhiteSpace($BackendSourcePath)) {
|
|
$BackendSourcePath = Join-Path $repoRoot "backend"
|
|
}
|
|
|
|
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-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
|
|
}
|
|
($output -join [Environment]::NewLine) | Set-Content -Encoding UTF8 -Path $logPath
|
|
return [ordered]@{
|
|
exit_code = $exitCode
|
|
log = $logPath
|
|
output = ($output -join [Environment]::NewLine)
|
|
}
|
|
}
|
|
|
|
function Invoke-ScpToRemote {
|
|
param(
|
|
[string]$LocalPath,
|
|
[string]$RemotePath,
|
|
[string]$LogName
|
|
)
|
|
|
|
$target = "${SshUser}@${ServerHost}:$RemotePath"
|
|
$logPath = Join-Path $artifactDir $LogName
|
|
$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
|
|
}
|
|
}
|
|
|
|
function Get-EnvMap {
|
|
param([string]$Path)
|
|
$data = @{}
|
|
foreach ($line in Get-Content $Path) {
|
|
$trimmed = $line.Trim()
|
|
if (-not $trimmed -or $trimmed.StartsWith("#")) { continue }
|
|
$parts = $trimmed -split "=", 2
|
|
if ($parts.Length -eq 2) {
|
|
$data[$parts[0].Trim()] = $parts[1]
|
|
}
|
|
}
|
|
return $data
|
|
}
|
|
|
|
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"
|
|
}
|
|
|
|
if (-not (Test-Path $SharedAiSourcePath)) {
|
|
Add-Check -Key "shared_source" -Label "shared-ai source" -Status "fail" -Detail "shared-ai-platform source path not found: $SharedAiSourcePath"
|
|
} else {
|
|
Add-Check -Key "shared_source" -Label "shared-ai source" -Status "pass" -Detail "shared-ai-platform source path exists"
|
|
}
|
|
|
|
if (-not (Test-Path $BackendSourcePath)) {
|
|
Add-Check -Key "backend_source" -Label "backend source" -Status "fail" -Detail "backend source path not found: $BackendSourcePath"
|
|
} else {
|
|
Add-Check -Key "backend_source" -Label "backend source" -Status "pass" -Detail "backend source path exists"
|
|
}
|
|
|
|
$sharedEnvPath = Join-Path $SharedAiSourcePath ".env"
|
|
if (-not (Test-Path $sharedEnvPath)) {
|
|
Add-Check -Key "shared_env" -Label "shared-ai env" -Status "fail" -Detail "shared-ai-platform .env not found: $sharedEnvPath"
|
|
} else {
|
|
$sharedEnv = Get-EnvMap -Path $sharedEnvPath
|
|
$requiredSharedVars = @("SHARED_AI_OPENAI_BASE_URL", "SHARED_AI_OPENAI_API_KEY", "SHARED_AI_OPENAI_MODEL")
|
|
$missingSharedVars = @($requiredSharedVars | Where-Object { -not $sharedEnv.ContainsKey($_) -or [string]::IsNullOrWhiteSpace($sharedEnv[$_]) })
|
|
if ($missingSharedVars.Count -gt 0) {
|
|
Add-Check -Key "shared_env" -Label "shared-ai env" -Status "fail" -Detail "Missing required shared-ai env vars: $($missingSharedVars -join ', ')"
|
|
} else {
|
|
$remoteEnvContent = @(
|
|
"APP_NAME=Shared AI Platform"
|
|
"APP_ENV=production"
|
|
"API_V1_PREFIX=/api/v0.1"
|
|
"DATABASE_URL=sqlite:////app/storage/shared_ai_platform.db"
|
|
"SHARED_AI_PLATFORM_API_KEY=$($sharedEnv['SHARED_AI_PLATFORM_API_KEY'])"
|
|
"SHARED_AI_DEFAULT_PROVIDER=openai_compatible"
|
|
"SHARED_AI_DIFY_BASE_URL="
|
|
"SHARED_AI_DIFY_API_KEY="
|
|
"SHARED_AI_DIFY_WORKFLOW_API_KEY="
|
|
"SHARED_AI_DIFY_CHAT_API_KEY="
|
|
"SHARED_AI_OPENAI_BASE_URL=$($sharedEnv['SHARED_AI_OPENAI_BASE_URL'])"
|
|
"SHARED_AI_OPENAI_API_KEY=$($sharedEnv['SHARED_AI_OPENAI_API_KEY'])"
|
|
"SHARED_AI_OPENAI_MODEL=$($sharedEnv['SHARED_AI_OPENAI_MODEL'])"
|
|
"SHARED_AI_OPENAI_TEMPERATURE=$($sharedEnv['SHARED_AI_OPENAI_TEMPERATURE'])"
|
|
"SHARED_AI_OPENAI_MAX_TOKENS=$($sharedEnv['SHARED_AI_OPENAI_MAX_TOKENS'])"
|
|
"SHARED_AI_OPENAI_TIMEOUT_SECONDS=$($sharedEnv['SHARED_AI_OPENAI_TIMEOUT_SECONDS'])"
|
|
) -join [Environment]::NewLine
|
|
$remoteEnvContent | Set-Content -Encoding UTF8 -Path $localRemoteEnv
|
|
Add-Check -Key "shared_env" -Label "shared-ai env" -Status "pass" -Detail "shared-ai-platform env loaded and remote env artifact generated" -Meta @{ artifact = $localRemoteEnv }
|
|
}
|
|
}
|
|
|
|
$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 controlled test deploy" -Meta @{ artifact = $gitStatusPath }
|
|
}
|
|
} else {
|
|
Add-Check -Key "working_tree" -Label "working tree" -Status "pass" -Detail "Working tree is clean" -Meta @{ artifact = $gitStatusPath }
|
|
}
|
|
|
|
$failedPreflight = @($checks | Where-Object { $_.required -and $_.status -eq "fail" })
|
|
if ($failedPreflight.Count -eq 0) {
|
|
Copy-Item 'D:\ai-contract-platform\docker-compose.server.yml' $localComposeCopy -Force
|
|
Push-Location (Split-Path $SharedAiSourcePath -Parent)
|
|
try {
|
|
& tar -czf $localArchive --exclude=.env --exclude=.pytest_cache --exclude=__pycache__ --exclude=storage (Split-Path $SharedAiSourcePath -Leaf)
|
|
} finally {
|
|
Pop-Location
|
|
}
|
|
Push-Location (Split-Path $BackendSourcePath -Parent)
|
|
try {
|
|
& tar -czf $localBackendArchive --exclude=.env --exclude=.venv --exclude=.pytest_cache --exclude=__pycache__ --exclude=storage (Split-Path $BackendSourcePath -Leaf)
|
|
} finally {
|
|
Pop-Location
|
|
}
|
|
if ($LASTEXITCODE -eq 0 -and (Test-Path $localArchive) -and (Test-Path $localBackendArchive)) {
|
|
Add-Check -Key "archive" -Label "deployment archives" -Status "pass" -Detail "shared-ai and backend archives created" -Meta @{ shared_archive = $localArchive; backend_archive = $localBackendArchive }
|
|
} else {
|
|
Add-Check -Key "archive" -Label "deployment archives" -Status "fail" -Detail "Archive creation failed"
|
|
}
|
|
}
|
|
|
|
$failedBeforeUpload = @($checks | Where-Object { $_.required -and $_.status -eq "fail" })
|
|
if ($failedBeforeUpload.Count -eq 0) {
|
|
$uploadArchive = Invoke-ScpToRemote -LocalPath $localArchive -RemotePath $remoteArchive -LogName "upload-shared-ai-archive.log"
|
|
$uploadBackendArchive = Invoke-ScpToRemote -LocalPath $localBackendArchive -RemotePath $remoteBackendArchive -LogName "upload-backend-archive.log"
|
|
$uploadCompose = Invoke-ScpToRemote -LocalPath $localComposeCopy -RemotePath $remoteComposeCopy -LogName "upload-shared-ai-compose.log"
|
|
$uploadEnv = Invoke-ScpToRemote -LocalPath $localRemoteEnv -RemotePath $remoteEnv -LogName "upload-shared-ai-env.log"
|
|
if ($uploadArchive.exit_code -eq 0 -and $uploadBackendArchive.exit_code -eq 0 -and $uploadCompose.exit_code -eq 0 -and $uploadEnv.exit_code -eq 0) {
|
|
Add-Check -Key "upload" -Label "upload deployment artifacts" -Status "pass" -Detail "Archive, compose and env uploaded"
|
|
} else {
|
|
Add-Check -Key "upload" -Label "upload deployment artifacts" -Status "fail" -Detail "Artifact upload failed"
|
|
}
|
|
}
|
|
|
|
$failedBeforeRemote = @($checks | Where-Object { $_.required -and $_.status -eq "fail" })
|
|
if ($failedBeforeRemote.Count -eq 0) {
|
|
$remoteScript = @"
|
|
set -euo pipefail
|
|
project='$RemoteProjectPath'
|
|
service_dir='$RemoteServicePath'
|
|
archive='$remoteArchive'
|
|
backend_archive='$remoteBackendArchive'
|
|
compose_tmp='$remoteComposeCopy'
|
|
remote_env='$remoteEnv'
|
|
compose='$ComposeFile'
|
|
stamp='$timestamp'
|
|
cd "`$project"
|
|
test -f "`$archive"
|
|
test -f "`$backend_archive"
|
|
test -f "`$compose_tmp"
|
|
test -f "`$remote_env"
|
|
mkdir -p "`$service_dir"
|
|
backup_root="`$project/backups/shared-ai-platform"
|
|
mkdir -p "`$backup_root"
|
|
if [ -d "`$service_dir" ]; then
|
|
tar -czf "`$backup_root/shared-ai-platform-before-`$stamp.tgz" -C "`$project" shared-ai-platform || true
|
|
fi
|
|
rm -rf "`$service_dir"
|
|
mkdir -p "`$service_dir"
|
|
tar -xzf "`$archive" --strip-components=1 -C "`$service_dir"
|
|
mv "`$remote_env" "`$service_dir/.env"
|
|
chmod 600 "`$service_dir/.env"
|
|
backend_backup_root="`$project/backups/backend-shared-ai-rollout"
|
|
mkdir -p "`$backend_backup_root"
|
|
if [ -d "`$project/backend" ]; then
|
|
tar -czf "`$backend_backup_root/backend-before-`$stamp.tgz" -C "`$project" backend || true
|
|
fi
|
|
rm -rf "`$project/backend"
|
|
mkdir -p "`$project/backend"
|
|
tar -xzf "`$backend_archive" --strip-components=1 -C "`$project/backend"
|
|
cp "`$compose_tmp" "`$project/`$compose"
|
|
docker compose -f "`$compose" build shared-ai-platform backend
|
|
docker compose -f "`$compose" up -d shared-ai-platform backend
|
|
docker compose -f "`$compose" ps shared-ai-platform backend frontend
|
|
"@
|
|
$remoteDeploy = Invoke-Remote -Name "remote-shared-ai-docker-deploy" -Command $remoteScript
|
|
if ($remoteDeploy.exit_code -eq 0) {
|
|
Add-Check -Key "remote_deploy" -Label "remote docker deploy" -Status "pass" -Detail "shared-ai-platform and backend rebuilt/restarted on remote host" -Meta @{ log = $remoteDeploy.log }
|
|
} else {
|
|
Add-Check -Key "remote_deploy" -Label "remote docker deploy" -Status "fail" -Detail "Remote deploy failed" -Meta @{ log = $remoteDeploy.log }
|
|
}
|
|
}
|
|
|
|
$failedBeforeVerify = @($checks | Where-Object { $_.required -and $_.status -eq "fail" })
|
|
if ($failedBeforeVerify.Count -eq 0) {
|
|
Start-Sleep -Seconds 8
|
|
$appRegistryLog = Join-Path $artifactDir "shared-ai-app-registry.log"
|
|
$sharedAiAdminBase = "http://${ServerHost}:8012/api/v0.2"
|
|
$appRegistryHeaders = @{
|
|
Authorization = "Bearer $($sharedEnv['SHARED_AI_PLATFORM_API_KEY'])"
|
|
"Content-Type" = "application/json"
|
|
}
|
|
$appRegistryPayload = @{
|
|
app_id = "ai-contract-platform"
|
|
name = "AI Contract Platform"
|
|
description = "Remote app registration for ai-contract-platform"
|
|
owner_team = "contract-platform"
|
|
scopes = @(
|
|
"conversations.read",
|
|
"conversations.write",
|
|
"knowledge.read",
|
|
"knowledge.write"
|
|
)
|
|
}
|
|
try {
|
|
try {
|
|
$createResponse = Invoke-RestMethod -Method Post -Uri "$sharedAiAdminBase/apps" -Headers $appRegistryHeaders -Body ($appRegistryPayload | ConvertTo-Json -Depth 6)
|
|
$createResponse | ConvertTo-Json -Depth 6 | Set-Content -Encoding UTF8 -Path $appRegistryLog
|
|
} catch {
|
|
$createFailed = $_.Exception
|
|
if ($createFailed.Response -and [int]$createFailed.Response.StatusCode -ne 409) {
|
|
throw
|
|
}
|
|
"APP_ALREADY_EXISTS" | Set-Content -Encoding UTF8 -Path $appRegistryLog
|
|
}
|
|
|
|
$scopeReadResponse = Invoke-RestMethod -Method Get -Uri "$sharedAiAdminBase/apps/ai-contract-platform/scopes" -Headers $appRegistryHeaders
|
|
($scopeReadResponse | ConvertTo-Json -Depth 6) | Add-Content -Encoding UTF8 -Path $appRegistryLog
|
|
$existingScopes = @($scopeReadResponse.data.scopes)
|
|
$missingScopes = @($appRegistryPayload.scopes | Where-Object { $_ -notin $existingScopes })
|
|
if ($missingScopes.Count -gt 0) {
|
|
throw "Shared AI app registry is missing scopes: $($missingScopes -join ', ')"
|
|
}
|
|
|
|
Add-Check -Key "app_registry" -Label "shared-ai app registry" -Status "pass" -Detail "ai-contract-platform app registration/scopes ensured on remote host" -Meta @{ log = $appRegistryLog }
|
|
} catch {
|
|
($_ | Out-String) | Set-Content -Encoding UTF8 -Path $appRegistryLog
|
|
Add-Check -Key "app_registry" -Label "shared-ai app registry" -Status "fail" -Detail "Failed to ensure ai-contract-platform app registry/scopes" -Meta @{ log = $appRegistryLog }
|
|
}
|
|
|
|
$providerStatus = Invoke-Remote -Name "remote-shared-ai-provider-status" -Command "curl -fsS http://127.0.0.1:8012/system/provider-status"
|
|
if ($providerStatus.exit_code -eq 0 -and $providerStatus.output -match 'openai_compatible') {
|
|
Add-Check -Key "provider_status" -Label "shared-ai provider status" -Status "pass" -Detail "shared-ai-platform provider-status responded on remote host" -Meta @{ log = $providerStatus.log }
|
|
} else {
|
|
Add-Check -Key "provider_status" -Label "shared-ai provider status" -Status "fail" -Detail "shared-ai-platform provider-status probe failed" -Meta @{ log = $providerStatus.log }
|
|
}
|
|
|
|
$backendHealth = Invoke-Remote -Name "remote-backend-health" -Command "curl -fsS http://127.0.0.1:8001/health"
|
|
if ($backendHealth.exit_code -eq 0 -and $backendHealth.output -match 'status') {
|
|
Add-Check -Key "backend_health" -Label "backend health" -Status "pass" -Detail "backend health probe responded on remote host" -Meta @{ log = $backendHealth.log }
|
|
} else {
|
|
Add-Check -Key "backend_health" -Label "backend health" -Status "fail" -Detail "backend health probe failed" -Meta @{ log = $backendHealth.log }
|
|
}
|
|
|
|
$reviewSmokeScript = @"
|
|
set -euo pipefail
|
|
cd '$RemoteProjectPath'
|
|
docker compose -f $ComposeFile exec -T backend python - <<'PY'
|
|
import json
|
|
from app.services.review_service import ReviewService
|
|
|
|
service = ReviewService(db=None)
|
|
class StubRepository:
|
|
def __init__(self): self.items={}
|
|
def create(self, obj): self.items[obj.id]=obj; return obj
|
|
def update(self, obj): self.items[obj.id]=obj; return obj
|
|
def get_by_id(self, review_id, tenant_id=None, user_id=None): return self.items.get(review_id)
|
|
service.repository = StubRepository()
|
|
class StubRuleRepo:
|
|
def list_active_for_contract_type(self, tenant_id, contract_type): return []
|
|
service.review_rule_repository = StubRuleRepo()
|
|
class StubReviewResultRepository:
|
|
def __init__(self): self.items={}
|
|
def create(self, obj): self.items[obj.review_task_id]=obj; return obj
|
|
def update(self, obj): self.items[obj.review_task_id]=obj; return obj
|
|
def get_by_review_task_id(self, review_task_id): return self.items.get(review_task_id)
|
|
service.review_result_repository = StubReviewResultRepository()
|
|
class StubFileService: pass
|
|
service.file_service = StubFileService()
|
|
created = service.create_review_task(
|
|
tenant_id='tenant-demo-001',
|
|
user_id='user-admin',
|
|
review_as='party_a',
|
|
contract_type='procurement',
|
|
text='This agreement includes auto-renewal, limitation of liability, payment milestones, confidentiality obligations, and liquidated damages. Please produce a structured legal review result in Chinese.',
|
|
file_name='inline.txt',
|
|
)
|
|
result = service.get_review_result({'id':'user-admin','tenant_id':'tenant-demo-001'}, created['id'])
|
|
assert result['risk_summary']['overall'] != 'low'
|
|
assert result['recommendation'] in {'manual_review', 'proceed_with_revision', 'proceed'}
|
|
print(json.dumps({'created': created, 'result': result}, ensure_ascii=False))
|
|
PY
|
|
"@
|
|
$reviewSmoke = Invoke-Remote -Name "remote-review-smoke" -Command $reviewSmokeScript
|
|
if ($reviewSmoke.exit_code -eq 0) {
|
|
Add-Check -Key "review_smoke" -Label "backend shared-ai review smoke" -Status "pass" -Detail "Remote backend can execute review flow against shared-ai-platform" -Meta @{ log = $reviewSmoke.log }
|
|
} else {
|
|
Add-Check -Key "review_smoke" -Label "backend shared-ai review smoke" -Status "fail" -Detail "Remote backend review smoke failed" -Meta @{ log = $reviewSmoke.log }
|
|
}
|
|
}
|
|
|
|
$failedRequired = @($checks | Where-Object { $_.required -and $_.status -eq "fail" })
|
|
$overallStatus = if ($failedRequired.Count -gt 0) { "FAIL" } else { "PASS" }
|
|
|
|
$summary = [ordered]@{
|
|
name = "shared-ai-platform-docker-deploy"
|
|
status = $overallStatus
|
|
generated_at = (Get-Date).ToUniversalTime().ToString("o")
|
|
artifact_dir = $artifactDir
|
|
target = [ordered]@{
|
|
server_host = $ServerHost
|
|
remote_project_path = $RemoteProjectPath
|
|
remote_service_path = $RemoteServicePath
|
|
}
|
|
checks = $checks
|
|
}
|
|
Save-Json -Path $summaryJson -Value $summary
|
|
|
|
$markdown = New-Object System.Collections.Generic.List[string]
|
|
$markdown.Add("# Shared AI Platform 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("- Server: $ServerHost") | Out-Null
|
|
$markdown.Add("- Remote path: $RemoteServicePath") | Out-Null
|
|
$markdown.Add("") | Out-Null
|
|
$markdown.Add("## Checks") | Out-Null
|
|
foreach ($check in $checks) {
|
|
$markdown.Add("- [$($check.status)] $($check.label): $($check.detail)") | Out-Null
|
|
}
|
|
$markdown | Set-Content -Encoding UTF8 -Path $summaryMd
|
|
|
|
Write-Output $artifactDir
|