From 1a517f606d5cf7452febe017cd24de1fd3ddcee2 Mon Sep 17 00:00:00 2001 From: Daniel Covington Date: Fri, 21 Aug 2026 09:34:10 -0400 Subject: [PATCH] Fix build-release.ps1 dropping db/migrations from every release The top-level prune step deleted the whole db/ folder before the second step meant to keep db/migrations under it ever ran, so no deploy has actually shipped migrations - the remote apply step was running runMigrations.vbs against an empty migrations folder and silently reporting nothing pending. Also adds scripts/run-migrations-remote(-apply).ps1, a lighter migrations-only counterpart to deploy-iis.ps1 for catching a production schema up without redeploying app code - used just now to patch prod after this bug meant it never received prior migrations. Co-Authored-By: Claude Sonnet 5 --- scripts/build-release.ps1 | 2 +- scripts/run-migrations-remote-apply.ps1 | 45 +++++++++++ scripts/run-migrations-remote.ps1 | 103 ++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 scripts/run-migrations-remote-apply.ps1 create mode 100644 scripts/run-migrations-remote.ps1 diff --git a/scripts/build-release.ps1 b/scripts/build-release.ps1 index 03b4281..c65f054 100644 --- a/scripts/build-release.ps1 +++ b/scripts/build-release.ps1 @@ -30,7 +30,7 @@ $ErrorActionPreference = 'Stop' # the migration runner (+ its generator siblings, harmless to ship alongside it). Only # db\migrations is kept under db - never db\webdata.accdb, even if something ever put a # stray copy there. -$KeepTopLevel = @('app', 'core', 'public', 'scripts') +$KeepTopLevel = @('app', 'core', 'public', 'scripts', 'db') $KeepUnderDb = @('migrations') $repoRoot = Split-Path $PSScriptRoot -Parent diff --git a/scripts/run-migrations-remote-apply.ps1 b/scripts/run-migrations-remote-apply.ps1 new file mode 100644 index 0000000..b752035 --- /dev/null +++ b/scripts/run-migrations-remote-apply.ps1 @@ -0,0 +1,45 @@ +<# + Runs ON the IIS server (invoked over SSH by scripts\run-migrations-remote.ps1). Not meant + to be run by hand except for troubleshooting. + + Applies pending migrations to the live database WITHOUT touching the deployed site files: + - Copies the live public\web.config from RemoteDir into WorkDir, so runMigrations.vbs picks + up the real production connection string without it being hard-coded here. + - Runs `runMigrations.vbs status` (for the log) then `up` from WorkDir. + - Deletes WorkDir when done, win or lose - nothing is left behind on the server. +#> + +param( + [Parameter(Mandatory = $true)][string]$RemoteDir, + [Parameter(Mandatory = $true)][string]$WorkDir +) + +$ErrorActionPreference = 'Stop' + +$liveWebConfig = Join-Path $RemoteDir 'public\web.config' +if(!(Test-Path $liveWebConfig)){ + throw "No deployed web.config found at $liveWebConfig - is the site actually deployed at -RemoteDir?" +} + +$workPublicDir = Join-Path $WorkDir 'public' +New-Item -ItemType Directory -Force -Path $workPublicDir | Out-Null +Copy-Item $liveWebConfig (Join-Path $workPublicDir 'web.config') -Force + +try { + Push-Location $WorkDir + Write-Host '-- status before --' + & cscript.exe //nologo scripts\runMigrations.vbs status + if($LASTEXITCODE -ne 0){ throw 'runMigrations.vbs status failed - see output above' } + + Write-Host '' + Write-Host '-- applying pending migrations --' + & cscript.exe //nologo scripts\runMigrations.vbs up + if($LASTEXITCODE -ne 0){ throw 'runMigrations.vbs up failed - see output above' } +} finally { + Pop-Location + Write-Host '' + Write-Host "Cleaning up $WorkDir" + Remove-Item -Recurse -Force $WorkDir -ErrorAction SilentlyContinue +} + +Write-Host 'Remote migration apply complete.' diff --git a/scripts/run-migrations-remote.ps1 b/scripts/run-migrations-remote.ps1 new file mode 100644 index 0000000..504f445 --- /dev/null +++ b/scripts/run-migrations-remote.ps1 @@ -0,0 +1,103 @@ +<# + Applies pending db\migrations\*.asp to the production database WITHOUT redeploying the + site - useful when the app code on the server is already current but the schema has + drifted (e.g. a migration was added after the last full deploy, or - as happened once - + build-release.ps1 was silently dropping db\migrations\ from every release). + + Flow: + 1. scp scripts\runMigrations.vbs and db\migrations\*.asp to a throwaway work folder in + C:\Windows\Temp on the remote host (never touches the live site's own deploy directory). + 2. scp scripts\run-migrations-remote-apply.ps1 and run it over ssh - it copies the site's + already-deployed public\web.config into the work folder (so it uses the real production + connection string), runs `runMigrations.vbs status` then `up`, and deletes the work + folder afterward. + + For a normal deploy (which also ships db\migrations and runs migrations automatically), + use scripts\deploy-iis.ps1 instead. + + Usage: + powershell -File scripts\run-migrations-remote.ps1 + + Remote target defaults to the "ssh user@host" line in scripts\deployinfo.txt (gitignored), + same as deploy-iis.ps1. +#> + +param( + [string]$RemoteTarget = '', + [int]$RemotePort = 22, + [string]$RemoteDir = 'C:\inetpub\wwwroot\Purple_Envelop_Order_Site', + [string]$SshExe = 'ssh', + [string]$ScpExe = 'scp' +) + +$ErrorActionPreference = 'Stop' +$repoRoot = Split-Path $PSScriptRoot -Parent + +function Ensure-Command { + param([string]$Name) + if(!(Get-Command $Name -ErrorAction SilentlyContinue)){ + throw "$Name not found on PATH" + } +} + +function Get-DefaultRemoteTarget { + $infoPath = Join-Path $PSScriptRoot 'deployinfo.txt' + if(!(Test-Path $infoPath)){ return '' } + $sshLine = Get-Content $infoPath | Where-Object { $_ -match '^\s*ssh\s+' } | Select-Object -First 1 + if([string]::IsNullOrWhiteSpace($sshLine)){ return '' } + return ($sshLine -replace '^\s*ssh\s+', '').Trim() +} + +if([string]::IsNullOrWhiteSpace($RemoteTarget)){ + $RemoteTarget = Get-DefaultRemoteTarget +} +if([string]::IsNullOrWhiteSpace($RemoteTarget)){ + throw 'No -RemoteTarget given and scripts\deployinfo.txt not found. Create scripts\deployinfo.txt with a line like: ssh user@host' +} + +Ensure-Command $SshExe +Ensure-Command $ScpExe + +$AuthOpts = @( + '-o', 'PreferredAuthentications=publickey,keyboard-interactive,password', + '-o', 'NumberOfPasswordPrompts=3' +) + +# --- 1. Stage a local bundle: scripts\runMigrations.vbs + db\migrations\*.asp --- +$stageDir = Join-Path $repoRoot ('dist\migrations_stage_' + (Get-Date -Format 'yyyyMMdd_HHmmss')) +New-Item -ItemType Directory -Force -Path (Join-Path $stageDir 'scripts') | Out-Null +New-Item -ItemType Directory -Force -Path (Join-Path $stageDir 'db\migrations') | Out-Null +Copy-Item (Join-Path $repoRoot 'scripts\runMigrations.vbs') (Join-Path $stageDir 'scripts\runMigrations.vbs') -Force +Copy-Item (Join-Path $repoRoot 'db\migrations\*.asp') (Join-Path $stageDir 'db\migrations') -Force + +$remoteWorkDir = 'C:\Windows\Temp\pe-migrations-' + (Get-Date -Format 'yyyyMMdd_HHmmss') +$remoteApplyDest = 'C:\Windows\Temp\run-migrations-remote-apply.ps1' + +# --- 2. Ship the bundle and the remote-apply script --- +# A single recursive source -> a destination that doesn't exist yet makes scp create that +# destination as a copy of the source, so this also creates $remoteWorkDir - no separate +# "ssh mkdir" step needed (the remote shell is cmd.exe, not PowerShell, so raw PowerShell +# commands passed straight through ssh don't run there without an explicit powershell.exe +# wrapper; simplest to just avoid needing one here). +Write-Host "Copying migrations bundle to $RemoteTarget" +& $ScpExe -P $RemotePort @AuthOpts -r $stageDir "${RemoteTarget}:$remoteWorkDir" +if($LASTEXITCODE -ne 0){ throw 'scp of migrations bundle failed - see scp output above for the actual reason' } + +& $ScpExe -P $RemotePort @AuthOpts (Join-Path $PSScriptRoot 'run-migrations-remote-apply.ps1') "${RemoteTarget}:$remoteApplyDest" +if($LASTEXITCODE -ne 0){ throw 'scp of remote-apply script failed - see scp output above for the actual reason' } + +Remove-Item -Recurse -Force $stageDir -ErrorAction SilentlyContinue + +# --- 3. Apply on the remote host (also deletes $remoteWorkDir when done) --- +$remoteCommandParts = @( + 'powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', + '-File', ('"' + $remoteApplyDest + '"'), + '-RemoteDir', ('"' + $RemoteDir + '"'), + '-WorkDir', ('"' + $remoteWorkDir + '"') +) + +Write-Host "Applying pending migrations on $RemoteTarget" +& $SshExe -p $RemotePort @AuthOpts $RemoteTarget ($remoteCommandParts -join ' ') +if($LASTEXITCODE -ne 0){ throw 'remote migration apply failed - see ssh output above' } + +Write-Host 'Done.'