From 50b7c817367a60cde613038c32b5221962899dda Mon Sep 17 00:00:00 2001 From: kellymichels Date: Sat, 25 Jul 2026 22:16:48 -0500 Subject: [PATCH] fix(ps): zbackup explicit target + robust DATABASE_URL parsing + real pg_dump error reporting (#10) * fix(ps): zbackup explicit target + robust DATABASE_URL parsing; ssh-stderr deploy fix Restores parked, previously-uncommitted PowerShell improvements: - zbackup / zbackup_and_sync require an explicit target: bare invocation now prints usage instead of quietly backing up everything; 'all' does what bare used to (matching zdeploy). setup_backup_schedule.ps1 passes 'all' to the scheduled task; both tolerate switch-style args. - zbackup parses more DATABASE_URL styles: strips surrounding quotes (Prisma convention), accepts postgres:// and postgresql+driver:// schemes, and treats the port as optional (defaults to 5432). - Invoke-Ec2Step survives ssh stderr warnings: under ErrorActionPreference 'Stop', PS 5.1 turns any native stderr line (e.g. Docker's COMPOSE_BAKE deprecation notice) into a terminating NativeCommandError, aborting a deploy that actually succeeded. Drop to Continue locally and flatten stderr so only the real exit code decides success. * fix(ps): zbackup reports the real pg_dump failure, not "No DATABASE_URL" Mirror of the bash fix. Invoke-LocalPgDump now owns all its messaging (caller just captures success) and distinguishes the cases: - no .env / no DATABASE_URL -> calm "No local DATABASE_URL". - database not reachable (connection refused / could not connect / DNS / timeout) -> calm "Local database not running at host:port" - a stopped dev DB is a normal state. - any other failure (version mismatch, auth, missing db) -> the loud, full pg_dump error plus the host:port/db it tried, instead of a bare "pg_dump failed (exit N)" followed by a misleading "No DATABASE_URL". Captures pg_dump stderr (was 2>$null); drops ErrorActionPreference to Continue locally so PS 5.1 doesn't turn that stderr into a terminating NativeCommandError under the script's 'Stop' setting. --- CHANGELOG.md | 10 ++++++ ZHelpers.ps1 | 11 ++++++- setup_backup_schedule.ps1 | 2 +- zbackup.ps1 | 65 ++++++++++++++++++++++++++++++--------- zbackup_and_sync.ps1 | 22 ++++++++----- 5 files changed, 87 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f575982..fec7eef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,16 @@ Notable changes to the Evomedia.net Token Savers. grew up on per-project switches. ### Changed +- **`zbackup` / `zbackup_and_sync` require an explicit target** — running + them bare now shows usage instead of quietly backing up every project; + `all` does what bare invocation used to (matching `zdeploy`). The + scheduled task created by `setup_backup_schedule.ps1` passes `all` — + re-run it if your task was registered before this change. +- **`zbackup` parses more `DATABASE_URL` styles** — double/single-quoted + values (Prisma convention), `postgres://` and `postgresql+driver://` + schemes, and URLs without an explicit port (defaults to 5432) all work; + previously these skipped the Postgres dump with "Could not parse + DATABASE_URL". - **`zkill` / port cleanup kills the whole process tree** — listeners on a project's port are now terminated children-first. Auto-reloading servers (uvicorn/watchfiles, nodemon) spawn workers that inherit the listening diff --git a/ZHelpers.ps1 b/ZHelpers.ps1 index a5127d4..52ae1e1 100644 --- a/ZHelpers.ps1 +++ b/ZHelpers.ps1 @@ -109,7 +109,16 @@ function Invoke-Ec2Step { ) $cfg = Get-ZConfig Write-Host " >> $Label" -ForegroundColor DarkCyan - ssh -o StrictHostKeyChecking=no -i $cfg.ec2.pemKey (Get-Ec2Target) $Bash + # ssh can emit warnings on stderr (e.g. Docker's "COMPOSE_BAKE is + # deprecated" notice during a compose build). The deploy runs under + # ErrorActionPreference='Stop', and PowerShell 5.1 turns any native stderr + # line into a terminating NativeCommandError — aborting the deploy before we + # ever read the real exit code, even though the remote step succeeded. Drop + # to Continue locally (function-scoped, auto-reverts) and flatten stderr + # into normal output, so only the actual exit status decides success. + $ErrorActionPreference = 'Continue' + ssh -o StrictHostKeyChecking=no -i $cfg.ec2.pemKey (Get-Ec2Target) $Bash 2>&1 | + ForEach-Object { "$_" } if ($LASTEXITCODE -ne 0) { $msg = "Remote step failed: '$Label' (exit $LASTEXITCODE)." if ($FailHint) { $msg += " $FailHint" } diff --git a/setup_backup_schedule.ps1 b/setup_backup_schedule.ps1 index c6a15b6..90e904f 100644 --- a/setup_backup_schedule.ps1 +++ b/setup_backup_schedule.ps1 @@ -46,7 +46,7 @@ if ($existingTask) { $trigger = New-ScheduledTaskTrigger -Daily -At "02:00" $action = New-ScheduledTaskAction ` -Execute "powershell.exe" ` - -Argument "-ExecutionPolicy Bypass -NoProfile -File `"$ScriptPath`"" + -Argument "-ExecutionPolicy Bypass -NoProfile -File `"$ScriptPath`" all" $settings = New-ScheduledTaskSettingsSet ` -AllowStartIfOnBatteries ` -DontStopIfGoingOnBatteries ` diff --git a/zbackup.ps1 b/zbackup.ps1 index 09c9bfb..a030764 100644 --- a/zbackup.ps1 +++ b/zbackup.ps1 @@ -6,8 +6,8 @@ # project's .env has a DATABASE_URL) into the backups folder. # # Usage: -# zbackup # every project in zconfig.json + this scripts folder # zbackup [ ...] +# zbackup all # every project in zconfig.json + this scripts folder # zbackup scripts # just this scripts folder ('scripts' is a reserved word) # zbackup pyapp -Tag "pre-migration" # @@ -26,8 +26,22 @@ Start-ZTracking $cfg = Get-ZConfig $ProjectsBackupRoot = $cfg.paths.backupsLocal -$includeScripts = $false +# Tolerate switch-style args (zbackup -myproject) from muscle memory. +$Projects = @($Projects | ForEach-Object { $_.TrimStart('-') }) + +# No args shows usage instead of quietly backing up everything — 'all' is +# explicit, matching zdeploy and the other z-commands. if ($Projects.Count -eq 0) { + $keys = (Get-ZProjectKeys) -join ', ' + Write-Host "" + Write-Host "Usage: zbackup [ ...] | all | scripts [-Tag `"label`"]" -ForegroundColor Yellow + Write-Host " Projects in zconfig.json: $keys" -ForegroundColor Gray + Write-Host " 'all' backs up every project plus this scripts folder; 'scripts' just this folder." -ForegroundColor Gray + Stop-ZTracking; exit 1 +} + +$includeScripts = $false +if ($Projects -contains 'all') { $Projects = @(Get-ZProjectKeys) $includeScripts = $true } elseif ($Projects -contains 'scripts') { @@ -53,19 +67,29 @@ function Ensure-BackupDir { # Dump the project's Postgres database if its .env declares a DATABASE_URL. # Checks \.env, then \backend\.env (frontend/backend split projects). +# Prints its own status line for every outcome; returns $true (dumped) or $false. +# A not-running database (connection refused) is a calm, expected skip; a real +# failure (version mismatch, auth, missing db) prints the loud pg_dump error. function Invoke-LocalPgDump { param([string]$Root, [string]$OutPath) $envFile = Join-Path $Root ".env" if (-not (Test-Path -LiteralPath $envFile)) { $envFile = Join-Path $Root "backend\.env" } - if (-not (Test-Path -LiteralPath $envFile)) { return $false } + if (-not (Test-Path -LiteralPath $envFile)) { + Write-Host " No local DATABASE_URL - source-only backup." -ForegroundColor DarkGray; return $false + } $dbLine = @(Get-Content -LiteralPath $envFile | Where-Object { $_ -match "^DATABASE_URL=" } | Select-Object -First 1) - if ($dbLine.Count -eq 0) { return $false } + if ($dbLine.Count -eq 0) { + Write-Host " No local DATABASE_URL - source-only backup." -ForegroundColor DarkGray; return $false + } - $dbUrl = ($dbLine[0] -replace "^DATABASE_URL=", "").Trim() - $dbUrl = $dbUrl -replace '^postgresql\+[^:]+://', 'postgresql://' + # Strip surrounding quotes (Prisma-style .env values are double-quoted), + # accept postgres:// and postgresql+driver:// schemes, and treat the + # port as optional (Postgres default 5432). + $dbUrl = ($dbLine[0] -replace "^DATABASE_URL=", "").Trim().Trim('"').Trim("'") + $dbUrl = $dbUrl -replace '^postgres(ql)?(\+[^:]+)?://', 'postgresql://' $rx = [regex]::Match( $dbUrl, - '^postgresql://(?[^:]+):(?[^@]+)@(?[^:]+):(?\d+)/(?[^?]+)' + '^postgresql://(?[^:@/]+):(?[^@]+)@(?[^:/?]+)(:(?\d+))?/(?[^?\s]+)' ) if (-not $rx.Success) { Write-Host ' Could not parse DATABASE_URL - skipping PG backup' -ForegroundColor Red @@ -74,7 +98,7 @@ function Invoke-LocalPgDump { $env:PGPASSWORD = [Uri]::UnescapeDataString($rx.Groups['pass'].Value) $pgUser = $rx.Groups['user'].Value $pgHost = $rx.Groups['host'].Value - $pgPort = $rx.Groups['port'].Value + $pgPort = if ($rx.Groups['port'].Success) { $rx.Groups['port'].Value } else { '5432' } $pgDb = $rx.Groups['db'].Value $pgDump = "pg_dump" @@ -87,16 +111,31 @@ function Invoke-LocalPgDump { foreach ($candidate in $pgBinPaths) { if (Test-Path $candidate) { $pgDump = $candidate; break } } - & $pgDump -h $pgHost -p $pgPort -U $pgUser -d $pgDb -F p -f $OutPath 2>$null + # Capture stderr so a failure is diagnosable (version mismatch, unreachable + # host, auth) instead of a bare "pg_dump failed". PS 5.1 turns native stderr + # into a terminating NativeCommandError under 'Stop', so drop to Continue + # locally (function-scoped, auto-reverts) while running pg_dump. + $ErrorActionPreference = 'Continue' + $errOut = & $pgDump -h $pgHost -p $pgPort -U $pgUser -d $pgDb -F p -f $OutPath 2>&1 $ok = ($LASTEXITCODE -eq 0) -and (Test-Path -LiteralPath $OutPath) Remove-Item Env:\PGPASSWORD -ErrorAction SilentlyContinue if ($ok) { $size = [math]::Round((Get-Item $OutPath).Length / 1KB, 1) Write-Host " PostgreSQL dump: ${size} KB" -ForegroundColor Green - } else { - Write-Host " pg_dump failed (exit $LASTEXITCODE)" -ForegroundColor Red + return $true } - return $ok + $errText = ($errOut | Out-String).Trim() + # Not reachable (usually just not running locally) is expected - stay calm. + if ($errText -match '(?i)connection refused|could not connect|no route to host|could not translate host|timeout expired') { + Write-Host " Local database not running at ${pgHost}:${pgPort} - source-only backup." -ForegroundColor DarkGray + return $false + } + # A real failure (version mismatch, auth, missing db) - show the reason. + Write-Host " pg_dump failed (${pgHost}:${pgPort}/${pgDb} as ${pgUser}):" -ForegroundColor Red + foreach ($line in ($errText -split '\r?\n' | Where-Object { $_.Trim() -ne '' })) { + Write-Host " $line" -ForegroundColor Red + } + return $false } function Invoke-ProjectBackup { @@ -124,8 +163,6 @@ function Invoke-ProjectBackup { $dbDumpPath = Join-Path $dumpDir "database_pg.sql" if (Invoke-LocalPgDump -Root $root -OutPath $dbDumpPath) { $extraFiles += $dbDumpPath - } else { - Write-Host " No local DATABASE_URL - source-only backup." -ForegroundColor DarkGray } Write-Host " [2/3] Archiving source..." -ForegroundColor Yellow diff --git a/zbackup_and_sync.ps1 b/zbackup_and_sync.ps1 index cb0576b..19d9af1 100644 --- a/zbackup_and_sync.ps1 +++ b/zbackup_and_sync.ps1 @@ -5,11 +5,11 @@ # zbackup_and_sync.ps1 — run backups, then sync the backups folder offsite. # # Usage: -# zbackup_and_sync.ps1 # backup everything + sync # zbackup_and_sync.ps1 [ ...] +# zbackup_and_sync.ps1 all # backup everything + sync # # Scheduled Task example (see setup_backup_schedule.ps1): -# powershell -ExecutionPolicy Bypass -NoProfile -File "\zbackup_and_sync.ps1" +# powershell -ExecutionPolicy Bypass -NoProfile -File "\zbackup_and_sync.ps1" all param( [Parameter(Position = 0, ValueFromRemainingArguments = $true)] @@ -21,6 +21,18 @@ $ScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Definition . (Join-Path $ScriptRoot "ZHelpers.ps1") Start-ZTracking +# Tolerate switch-style args from muscle memory; require an explicit target +# ('all' included) — same convention as zbackup/zdeploy. +$Projects = @($Projects | ForEach-Object { $_.TrimStart('-') }) +if ($Projects.Count -eq 0) { + $keys = (Get-ZProjectKeys) -join ', ' + Write-Host "" + Write-Host "Usage: zbackup_and_sync [ ...] | all" -ForegroundColor Yellow + Write-Host " Projects in zconfig.json: $keys" -ForegroundColor Gray + Write-Host " 'all' backs up every project plus the scripts folder, then syncs offsite." -ForegroundColor Gray + Stop-ZTracking; exit 1 +} + Write-Host "" Write-Host "============================================" -ForegroundColor Cyan Write-Host " Backup & Sync - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Cyan @@ -29,11 +41,7 @@ Write-Host "" Write-Host "[1/2] Running backups..." -ForegroundColor Yellow $backupPath = Join-Path $ScriptRoot "zbackup.ps1" -if ($Projects.Count -gt 0) { - & powershell -NoProfile -File $backupPath @Projects -} else { - & powershell -NoProfile -File $backupPath -} +& powershell -NoProfile -File $backupPath @Projects $backupExitCode = $LASTEXITCODE if ($backupExitCode -ne 0) {