From 9a0e940da483e120422621261b24d1c777b50a66 Mon Sep 17 00:00:00 2001 From: Daniel Covington Date: Thu, 20 Aug 2026 13:31:51 -0400 Subject: [PATCH] Add build/deploy pipeline for the IIS production server - .gitattributes + an explicit allow-list in build-release.ps1 assemble a clean release tree (app/, core/, public/, scripts/, db/migrations only) - anything not on the allow-list is deleted from the deploy, so a stray file added later can't ship to production by accident. - public/web.config.production.template holds the production appSettings (DB path outside the deploy dir, Environment=Production, error logging on). - deploy-iis.ps1 builds, zips, and scp's a release to the IIS host, then runs deploy-iis-remote-apply.ps1 there over ssh to swap in the new files, run migrations, and restart the site/app pool - the DB and error log live outside the deploy directory so a redeploy never touches them. - Removed deploy-iis-git.ps1 and migrate_isbusiness_to_households.vbs, leftovers from a different template project that didn't apply here. Co-Authored-By: Claude Sonnet 5 --- .gitattributes | 15 + .gitignore | 8 +- public/web.config.production.template | 114 +++++++ scripts/build-release.ps1 | 89 +++++ scripts/deploy-iis-git.ps1 | 324 ------------------- scripts/deploy-iis-remote-apply.ps1 | 87 +++++ scripts/deploy-iis.ps1 | 115 +++++++ scripts/migrate_isbusiness_to_households.vbs | 90 ------ 8 files changed, 427 insertions(+), 415 deletions(-) create mode 100644 .gitattributes create mode 100644 public/web.config.production.template create mode 100644 scripts/build-release.ps1 delete mode 100644 scripts/deploy-iis-git.ps1 create mode 100644 scripts/deploy-iis-remote-apply.ps1 create mode 100644 scripts/deploy-iis.ps1 delete mode 100644 scripts/migrate_isbusiness_to_households.vbs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..27172ee --- /dev/null +++ b/.gitattributes @@ -0,0 +1,15 @@ +# Paths excluded from `git archive` (used by scripts/build-release.ps1 to assemble the +# deployable file set). Everything else - app/, core/, public/, db/migrations/, scripts/ - +# ships to production. +/.claude export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/AGENTS.md export-ignore +/CLAUDE.md export-ignore +/README.md export-ignore +/TESTING.md export-ignore +/applicationhost.config export-ignore +/run_site.cmd export-ignore +/widget export-ignore +/docs export-ignore +/tests export-ignore diff --git a/.gitignore b/.gitignore index e0f8c72..44adb21 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,10 @@ db/webdata.accdb *.laccdb # Personal Claude Code permission grants for this machine/session - not team config -.claude/settings.local.json \ No newline at end of file +.claude/settings.local.json + +# Deploy target info (SSH host/user) - environment-specific, not team config +scripts/deployinfo.txt + +# Local build/deploy scratch output +/dist \ No newline at end of file diff --git a/public/web.config.production.template b/public/web.config.production.template new file mode 100644 index 0000000..e74e448 --- /dev/null +++ b/public/web.config.production.template @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/build-release.ps1 b/scripts/build-release.ps1 new file mode 100644 index 0000000..03b4281 --- /dev/null +++ b/scripts/build-release.ps1 @@ -0,0 +1,89 @@ +<# + Builds a deployable release folder for the Purple Envelope Orders Site. + + "Build" for this Classic ASP app just means: assemble the exact file set that should ship + to production, with the production web.config swapped in. There's no compile step. + + - Archives the given git ref via `git archive`, which respects .gitattributes + export-ignore rules (drops docs/, tests/, .claude/, dev-only config, etc. - see + .gitattributes for the full list). + - Enforces $KeepTopLevel below as the authoritative allow-list: anything extracted from + the archive that isn't on this list gets deleted. This is the real safety net - a file + added to the repo root later and never added to .gitattributes would otherwise ship to + production by default. Here, it has to be explicitly added to $KeepTopLevel to ship. + - Overwrites public/web.config in the archive with public/web.config.production.template. + - Leaves the assembled tree in -OutDir, ready to zip and ship. + + Usage: + powershell -File scripts\build-release.ps1 + powershell -File scripts\build-release.ps1 -Ref master -OutDir dist\release +#> + +param( + [string]$Ref = 'HEAD', + [string]$OutDir = '' +) + +$ErrorActionPreference = 'Stop' + +# Everything the live site needs to run: app code, framework internals, the IIS root, and +# 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') +$KeepUnderDb = @('migrations') + +$repoRoot = Split-Path $PSScriptRoot -Parent +Push-Location $repoRoot +try { + if([string]::IsNullOrWhiteSpace($OutDir)){ + $OutDir = Join-Path $repoRoot ('dist\release_' + (Get-Date -Format 'yyyyMMdd_HHmmss')) + } + if(Test-Path $OutDir){ + Remove-Item -Recurse -Force $OutDir + } + New-Item -ItemType Directory -Force -Path $OutDir | Out-Null + + $templatePath = Join-Path $repoRoot 'public\web.config.production.template' + if(!(Test-Path $templatePath)){ + throw "public\web.config.production.template not found - can't build a production release without it." + } + + Write-Host "Archiving $Ref (honoring .gitattributes export-ignore rules)..." + $archivePath = Join-Path $OutDir '_archive.zip' + git archive --format=zip --output $archivePath $Ref + if($LASTEXITCODE -ne 0){ throw 'git archive failed' } + + Expand-Archive -Path $archivePath -DestinationPath $OutDir -Force + Remove-Item $archivePath + + Write-Host "Pruning to the allow-list: $($KeepTopLevel -join ', '), db\$($KeepUnderDb -join ', db\')" + Get-ChildItem -Path $OutDir -Force | ForEach-Object { + if($_.Name -notin $KeepTopLevel){ + Write-Host " removing (not on allow-list): $($_.Name)" + Remove-Item -Recurse -Force $_.FullName + } + } + $dbDir = Join-Path $OutDir 'db' + if(Test-Path $dbDir){ + Get-ChildItem -Path $dbDir -Force | ForEach-Object { + if($_.Name -notin $KeepUnderDb){ + Write-Host " removing (not on allow-list): db\$($_.Name)" + Remove-Item -Recurse -Force $_.FullName + } + } + } + + if(!(Test-Path (Join-Path $OutDir 'public'))){ + throw "Release has no public\ folder after pruning - check `$KeepTopLevel in this script" + } + + $liveConfig = Join-Path $OutDir 'public\web.config' + Copy-Item $templatePath $liveConfig -Force + Remove-Item (Join-Path $OutDir 'public\web.config.production.template') -ErrorAction SilentlyContinue + + Write-Host "Release built at $OutDir" + Write-Host " public\web.config swapped in from web.config.production.template" +} finally { + Pop-Location +} diff --git a/scripts/deploy-iis-git.ps1 b/scripts/deploy-iis-git.ps1 deleted file mode 100644 index 3326f8a..0000000 --- a/scripts/deploy-iis-git.ps1 +++ /dev/null @@ -1,324 +0,0 @@ -<# - Deploy asp-territory to an existing IIS site, locally or over SSH. - - Remote mode: - - Copies this script to the remote Windows host with scp - - Executes it remotely via ssh in -RunRemoteCore mode - - Preserves the remote site's current DB path unless -DbPath is passed - - Can run standard migrations and an optional legacy migration script - - Local / remote core behavior: - - Infers IIS site/app pool/work dir from the existing site when possible - - Stops the site/app pool while deploying - - Clones/pulls and hard-resets to origin/ - - Points IIS at \public - - Reapplies the effective DB path in public\web.config - - Grants IIS AppPool rights to the DB folder - - Runs migrations - - Restarts the site/app pool and smoke tests key routes -#> - -param( - [string]$Repo = 'git@onefortheroadgit.sytes.net:dcovington/asp-classic-unified-framework.git', - [string]$Branch = 'main', - [string]$SiteName = 'ttasp', - [string]$AppPool = '', - [string]$WorkDir = '', - [string]$PublicDir = '', - [string]$BaseUrl = '', - [string]$DbPath = '', - - [switch]$RunMigrations = $true, - [switch]$SkipLegacyIsBusinessMigration, - [string]$LegacyMigrationScript = 'scripts\migrate_isbusiness_to_households.vbs', - - [switch]$UseRemoteSsh, - [string]$RemoteTarget = '', - [int]$RemotePort = 22, - [string]$SshExe = 'ssh', - [string]$ScpExe = 'scp', - [switch]$RunRemoteCore -) - -$ErrorActionPreference = 'Stop' - -function Ensure-Dir { - param([string]$Path) - if([string]::IsNullOrWhiteSpace($Path)){ return } - if(!(Test-Path $Path)){ - New-Item -ItemType Directory -Force -Path $Path | Out-Null - } -} - -function Ensure-Command { - param([string]$Name) - if(!(Get-Command $Name -ErrorAction SilentlyContinue)){ - throw "$Name not found on PATH" - } -} - -function Get-DefaultRemoteTargetFromInfo { - $infoPath = Join-Path $PSScriptRoot 'depolyinfo.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() -} - -function ConvertTo-PowerShellLiteral { - param([AllowNull()][string]$Value) - if($null -eq $Value){ return "''" } - return "'" + ($Value -replace "'", "''") + "'" -} - -function ConvertTo-CmdDoubleQuoted { - param([AllowNull()][string]$Value) - if($null -eq $Value){ return '""' } - return '"' + ($Value -replace '"', '""') + '"' -} - -function Get-DataSourceFromConfig { - param([string]$ConfigPath) - if(!(Test-Path $ConfigPath)){ return '' } - - $raw = Get-Content $ConfigPath -Raw - $match = [regex]::Match($raw, 'Data Source=([^;]+);', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) - if($match.Success){ - return $match.Groups[1].Value.Trim() - } - - return '' -} - -function Set-DataSourceInConfig { - param( - [string]$ConfigPath, - [string]$EffectiveDbPath - ) - - if(!(Test-Path $ConfigPath)){ return } - - $raw = Get-Content $ConfigPath -Raw - $updated = [regex]::Replace( - $raw, - 'Data Source=[^;]*;', - ('Data Source=' + $EffectiveDbPath + ';'), - [System.Text.RegularExpressions.RegexOptions]::IgnoreCase - ) - - if($updated -ne $raw){ - Set-Content -Path $ConfigPath -Value $updated -Encoding UTF8 - Write-Host "Updated ConnectionString Data Source to $EffectiveDbPath" - } -} - -function Get-BaseUrlFromSite { - param($Site) - - $httpBind = $Site.Bindings.Collection | Where-Object { $_.protocol -eq 'http' } | Select-Object -First 1 - if($httpBind){ - $parts = $httpBind.bindingInformation.Split(':') - $port = $parts[1] - if([string]::IsNullOrWhiteSpace($port)){ $port = '80' } - return ('http://127.0.0.1:' + $port) - } - - return 'http://127.0.0.1' -} - -function Invoke-DeployCore { - Ensure-Command git - Import-Module WebAdministration - - $site = Get-Website -Name $SiteName - if(!$site){ throw "IIS site not found: $SiteName" } - - if([string]::IsNullOrWhiteSpace($AppPool)){ - $AppPool = $site.applicationPool - } - - if([string]::IsNullOrWhiteSpace($PublicDir)){ - $PublicDir = $site.physicalPath - } - - if([string]::IsNullOrWhiteSpace($WorkDir)){ - $pd = [Environment]::ExpandEnvironmentVariables($PublicDir) - $pd = $pd.Trim().Trim('"') - $pd = $pd.TrimEnd('\','/') - - if((Split-Path $pd -Leaf).ToLower() -eq 'public'){ - $WorkDir = Split-Path $pd -Parent - } else { - $WorkDir = $pd - } - } - - if([string]::IsNullOrWhiteSpace($BaseUrl)){ - $BaseUrl = Get-BaseUrlFromSite -Site $site - } - - $currentPublicDir = $PublicDir - $currentConfigPath = Join-Path $currentPublicDir 'web.config' - $effectiveDbPath = $DbPath - if([string]::IsNullOrWhiteSpace($effectiveDbPath)){ - $effectiveDbPath = Get-DataSourceFromConfig -ConfigPath $currentConfigPath - } - - if([string]::IsNullOrWhiteSpace($effectiveDbPath)){ - throw 'No database path was provided and no existing Data Source could be read from the current web.config' - } - - Write-Host "Stopping IIS site $SiteName and app pool $AppPool" - try { Stop-Website -Name $SiteName } catch { } - try { Stop-WebAppPool -Name $AppPool } catch { } - - Ensure-Dir (Split-Path $WorkDir -Parent) - if((Test-Path $WorkDir) -and !(Test-Path (Join-Path $WorkDir '.git'))){ - $bak = ($WorkDir.TrimEnd('\') + '_pre_git_' + (Get-Date -Format 'yyyyMMdd_HHmmss')) - Write-Host "Existing non-git folder detected. Moving to $bak" - Move-Item -Force $WorkDir $bak - } - - if(!(Test-Path $WorkDir)){ - Write-Host "Cloning $Repo -> $WorkDir" - git clone $Repo $WorkDir - } - - Push-Location $WorkDir - try { - Write-Host "Updating to origin/$Branch" - git fetch origin - git checkout $Branch - & git reset --hard ("origin/" + $Branch) - } finally { - Pop-Location - } - - if((Split-Path $WorkDir -Leaf).ToLower() -eq 'public'){ - $WorkDir = Split-Path $WorkDir -Parent - } - - $PublicDir = Join-Path $WorkDir 'public' - $cfg = Join-Path $PublicDir 'web.config' - - Set-ItemProperty ('IIS:\Sites\' + $SiteName) -Name physicalPath -Value $PublicDir - Set-ItemProperty ('IIS:\Sites\' + $SiteName) -Name applicationPool -Value $AppPool - Set-ItemProperty ('IIS:\AppPools\' + $AppPool) -Name processModel.identityType -Value NetworkService - - Set-DataSourceInConfig -ConfigPath $cfg -EffectiveDbPath $effectiveDbPath - - $dbFolder = Split-Path $effectiveDbPath -Parent - if(!(Test-Path $dbFolder)){ - Ensure-Dir $dbFolder - } - icacls $dbFolder /grant ("IIS AppPool\" + $AppPool + ":(OI)(CI)(M)") /T | Out-Null - - Push-Location $WorkDir - try { - if($RunMigrations){ - Write-Host 'Running standard migrations' - cscript //nologo scripts\runMigrations.vbs up - } - - if(-not $SkipLegacyIsBusinessMigration){ - $legacyPath = Join-Path $WorkDir $LegacyMigrationScript - if(!(Test-Path $legacyPath)){ - throw "Legacy migration script not found: $legacyPath" - } - - Write-Host 'Running legacy IsBusiness migration' - cscript //nologo $legacyPath $effectiveDbPath - } - } finally { - Pop-Location - } - - if((Get-WebAppPoolState -Name $AppPool).Value -eq 'Started'){ - Restart-WebAppPool -Name $AppPool - } else { - Start-WebAppPool -Name $AppPool - } - Start-Website $SiteName - - Start-Sleep -Seconds 1 - - $paths = @('/','/territories','/households','/householder-names') - foreach($path in $paths){ - $url = $BaseUrl + $path - $response = Invoke-WebRequest -UseBasicParsing -Uri $url -TimeoutSec 30 - Write-Host ("OK " + $path + ' -> ' + $response.StatusCode) - } - - Write-Host 'Deploy complete.' -} - -if($UseRemoteSsh -and !$RunRemoteCore -and [string]::IsNullOrWhiteSpace($RemoteTarget)){ - $RemoteTarget = Get-DefaultRemoteTargetFromInfo -} - -if($UseRemoteSsh -and !$RunRemoteCore -and -not [string]::IsNullOrWhiteSpace($RemoteTarget)){ - Ensure-Command $SshExe - Ensure-Command $ScpExe - - $remoteScriptPath = 'C:\Windows\Temp\deploy-test-territory-git.ps1' - $scpDestination = "${RemoteTarget}:C:/Windows/Temp/deploy-test-territory-git.ps1" - - Write-Host "Copying deploy script to $RemoteTarget" - & $ScpExe -P $RemotePort $PSCommandPath $scpDestination - if($LASTEXITCODE -ne 0){ throw 'scp failed' } - - $remoteCommand = New-Object System.Collections.Generic.List[string] - @( - 'powershell', - '-NoProfile', - '-ExecutionPolicy', 'Bypass', - '-File', (ConvertTo-CmdDoubleQuoted $remoteScriptPath), - '-RunRemoteCore', - '-Repo', (ConvertTo-CmdDoubleQuoted $Repo), - '-Branch', (ConvertTo-CmdDoubleQuoted $Branch), - '-SiteName', (ConvertTo-CmdDoubleQuoted $SiteName) - ) | ForEach-Object { [void]$remoteCommand.Add($_) } - - if(-not [string]::IsNullOrWhiteSpace($AppPool)){ - [void]$remoteCommand.Add('-AppPool') - [void]$remoteCommand.Add((ConvertTo-CmdDoubleQuoted $AppPool)) - } - - if(-not [string]::IsNullOrWhiteSpace($WorkDir)){ - [void]$remoteCommand.Add('-WorkDir') - [void]$remoteCommand.Add((ConvertTo-CmdDoubleQuoted $WorkDir)) - } - - if(-not [string]::IsNullOrWhiteSpace($PublicDir)){ - [void]$remoteCommand.Add('-PublicDir') - [void]$remoteCommand.Add((ConvertTo-CmdDoubleQuoted $PublicDir)) - } - - if(-not [string]::IsNullOrWhiteSpace($BaseUrl)){ - [void]$remoteCommand.Add('-BaseUrl') - [void]$remoteCommand.Add((ConvertTo-CmdDoubleQuoted $BaseUrl)) - } - - if(-not [string]::IsNullOrWhiteSpace($DbPath)){ - [void]$remoteCommand.Add('-DbPath') - [void]$remoteCommand.Add((ConvertTo-CmdDoubleQuoted $DbPath)) - } - - if(-not [string]::IsNullOrWhiteSpace($LegacyMigrationScript)){ - [void]$remoteCommand.Add('-LegacyMigrationScript') - [void]$remoteCommand.Add((ConvertTo-CmdDoubleQuoted $LegacyMigrationScript)) - } - - if($RunMigrations){ $remoteCommand += '-RunMigrations' } - if($SkipLegacyIsBusinessMigration){ $remoteCommand += '-SkipLegacyIsBusinessMigration' } - - Write-Host "Executing remote deploy on $RemoteTarget" - & $SshExe -p $RemotePort $RemoteTarget ($remoteCommand -join ' ') - if($LASTEXITCODE -ne 0){ throw 'remote deploy failed' } - - exit 0 -} - -Invoke-DeployCore diff --git a/scripts/deploy-iis-remote-apply.ps1 b/scripts/deploy-iis-remote-apply.ps1 new file mode 100644 index 0000000..604e4a8 --- /dev/null +++ b/scripts/deploy-iis-remote-apply.ps1 @@ -0,0 +1,87 @@ +<# + Runs ON the IIS server (invoked over SSH by scripts\deploy-iis.ps1). Not meant to be run + by hand except for troubleshooting a stuck deploy. + + - Stops the site/app pool + - Wipes RemoteDir and re-extracts the release zip into it (safe: the DB and error log + live outside RemoteDir, at DbPath/ErrorLogDir, so a full wipe never touches them) + - Points IIS at RemoteDir\public + - Ensures the app pool identity has modify rights on the persistent data folder + - Runs pending migrations (32-bit cscript - ACE OLEDB is x86-only) + - Restarts the site/app pool +#> + +param( + [Parameter(Mandatory = $true)][string]$ZipPath, + [Parameter(Mandatory = $true)][string]$RemoteDir, + [Parameter(Mandatory = $true)][string]$SiteName, + [Parameter(Mandatory = $true)][string]$AppPool, + [Parameter(Mandatory = $true)][string]$DbPath, + [switch]$RunMigrations = $true +) + +$ErrorActionPreference = 'Stop' +Import-Module WebAdministration + +if(!(Get-Website -Name $SiteName -ErrorAction SilentlyContinue)){ + throw "IIS site '$SiteName' does not exist yet. Create the site and app pool once manually (or via a one-time setup script) before running this deploy." +} +if(!(Test-Path ('IIS:\AppPools\' + $AppPool))){ + throw "App pool '$AppPool' does not exist yet. Create it once manually before running this deploy." +} + +Write-Host "Stopping IIS site $SiteName and app pool $AppPool" +try { Stop-Website -Name $SiteName } catch { } +try { Stop-WebAppPool -Name $AppPool } catch { } + +if(Test-Path $RemoteDir){ + Write-Host "Wiping $RemoteDir" + Remove-Item -Recurse -Force (Join-Path $RemoteDir '*') -ErrorAction SilentlyContinue +} else { + New-Item -ItemType Directory -Force -Path $RemoteDir | Out-Null +} + +Write-Host "Extracting $ZipPath -> $RemoteDir" +Expand-Archive -Path $ZipPath -DestinationPath $RemoteDir -Force +Remove-Item $ZipPath -ErrorAction SilentlyContinue + +$publicDir = Join-Path $RemoteDir 'public' +if(!(Test-Path $publicDir)){ + throw "No public\ folder in the extracted release - deploy aborted, site left stopped for inspection." +} + +# Persistent data folder (DB + error log) - lives outside RemoteDir so it survives the wipe +# above. Create it and grant the app pool identity modify rights. +$dataDir = Split-Path $DbPath -Parent +if(!(Test-Path $dataDir)){ + New-Item -ItemType Directory -Force -Path $dataDir | Out-Null +} +$logDir = Join-Path $dataDir 'logs' +if(!(Test-Path $logDir)){ + New-Item -ItemType Directory -Force -Path $logDir | Out-Null +} +icacls $dataDir /grant ("IIS AppPool\" + $AppPool + ":(OI)(CI)(M)") /T | Out-Null + +Set-ItemProperty ('IIS:\Sites\' + $SiteName) -Name physicalPath -Value $publicDir +Set-ItemProperty ('IIS:\Sites\' + $SiteName) -Name applicationPool -Value $AppPool + +if($RunMigrations){ + Write-Host 'Running pending migrations' + Push-Location $RemoteDir + try { + & C:\Windows\SysWOW64\cscript.exe //nologo scripts\runMigrations.vbs up + if($LASTEXITCODE -ne 0){ throw 'runMigrations.vbs up failed - see output above' } + } finally { + Pop-Location + } +} + +Write-Host "Starting IIS site $SiteName and app pool $AppPool" +if((Get-WebAppPoolState -Name $AppPool).Value -eq 'Started'){ + Restart-WebAppPool -Name $AppPool +} else { + Start-WebAppPool -Name $AppPool +} +Start-Website $SiteName + +Write-Host 'Remote apply complete.' diff --git a/scripts/deploy-iis.ps1 b/scripts/deploy-iis.ps1 new file mode 100644 index 0000000..e654f26 --- /dev/null +++ b/scripts/deploy-iis.ps1 @@ -0,0 +1,115 @@ +<# + Deploys the Purple Envelope Orders Site to the production IIS server. + + Flow: + 1. Build a release locally (scripts\build-release.ps1) - a clean file tree with the + production web.config swapped in, and dev-only files excluded via .gitattributes. + 2. Zip it and scp the zip to the remote host. + 3. scp scripts\deploy-iis-remote-apply.ps1 to the remote host and run it over ssh - it + wipes the deploy directory, extracts the new release, points IIS at it, runs + migrations, and restarts the site/app pool. The database and error log live outside + the deploy directory, so the wipe never touches them. + 4. Smoke-test a few routes over HTTPS from this machine. + + Usage: + powershell -File scripts\deploy-iis.ps1 + powershell -File scripts\deploy-iis.ps1 -Ref master + + Remote target defaults to the "ssh user@host" line in scripts\deployinfo.txt (gitignored, + not committed - create it locally, e.g.: ssh daniel_admin@kci-uluto-web). +#> + +param( + [string]$Ref = 'HEAD', + [string]$RemoteTarget = '', + [int]$RemotePort = 22, + [string]$RemoteDir = 'C:\inetpub\wwwroot\Purple_Envelop_Order_Site', + [string]$SiteName = 'PurpleEnvelopes', + [string]$AppPool = 'PurpleEnvelopes', + [string]$DbPath = 'C:\inetpub\data\webdata.accdb', + [string]$BaseUrl = 'https://pe.kentcommunications.com/', + [switch]$RunMigrations = $true, + [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 +Ensure-Command git + +# --- 1. Build --- +$outDir = Join-Path $repoRoot ('dist\release_' + (Get-Date -Format 'yyyyMMdd_HHmmss')) +& (Join-Path $PSScriptRoot 'build-release.ps1') -Ref $Ref -OutDir $outDir +if($LASTEXITCODE -ne 0 -and $LASTEXITCODE -ne $null){ throw 'build-release.ps1 failed' } + +# --- 2. Zip and ship it --- +$zipName = 'release_' + (Get-Date -Format 'yyyyMMdd_HHmmss') + '.zip' +$localZip = Join-Path (Split-Path $outDir -Parent) $zipName +Compress-Archive -Path (Join-Path $outDir '*') -DestinationPath $localZip -Force + +$remoteZip = 'C:\Windows\Temp\' + $zipName +$remoteApplyScript = Join-Path $PSScriptRoot 'deploy-iis-remote-apply.ps1' +$remoteApplyDest = 'C:\Windows\Temp\deploy-iis-remote-apply.ps1' + +Write-Host "Copying release to $RemoteTarget" +& $ScpExe -P $RemotePort $localZip "${RemoteTarget}:$remoteZip" +if($LASTEXITCODE -ne 0){ throw 'scp of release zip failed' } + +& $ScpExe -P $RemotePort $remoteApplyScript "${RemoteTarget}:$remoteApplyDest" +if($LASTEXITCODE -ne 0){ throw 'scp of remote-apply script failed' } + +# --- 3. Apply on the remote host --- +$remoteCommandParts = @( + 'powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', + '-File', ('"' + $remoteApplyDest + '"'), + '-ZipPath', ('"' + $remoteZip + '"'), + '-RemoteDir', ('"' + $RemoteDir + '"'), + '-SiteName', ('"' + $SiteName + '"'), + '-AppPool', ('"' + $AppPool + '"'), + '-DbPath', ('"' + $DbPath + '"') +) +if($RunMigrations){ $remoteCommandParts += '-RunMigrations' } + +Write-Host "Applying release on $RemoteTarget" +& $SshExe -p $RemotePort $RemoteTarget ($remoteCommandParts -join ' ') +if($LASTEXITCODE -ne 0){ throw 'remote apply failed' } + +# --- 4. Local cleanup --- +Remove-Item $localZip -ErrorAction SilentlyContinue +Remove-Item -Recurse -Force $outDir -ErrorAction SilentlyContinue + +# --- 5. Smoke test --- +Write-Host 'Smoke testing...' +$paths = @('/', '/request-order', '/404') +foreach($path in $paths){ + $url = $BaseUrl.TrimEnd('/') + $path + $response = Invoke-WebRequest -UseBasicParsing -Uri $url -TimeoutSec 30 + Write-Host ("OK " + $path + ' -> ' + $response.StatusCode) +} + +Write-Host 'Deploy complete.' diff --git a/scripts/migrate_isbusiness_to_households.vbs b/scripts/migrate_isbusiness_to_households.vbs deleted file mode 100644 index 5f0a8b9..0000000 --- a/scripts/migrate_isbusiness_to_households.vbs +++ /dev/null @@ -1,90 +0,0 @@ -' migrate_isbusiness_to_households.vbs -' Moves IsBusiness from HouseholderNames to Households. -' -' Usage: -' cscript //nologo scripts\migrate_isbusiness_to_households.vbs "C:\path\to\myAccessFile.accdb" -' -' What it does: -' 1) Adds Households.IsBusiness (SMALLINT) if missing -' 2) Copies data: sets Households.IsBusiness=1 if any related HouseholderNames.IsBusiness<>0 -' 3) Sets NULLs to 0 -' 4) Drops HouseholderNames.IsBusiness if present -' -Option Explicit - -Dim dbPath -If WScript.Arguments.Count < 1 Then - WScript.Echo "ERROR: missing db path." - WScript.Echo "Usage: cscript //nologo scripts\migrate_isbusiness_to_households.vbs ""C:\path\to\db.accdb""" - WScript.Quit 1 -End If - -dbPath = WScript.Arguments(0) - -Dim conn -Set conn = CreateObject("ADODB.Connection") -conn.Open "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & dbPath & ";Persist Security Info=False;" - -On Error Resume Next - -If Not ColumnExists(conn, "Households", "IsBusiness") Then - Exec conn, "ALTER TABLE [Households] ADD COLUMN [IsBusiness] SMALLINT" - If Err.Number <> 0 Then - WScript.Echo "ERROR adding Households.IsBusiness: " & Err.Description - WScript.Quit 1 - End If - WScript.Echo "Added Households.IsBusiness" -Else - WScript.Echo "Households.IsBusiness already exists" -End If - -' Copy data (only if the old column exists) -If ColumnExists(conn, "HouseholderNames", "IsBusiness") Then - ' Normalize all existing households first so the column is never left NULL. - Exec conn, "UPDATE [Households] SET [IsBusiness]=0" - If Err.Number <> 0 Then - WScript.Echo "ERROR initializing Households.IsBusiness: " & Err.Description - WScript.Quit 1 - End If - - ' Promote households to business when any related name was previously marked as a business. - Exec conn, "UPDATE [Households] SET [IsBusiness]=1 WHERE [Id] IN (SELECT [HouseholdId] FROM [HouseholderNames] WHERE [IsBusiness]<>0)" - If Err.Number <> 0 Then - WScript.Echo "ERROR copying IsBusiness to Households: " & Err.Description - WScript.Quit 1 - End If - WScript.Echo "Copied IsBusiness values to Households" - - Exec conn, "ALTER TABLE [HouseholderNames] DROP COLUMN [IsBusiness]" - If Err.Number <> 0 Then - WScript.Echo "ERROR dropping HouseholderNames.IsBusiness: " & Err.Description - WScript.Quit 1 - End If - WScript.Echo "Dropped HouseholderNames.IsBusiness" -Else - WScript.Echo "HouseholderNames.IsBusiness does not exist; nothing to drop" -End If - -conn.Close -Set conn = Nothing -WScript.Echo "Done." - -' --- helpers --- -Sub Exec(c, sql) - Err.Clear - c.Execute sql -End Sub - -Function ColumnExists(c, tableName, colName) - Dim rs - ColumnExists = False - Err.Clear - Set rs = c.OpenSchema(4, Array(Empty, Empty, tableName, colName)) ' adSchemaColumns=4 - If Err.Number <> 0 Then - Err.Clear - Exit Function - End If - If Not rs.EOF Then ColumnExists = True - rs.Close - Set rs = Nothing -End Function