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.
This commit is contained in:
KellyMichels 2026-07-23 00:15:19 -05:00
parent 4ebde3ebcf
commit 84523c8e4d
5 changed files with 59 additions and 15 deletions

View File

@ -44,6 +44,16 @@ Notable changes to the Evomedia.net Token Savers.
grew up on per-project switches. grew up on per-project switches.
### Changed ### 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 - **`zkill` / port cleanup kills the whole process tree** — listeners on a
project's port are now terminated children-first. Auto-reloading servers project's port are now terminated children-first. Auto-reloading servers
(uvicorn/watchfiles, nodemon) spawn workers that inherit the listening (uvicorn/watchfiles, nodemon) spawn workers that inherit the listening

View File

@ -109,7 +109,16 @@ function Invoke-Ec2Step {
) )
$cfg = Get-ZConfig $cfg = Get-ZConfig
Write-Host " >> $Label" -ForegroundColor DarkCyan 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) { if ($LASTEXITCODE -ne 0) {
$msg = "Remote step failed: '$Label' (exit $LASTEXITCODE)." $msg = "Remote step failed: '$Label' (exit $LASTEXITCODE)."
if ($FailHint) { $msg += " $FailHint" } if ($FailHint) { $msg += " $FailHint" }

View File

@ -46,7 +46,7 @@ if ($existingTask) {
$trigger = New-ScheduledTaskTrigger -Daily -At "02:00" $trigger = New-ScheduledTaskTrigger -Daily -At "02:00"
$action = New-ScheduledTaskAction ` $action = New-ScheduledTaskAction `
-Execute "powershell.exe" ` -Execute "powershell.exe" `
-Argument "-ExecutionPolicy Bypass -NoProfile -File `"$ScriptPath`"" -Argument "-ExecutionPolicy Bypass -NoProfile -File `"$ScriptPath`" all"
$settings = New-ScheduledTaskSettingsSet ` $settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries ` -AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries ` -DontStopIfGoingOnBatteries `

View File

@ -6,8 +6,8 @@
# project's .env has a DATABASE_URL) into the backups folder. # project's .env has a DATABASE_URL) into the backups folder.
# #
# Usage: # Usage:
# zbackup # every project in zconfig.json + this scripts folder
# zbackup <project> [<project> ...] # zbackup <project> [<project> ...]
# zbackup all # every project in zconfig.json + this scripts folder
# zbackup scripts # just this scripts folder ('scripts' is a reserved word) # zbackup scripts # just this scripts folder ('scripts' is a reserved word)
# zbackup pyapp -Tag "pre-migration" # zbackup pyapp -Tag "pre-migration"
# #
@ -26,8 +26,22 @@ Start-ZTracking
$cfg = Get-ZConfig $cfg = Get-ZConfig
$ProjectsBackupRoot = $cfg.paths.backupsLocal $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) { if ($Projects.Count -eq 0) {
$keys = (Get-ZProjectKeys) -join ', '
Write-Host ""
Write-Host "Usage: zbackup <project> [<project> ...] | 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) $Projects = @(Get-ZProjectKeys)
$includeScripts = $true $includeScripts = $true
} elseif ($Projects -contains 'scripts') { } elseif ($Projects -contains 'scripts') {
@ -61,11 +75,14 @@ function Invoke-LocalPgDump {
$dbLine = @(Get-Content -LiteralPath $envFile | Where-Object { $_ -match "^DATABASE_URL=" } | Select-Object -First 1) $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) { return $false }
$dbUrl = ($dbLine[0] -replace "^DATABASE_URL=", "").Trim() # Strip surrounding quotes (Prisma-style .env values are double-quoted),
$dbUrl = $dbUrl -replace '^postgresql\+[^:]+://', 'postgresql://' # 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( $rx = [regex]::Match(
$dbUrl, $dbUrl,
'^postgresql://(?<user>[^:]+):(?<pass>[^@]+)@(?<host>[^:]+):(?<port>\d+)/(?<db>[^?]+)' '^postgresql://(?<user>[^:@/]+):(?<pass>[^@]+)@(?<host>[^:/?]+)(:(?<port>\d+))?/(?<db>[^?\s]+)'
) )
if (-not $rx.Success) { if (-not $rx.Success) {
Write-Host ' Could not parse DATABASE_URL - skipping PG backup' -ForegroundColor Red Write-Host ' Could not parse DATABASE_URL - skipping PG backup' -ForegroundColor Red
@ -74,7 +91,7 @@ function Invoke-LocalPgDump {
$env:PGPASSWORD = [Uri]::UnescapeDataString($rx.Groups['pass'].Value) $env:PGPASSWORD = [Uri]::UnescapeDataString($rx.Groups['pass'].Value)
$pgUser = $rx.Groups['user'].Value $pgUser = $rx.Groups['user'].Value
$pgHost = $rx.Groups['host'].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 $pgDb = $rx.Groups['db'].Value
$pgDump = "pg_dump" $pgDump = "pg_dump"

View File

@ -5,11 +5,11 @@
# zbackup_and_sync.ps1 — run backups, then sync the backups folder offsite. # zbackup_and_sync.ps1 — run backups, then sync the backups folder offsite.
# #
# Usage: # Usage:
# zbackup_and_sync.ps1 # backup everything + sync
# zbackup_and_sync.ps1 <project> [<project> ...] # zbackup_and_sync.ps1 <project> [<project> ...]
# zbackup_and_sync.ps1 all # backup everything + sync
# #
# Scheduled Task example (see setup_backup_schedule.ps1): # Scheduled Task example (see setup_backup_schedule.ps1):
# powershell -ExecutionPolicy Bypass -NoProfile -File "<scriptsRoot>\zbackup_and_sync.ps1" # powershell -ExecutionPolicy Bypass -NoProfile -File "<scriptsRoot>\zbackup_and_sync.ps1" all
param( param(
[Parameter(Position = 0, ValueFromRemainingArguments = $true)] [Parameter(Position = 0, ValueFromRemainingArguments = $true)]
@ -21,6 +21,18 @@ $ScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Definition
. (Join-Path $ScriptRoot "ZHelpers.ps1") . (Join-Path $ScriptRoot "ZHelpers.ps1")
Start-ZTracking 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 <project> [<project> ...] | 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 ""
Write-Host "============================================" -ForegroundColor Cyan Write-Host "============================================" -ForegroundColor Cyan
Write-Host " Backup & Sync - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -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 Write-Host "[1/2] Running backups..." -ForegroundColor Yellow
$backupPath = Join-Path $ScriptRoot "zbackup.ps1" $backupPath = Join-Path $ScriptRoot "zbackup.ps1"
if ($Projects.Count -gt 0) { & powershell -NoProfile -File $backupPath @Projects
& powershell -NoProfile -File $backupPath @Projects
} else {
& powershell -NoProfile -File $backupPath
}
$backupExitCode = $LASTEXITCODE $backupExitCode = $LASTEXITCODE
if ($backupExitCode -ne 0) { if ($backupExitCode -ne 0) {