<# 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