Ka-Note/ka-note/deploy.ps1

241 lines
9.5 KiB
PowerShell

<#
.PARAMETER CheckOnly
Download and verify the prod DB without building or deploying.
.PARAMETER SkipDbBackup
Skip the DB download/verification step (use when DB hasn't changed since last run).
#>
param([switch]$CheckOnly, [switch]$SkipDbBackup)
$ErrorActionPreference = "Stop"
# Load .env from script directory
$envFile = Join-Path $PSScriptRoot ".env"
if (Test-Path $envFile) {
Get-Content $envFile | Where-Object { $_ -match '^\s*[^#]\S+=\S' } | ForEach-Object {
$k, $v = $_ -split '=', 2
[System.Environment]::SetEnvironmentVariable($k.Trim(), $v.Trim(), 'Process')
}
}
$ACR = "koogleacr"
$APP = "ka-note"
$RG = "rg-koogle-prod"
$IMAGE = "$ACR.azurecr.io/${APP}:latest"
# Read current version (don't bump yet -- only after DB check passes)
$versionFile = Join-Path $PSScriptRoot "VERSION"
$current = (Get-Content $versionFile -Raw).Trim()
if ($CheckOnly) {
$VERSION = $current
} else {
$parts = $current -split '\.'
$parts[2] = [int]$parts[2] + 1
$VERSION = $parts -join '.'
}
$checkLabel = if ($CheckOnly) { " [check-only]" } else { "" }
Write-Host "=== Version: $VERSION$checkLabel ===" -ForegroundColor Yellow
$KuduDbUrl = "https://${APP}.scm.azurewebsites.net/api/vfs/data/ka-note.db"
$AppUrl = "https://$APP.azurewebsites.net"
$BackupDir = Join-Path $PSScriptRoot "backups"
$null = New-Item -ItemType Directory -Path $BackupDir -Force
$BackupFile = Join-Path $BackupDir ("ka-note-pre-deploy-$VERSION.db")
if ($SkipDbBackup) {
Write-Host "=== Skipping DB Backup (-SkipDbBackup) ===" -ForegroundColor Yellow
$KuduToken = $null
} else {
Write-Host "=== Backup DB from Prod ===" -ForegroundColor Cyan
$KuduToken = az account get-access-token --resource "https://management.azure.com" --query accessToken -o tsv
if ($LASTEXITCODE -ne 0) { throw "Failed to get Azure access token" }
# Flush WAL before download so the snapshot is consistent (WAL file not included in Kudu download).
if ($env:KA_NOTE_DEPLOY_API_KEY) {
Write-Host " Flushing WAL checkpoint before download..." -ForegroundColor DarkGray
try {
$cpResult = Invoke-RestMethod -Uri "$AppUrl/api/admin/checkpoint" -Method POST `
-Headers @{ Authorization = "Bearer $env:KA_NOTE_DEPLOY_API_KEY" } `
-UseBasicParsing -TimeoutSec 30
Write-Host " Checkpoint: $($cpResult.checkpointed)/$($cpResult.log) pages" -ForegroundColor DarkGray
} catch {
Write-Host " Checkpoint call failed (continuing anyway): $_" -ForegroundColor Yellow
}
} else {
Write-Host " KA_NOTE_DEPLOY_API_KEY not set, skipping WAL checkpoint" -ForegroundColor Yellow
}
Write-Host " Downloading ka-note.db from prod..." -ForegroundColor DarkGray
try {
Invoke-WebRequest -Uri $KuduDbUrl `
-Headers @{ Authorization = "Bearer $KuduToken" } `
-OutFile $BackupFile -UseBasicParsing
} catch {
throw "Kudu DB download failed: $_"
}
$DbSize = (Get-Item $BackupFile).Length
Write-Host " Downloaded: $([math]::Round($DbSize/1KB,1)) KB" -ForegroundColor DarkGray
if ($DbSize -lt 4096) {
throw "DB backup is suspiciously small ($DbSize bytes) - aborting deploy"
}
# Verify with sqlite3 (must be in PATH)
$IntegrityResult = & sqlite3 $BackupFile "PRAGMA integrity_check;" 2>&1
if ($LASTEXITCODE -ne 0 -or $IntegrityResult -ne 'ok') {
throw "DB integrity_check FAILED: $IntegrityResult - aborting deploy. Backup saved at $BackupFile"
}
Write-Host " integrity_check: ok ($([math]::Round($DbSize/1KB,1)) KB)" -ForegroundColor Green
if ($CheckOnly) {
Write-Host ""
Write-Host "=== Check passed -- no deploy performed ===" -ForegroundColor Green
Write-Host " Backup: $BackupFile" -ForegroundColor DarkGray
Write-Host ""
exit 0
}
}
# Bump version now that DB check passed
Set-Content $versionFile $VERSION -Encoding UTF8 -NoNewline
Write-Host "=== Generate migrations ===" -ForegroundColor Cyan
Push-Location server
npx drizzle-kit generate
Pop-Location
if ($LASTEXITCODE -ne 0) { throw "Migration generation failed" }
Write-Host "=== Login to ACR ===" -ForegroundColor Cyan
$token = (az acr login --name $ACR --expose-token --output tsv --query accessToken)
if ($LASTEXITCODE -ne 0) { throw "ACR token fetch failed" }
$token | docker login "$ACR.azurecr.io" --username 00000000-0000-0000-0000-000000000000 --password-stdin
if ($LASTEXITCODE -ne 0) { throw "ACR login failed" }
Write-Host "=== Build Docker image ===" -ForegroundColor Cyan
docker build -t $IMAGE `
--build-arg VITE_AZURE_CLIENT_ID=$env:AZURE_CLIENT_ID `
--build-arg VITE_AZURE_TENANT_ID=$env:AZURE_TENANT_ID `
--build-arg APP_VERSION=$VERSION `
.
if ($LASTEXITCODE -ne 0) { throw "Docker build failed" }
Write-Host "=== Push to ACR ===" -ForegroundColor Cyan
docker push $IMAGE
if ($LASTEXITCODE -ne 0) { throw "Docker push failed" }
Write-Host "=== Set App Service environment ===" -ForegroundColor Cyan
az webapp config appsettings set --name $APP --resource-group $RG --settings `
AZURE_CLIENT_ID=$env:AZURE_CLIENT_ID `
AZURE_TENANT_ID=$env:AZURE_TENANT_ID | Out-Null
Write-Host "=== Graceful DB shutdown ===" -ForegroundColor Cyan
if ($env:KA_NOTE_DEPLOY_API_KEY) {
try {
Write-Host " Waking up app..." -ForegroundColor DarkGray
Invoke-RestMethod -Uri "$AppUrl/api/health" -Method GET `
-UseBasicParsing -TimeoutSec 30 | Out-Null
$shutdownResult = Invoke-RestMethod -Uri "$AppUrl/api/admin/shutdown" -Method POST `
-Headers @{ Authorization = "Bearer $env:KA_NOTE_DEPLOY_API_KEY" } `
-UseBasicParsing -TimeoutSec 30
Write-Host " Shutdown response: $($shutdownResult.message)" -ForegroundColor DarkGray
# Shutdown does WAL checkpoint synchronously before responding, then closes DB after 500ms.
# Wait 10s to ensure clean close before Azure kills the container.
Start-Sleep -Seconds 10
} catch {
Write-Host " Shutdown call failed (continuing anyway): $_" -ForegroundColor Yellow
}
} else {
Write-Host " KA_NOTE_DEPLOY_API_KEY not set, skipping graceful shutdown" -ForegroundColor Yellow
}
Write-Host "=== Restart App Service ===" -ForegroundColor Cyan
az webapp restart --name $APP --resource-group $RG
Write-Host "=== Post-Deploy DB Validation ===" -ForegroundColor Cyan
$validationOk = $false
$keepValidating = $true
while ($keepValidating) {
$keepValidating = $false
Write-Host " Waiting for server to boot (~20s)..." -ForegroundColor DarkGray
$bootDeadline = (Get-Date).AddSeconds(45)
$serverReady = $false
while ((Get-Date) -lt $bootDeadline) {
Start-Sleep -Seconds 3
try {
$h = Invoke-RestMethod -Uri "$AppUrl/api/health" -Method GET -UseBasicParsing -TimeoutSec 5
if ($h.status -eq 'ok') { $serverReady = $true; break }
} catch { }
}
if (-not $serverReady) {
Write-Host " WARNING: Server did not respond within 45s" -ForegroundColor Red
}
if ($serverReady -and $env:KA_NOTE_DEPLOY_API_KEY) {
$authHeader = @{ Authorization = "Bearer $env:KA_NOTE_DEPLOY_API_KEY" }
$maxRetries = 6
for ($attempt = 1; $attempt -le $maxRetries; $attempt++) {
try {
$stats = Invoke-RestMethod -Uri "$AppUrl/api/admin/stats" -Method GET `
-Headers $authHeader -UseBasicParsing -TimeoutSec 15
$contextCount = $stats.contextCount
$topicCount = $stats.topicCount
Write-Host " [$attempt/$maxRetries] Contexts: $contextCount, Topics: $topicCount" -ForegroundColor DarkGray
if ($contextCount -gt 0 -and $topicCount -gt 0) {
Write-Host " DB validation passed." -ForegroundColor Green
$validationOk = $true
break
}
if ($attempt -lt $maxRetries) { Start-Sleep -Seconds 10 }
} catch {
Write-Host " [$attempt/$maxRetries] Request error: $_" -ForegroundColor Yellow
if ($attempt -lt $maxRetries) { Start-Sleep -Seconds 10 }
}
}
if (-not $validationOk) {
Write-Host " DB validation FAILED after $maxRetries attempts" -ForegroundColor Red
}
} elseif (-not $env:KA_NOTE_DEPLOY_API_KEY) {
Write-Host " KA_NOTE_DEPLOY_API_KEY not set, skipping DB validation" -ForegroundColor Yellow
$validationOk = $true
}
if (-not $validationOk) {
$latestBackup = Get-ChildItem -Path $BackupDir -Filter "*.db" |
Sort-Object LastWriteTime -Descending | Select-Object -First 1
Write-Host ""
Write-Host " !! DB appears empty or unreachable after deploy !!" -ForegroundColor Red
Write-Host " Pre-deploy backup: $($latestBackup.FullName)" -ForegroundColor Yellow
Write-Host ""
$answer = Read-Host " Restore backup to prod? (yes/no/retry)"
if ($answer -eq 'retry') {
Write-Host " Retrying validation..." -ForegroundColor Cyan
$keepValidating = $true
} elseif ($answer -eq 'yes') {
Write-Host " Uploading backup to prod..." -ForegroundColor Cyan
if (-not $KuduToken) {
$KuduToken = az account get-access-token --resource "https://management.azure.com" --query accessToken -o tsv
if ($LASTEXITCODE -ne 0) { throw "Failed to get Azure access token for restore" }
}
Invoke-WebRequest -Uri $KuduDbUrl `
-Method PUT `
-Headers @{ Authorization = "Bearer $KuduToken" } `
-InFile $latestBackup.FullName -UseBasicParsing | Out-Null
Write-Host " Backup uploaded. Restarting app..." -ForegroundColor Cyan
az webapp restart --name $APP --resource-group $RG
Write-Host " Restore complete. Verify manually: $AppUrl" -ForegroundColor Green
} else {
Write-Host " Restore skipped. Manual intervention required!" -ForegroundColor Red
exit 1
}
}
}
Write-Host "=== Done! $VERSION deployed ===" -ForegroundColor Green
Write-Host "Check: https://$APP.azurewebsites.net"