Преглед изворни кода

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 <noreply@anthropic.com>
master
Daniel Covington пре 3 дана
родитељ
комит
9a0e940da4
8 измењених фајлова са 427 додато и 415 уклоњено
  1. +15
    -0
      .gitattributes
  2. +7
    -1
      .gitignore
  3. +114
    -0
      public/web.config.production.template
  4. +89
    -0
      scripts/build-release.ps1
  5. +0
    -324
      scripts/deploy-iis-git.ps1
  6. +87
    -0
      scripts/deploy-iis-remote-apply.ps1
  7. +115
    -0
      scripts/deploy-iis.ps1
  8. +0
    -90
      scripts/migrate_isbusiness_to_households.vbs

+ 15
- 0
.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

+ 7
- 1
.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
.claude/settings.local.json
# Deploy target info (SSH host/user) - environment-specific, not team config
scripts/deployinfo.txt
# Local build/deploy scratch output
/dist

+ 114
- 0
public/web.config.production.template Прегледај датотеку

@@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

<appSettings>
<!-- Access connection string. Points at the persistent data folder, outside the
deploy directory so a redeploy (which wipes and re-extracts the deploy dir) never
touches it. -->
<add key="ConnectionString"
value="Provider=Microsoft.ACE.OLEDB.12.0;Data Source=C:\inetpub\data\webdata.accdb;Persist Security Info=False;" />

<!-- Environment flag (Development / Staging / Production) -->
<add key="Environment" value="Production" />

<!-- Flash message display duration in milliseconds -->
<add key="FlashMessageTimeout" value="3000" />

<!-- 404 error page redirect countdown in seconds -->
<add key="Error404RedirectSeconds" value="5" />

<!-- Cache expiration year for static content -->
<add key="CacheExpirationYear" value="2030" />

<!-- Maximum characters to display in table cells before truncating -->
<add key="TableCellMaxLength" value="90" />

<!-- Character threshold for textarea vs input field in forms -->
<add key="FormTextareaThreshold" value="100" />

<!-- Enable error logging to file (true/false) -->
<add key="EnableErrorLogging" value="true" />

<!-- Error log file path - also outside the deploy directory so logs survive a redeploy. -->
<add key="ErrorLogPath" value="C:\inetpub\data\logs\errors.log" />

<!-- Enable cache-busting for URLs and assets (true/false) -->
<add key="EnableCacheBusting" value="false" />

<!-- Cache-bust parameter name (default: "v") -->
<add key="CacheBustParamName" value="v" />

<!-- SMTP settings used by CDOEmail for outbound order emails. -->
<add key="SmtpServer" value="kentcommunications-com.mail.protection.outlook.com" />
<add key="SmtpPort" value="25" />
<add key="SmtpUsername" value="" />
<add key="SmtpPassword" value="" />
<add key="SmtpUseSSL" value="false" />
<add key="SmtpFromAddress" value="no-reply@kentcommunications.com" />

<!-- Hours before an order continuation link (token) expires -->
<add key="OrderTokenExpirationHours" value="24" />

<!--
Jurisdiction number validation (request-order form). The list is essentially static
reference data, so it's cached in Application scope rather than fetched on every
submission - see JurisdictionCacheMinutes.
-->
<add key="JurisdictionApiUrl" value="http://192.168.1.40:8081/api/jurisdictions" />
<add key="JurisdictionCacheMinutes" value="60" />

<!-- Cloudflare Turnstile (CAPTCHA) keys. -->
<add key="TurnstileSiteKey" value="0x4AAAAAAEUPEPHEnA9hPgbw" />
<add key="TurnstileSecretKey" value="0x4AAAAAAEUPEFnbhCe6ttr_Mq6yCozxRkg" />
</appSettings>

<system.webServer>
<!-- Default document for the site root -->
<defaultDocument>
<files>
<clear />
<add value="Default.asp" />
</files>
</defaultDocument>

<!-- URL Rewrite: send everything through Default.asp except static assets -->
<rewrite>
<rules>
<rule name="Rewrite to Default.asp" stopProcessing="true">
<match url="^(?!Default\.asp$|css/|js/|images/|aspunit/|favicon\.ico$).*$" />

<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>

<action type="Rewrite" url="/Default.asp" />
</rule>
</rules>
</rewrite>

<!-- Strip the Server response header (reveals IIS version) - IIS 10+ only -->
<security>
<requestFiltering removeServerHeader="true" />
</security>

<!-- Strip framework-identifying headers that IIS/ASP.NET modules may add -->
<httpProtocol>
<customHeaders>
<remove name="X-Powered-By" />
<remove name="X-AspNet-Version" />
<remove name="X-AspNetMvc-Version" />
<!-- URL Rewrite module adds this in IIS Express, leaking the physical file path -->
<remove name="X-SourceFiles" />
</customHeaders>
</httpProtocol>
</system.webServer>

<location path="css">
<system.webServer>
<staticContent>
<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="01:00:00" />
</staticContent>
</system.webServer>
</location>
</configuration>

+ 89
- 0
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
}

+ 0
- 324
scripts/deploy-iis-git.ps1 Прегледај датотеку

@@ -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/<Branch>
- Points IIS at <WorkDir>\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

+ 87
- 0
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.'

+ 115
- 0
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.'

+ 0
- 90
scripts/migrate_isbusiness_to_households.vbs Прегледај датотеку

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

Loading…
Откажи
Сачувај

Powered by TurnKey Linux.