瀏覽代碼

ci: update scripts/deploy-iis-git.ps1

ci-iis-release-pipeline
Clawdbot 8 小時之前
父節點
當前提交
b3bf38cbd4
共有 1 個檔案被更改,包括 204 行新增263 行删除
  1. +204
    -263
      scripts/deploy-iis-git.ps1

+ 204
- 263
scripts/deploy-iis-git.ps1 查看文件

@@ -1,324 +1,265 @@
<#
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
.SYNOPSIS
Packages this repository and deploys it to IIS over Tailscale/OpenSSH.

.DESCRIPTION
This is the controller/CI entry point. It creates a ZIP containing the full
repository (excluding VCS and local deployment artifacts), computes SHA-256,
copies the package and host installer with scp, then invokes the installer on
the Windows host through ssh.

-DryRun performs local validation and prints the remote operations without
connecting. Gitea 1.11.4 does not run this itself; use a trusted external CI
worker, a scheduled task, or an operator workstation.
#>

[CmdletBinding()]
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 = '',
[Parameter(Mandatory = $true)]
[ValidatePattern('^[A-Za-z0-9_. -]+$')]
[string]$SiteName,

[switch]$RunMigrations = $true,
[switch]$SkipLegacyIsBusinessMigration,
[string]$LegacyMigrationScript = 'scripts\migrate_isbusiness_to_households.vbs',

[switch]$UseRemoteSsh,
[string]$RemoteTarget = '',
[string]$RemoteTarget = 'webserver-1',
[ValidateRange(1, 65535)]
[int]$RemotePort = 22,
[string]$SourcePath = (Split-Path $PSScriptRoot -Parent),
[string]$DeployRoot = '',
[string]$BaseUrl = '',
[string]$ReleaseId = '',
[string]$ExpectedBranch = 'master',
[ValidateRange(2, 100)]
[int]$KeepReleases = 5,
[string]$SshExe = 'ssh',
[string]$ScpExe = 'scp',
[switch]$RunRemoteCore
[switch]$AllowAnyBranch,
[switch]$AllowDirty,
[switch]$RunMigrations,
[switch]$SkipSmokeTest,
[switch]$RemotePreflightOnly,
[switch]$Rollback,
[string]$RollbackTo = '',
[switch]$DryRun,
[switch]$KeepPackage
)

Set-StrictMode -Version 2.0
$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 Write-Step {
param([string]$Message)
Write-Host ('==> ' + $Message)
}

function Ensure-Command {
function Assert-Command {
param([string]$Name)
if(!(Get-Command $Name -ErrorAction SilentlyContinue)){
throw "$Name not found on PATH"
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
throw "$Name was 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 ConvertTo-SingleQuotedPowerShell {
param([string]$Value)
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 {
function Add-RemoteArgument {
param(
[string]$ConfigPath,
[string]$EffectiveDbPath
[System.Collections.Generic.List[string]]$Arguments,
[string]$Name,
[string]$Value
)
$Arguments.Add($Name)
$Arguments.Add((ConvertTo-SingleQuotedPowerShell $Value))
}

if(!(Test-Path $ConfigPath)){ return }

$raw = Get-Content $ConfigPath -Raw
$updated = [regex]::Replace(
$raw,
'Data Source=[^;]*;',
('Data Source=' + $EffectiveDbPath + ';'),
[System.Text.RegularExpressions.RegexOptions]::IgnoreCase
function Copy-ReleaseSource {
param(
[string]$From,
[string]$To
)

if($updated -ne $raw){
Set-Content -Path $ConfigPath -Value $updated -Encoding UTF8
Write-Host "Updated ConnectionString Data Source to $EffectiveDbPath"
New-Item -ItemType Directory -Force -Path $To | Out-Null
$excludedNames = @('.git', '.deployment', 'releases')
Get-ChildItem -LiteralPath $From -Force | ForEach-Object {
if ($excludedNames -notcontains $_.Name) {
Copy-Item -LiteralPath $_.FullName -Destination $To -Recurse -Force
}
}
}

function Get-BaseUrlFromSite {
param($Site)
$SourcePath = [System.IO.Path]::GetFullPath($SourcePath)
$installerPath = Join-Path $PSScriptRoot 'install-iis-release.ps1'
if (-not (Test-Path -LiteralPath $installerPath -PathType Leaf)) {
throw "Host installer is missing: $installerPath"
}

$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)
$requiredSourceFiles = @(
'public\Default.asp',
'public\web.config',
'core\autoload_core.asp',
'app\controllers\autoload_controllers.asp'
)
foreach ($relativePath in $requiredSourceFiles) {
if (-not (Test-Path -LiteralPath (Join-Path $SourcePath $relativePath) -PathType Leaf)) {
throw "Source tree is incomplete; missing $relativePath"
}

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" }
try {
[xml](Get-Content -LiteralPath (Join-Path $SourcePath 'public\web.config') -Raw) | Out-Null
} catch {
throw "Source public\web.config is not valid XML: $($_.Exception.Message)"
}

if([string]::IsNullOrWhiteSpace($AppPool)){
$AppPool = $site.applicationPool
$directGitRoot = Test-Path -LiteralPath (Join-Path $SourcePath '.git')
$commit = 'nogit'
if ($directGitRoot) {
Assert-Command 'git'
$branch = (& git -C $SourcePath branch --show-current).Trim()
if ($LASTEXITCODE -ne 0) {
throw 'Could not determine the Git branch.'
}

if([string]::IsNullOrWhiteSpace($PublicDir)){
$PublicDir = $site.physicalPath
if ((-not $AllowAnyBranch) -and $branch -ne $ExpectedBranch) {
throw "Refusing to deploy branch '$branch'; expected '$ExpectedBranch'. Use -AllowAnyBranch only for an intentional exception."
}

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
}
$dirty = & git -C $SourcePath status --porcelain
if ($LASTEXITCODE -ne 0) {
throw 'Could not inspect the Git worktree.'
}

if([string]::IsNullOrWhiteSpace($BaseUrl)){
$BaseUrl = Get-BaseUrlFromSite -Site $site
if ((-not $AllowDirty) -and $null -ne $dirty -and @($dirty).Count -gt 0) {
throw 'Refusing to deploy a dirty worktree. Commit/stash changes or use -AllowDirty for an intentional, auditable exception.'
}

$currentPublicDir = $PublicDir
$currentConfigPath = Join-Path $currentPublicDir 'web.config'
$effectiveDbPath = $DbPath
if([string]::IsNullOrWhiteSpace($effectiveDbPath)){
$effectiveDbPath = Get-DataSourceFromConfig -ConfigPath $currentConfigPath
$commit = (& git -C $SourcePath rev-parse --short=12 HEAD).Trim()
if ($LASTEXITCODE -ne 0) {
throw 'Could not determine the Git commit.'
}

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 "Source branch: $branch"
Write-Host "Source commit: $commit"
} else {
Write-Warning 'SourcePath is not a standalone Git checkout; branch and dirty-worktree checks cannot be enforced.'
if (-not $AllowAnyBranch) {
throw 'Use a standalone CI checkout, or pass -AllowAnyBranch explicitly for a reviewed non-Git source tree.'
}
}

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 ([string]::IsNullOrWhiteSpace($ReleaseId)) {
$ReleaseId = (Get-Date -Format 'yyyyMMdd-HHmmss') + '-' + $commit
}
if ($ReleaseId -notmatch '^[A-Za-z0-9._-]+$') {
throw 'ReleaseId may contain only letters, numbers, dot, underscore, and hyphen.'
}
if ($Rollback -and [string]::IsNullOrWhiteSpace($RollbackTo)) {
throw '-Rollback requires -RollbackTo <release-id>.'
}
if ((-not $Rollback) -and -not [string]::IsNullOrWhiteSpace($RollbackTo)) {
throw '-RollbackTo is only valid with -Rollback.'
}
if ($RunMigrations -and $Rollback) {
throw '-RunMigrations is not valid during rollback.'
}

if(!(Test-Path $WorkDir)){
Write-Host "Cloning $Repo -> $WorkDir"
git clone $Repo $WorkDir
$workRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('asp-iis-deploy-' + [Guid]::NewGuid().ToString('N'))
$packageStage = Join-Path $workRoot 'package'
$packagePath = Join-Path $workRoot ($ReleaseId + '.zip')
$remoteDirectory = 'C:\Windows\Temp\asp-iis-deploy-' + $ReleaseId
$remotePackage = $remoteDirectory + '\' + $ReleaseId + '.zip'
$remoteInstaller = $remoteDirectory + '\install-iis-release.ps1'

try {
if (-not $Rollback -and -not $RemotePreflightOnly) {
Write-Step 'Staging the full repository for packaging'
Copy-ReleaseSource -From $SourcePath -To $packageStage
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::CreateFromDirectory(
$packageStage,
$packagePath,
[System.IO.Compression.CompressionLevel]::Optimal,
$false
)
$sha256 = (Get-FileHash -LiteralPath $packagePath -Algorithm SHA256).Hash
Write-Host "Package: $packagePath"
Write-Host "SHA-256: $sha256"
} else {
$sha256 = ''
}

Push-Location $WorkDir
try {
Write-Host "Updating to origin/$Branch"
git fetch origin
git checkout $Branch
& git reset --hard ("origin/" + $Branch)
} finally {
Pop-Location
$remoteArguments = New-Object 'System.Collections.Generic.List[string]'
$remoteArguments.Add('&')
$remoteArguments.Add((ConvertTo-SingleQuotedPowerShell $remoteInstaller))
Add-RemoteArgument -Arguments $remoteArguments -Name '-SiteName' -Value $SiteName

if ($Rollback) {
Add-RemoteArgument -Arguments $remoteArguments -Name '-RollbackTo' -Value $RollbackTo
} elseif (-not $RemotePreflightOnly) {
Add-RemoteArgument -Arguments $remoteArguments -Name '-PackagePath' -Value $remotePackage
Add-RemoteArgument -Arguments $remoteArguments -Name '-ReleaseId' -Value $ReleaseId
Add-RemoteArgument -Arguments $remoteArguments -Name '-ExpectedSha256' -Value $sha256
} else {
$remoteArguments.Add('-PreflightOnly')
}

if((Split-Path $WorkDir -Leaf).ToLower() -eq 'public'){
$WorkDir = Split-Path $WorkDir -Parent
if (-not [string]::IsNullOrWhiteSpace($DeployRoot)) {
Add-RemoteArgument -Arguments $remoteArguments -Name '-DeployRoot' -Value $DeployRoot
}

$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
if (-not [string]::IsNullOrWhiteSpace($BaseUrl)) {
Add-RemoteArgument -Arguments $remoteArguments -Name '-BaseUrl' -Value $BaseUrl
}
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
Add-RemoteArgument -Arguments $remoteArguments -Name '-KeepReleases' -Value $KeepReleases.ToString()
if ($RunMigrations) { $remoteArguments.Add('-RunMigrations') }
if ($SkipSmokeTest) { $remoteArguments.Add('-SkipSmokeTest') }

$remoteScript = $remoteArguments -join ' '
$remoteBytes = [Text.Encoding]::Unicode.GetBytes($remoteScript)
$remoteEncodedCommand = [Convert]::ToBase64String($remoteBytes)
$remoteCommand = 'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy RemoteSigned -EncodedCommand ' + $remoteEncodedCommand
Write-Host "Remote target: $RemoteTarget (Tailscale/OpenSSH port $RemotePort)"

if ($DryRun) {
Write-Step 'Dry-run complete; no network connection was made'
Write-Host "Would create remote directory: $remoteDirectory"
Write-Host "Would copy installer: $installerPath"
if (-not $Rollback -and -not $RemotePreflightOnly) {
Write-Host "Would copy package: $packagePath"
}
} finally {
Pop-Location
Write-Host ('Would execute host script: ' + $remoteScript)
Write-Host ('Transport command uses PowerShell -EncodedCommand to avoid remote-shell quoting ambiguity.')
exit 0
}

if((Get-WebAppPoolState -Name $AppPool).Value -eq 'Started'){
Restart-WebAppPool -Name $AppPool
} else {
Start-WebAppPool -Name $AppPool
}
Start-Website $SiteName
Assert-Command $SshExe
Assert-Command $ScpExe

Start-Sleep -Seconds 1
Write-Step 'Creating remote staging directory over Tailscale/OpenSSH'
$mkdirScript = "New-Item -ItemType Directory -Force -Path $(ConvertTo-SingleQuotedPowerShell $remoteDirectory) | Out-Null"
$mkdirBytes = [Text.Encoding]::Unicode.GetBytes($mkdirScript)
$mkdirEncodedCommand = [Convert]::ToBase64String($mkdirBytes)
& $SshExe -p $RemotePort $RemoteTarget ('powershell.exe -NoProfile -NonInteractive -EncodedCommand ' + $mkdirEncodedCommand)
if ($LASTEXITCODE -ne 0) { throw 'Remote directory creation failed.' }

$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-Step 'Copying the host installer'
& $ScpExe -P $RemotePort $installerPath ($RemoteTarget + ':' + $remoteDirectory.Replace('\', '/') + '/install-iis-release.ps1')
if ($LASTEXITCODE -ne 0) { throw 'Installer copy failed.' }

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 $Rollback -and -not $RemotePreflightOnly) {
Write-Step 'Copying the release package'
& $ScpExe -P $RemotePort $packagePath ($RemoteTarget + ':' + $remoteDirectory.Replace('\', '/') + '/' + $ReleaseId + '.zip')
if ($LASTEXITCODE -ne 0) { throw 'Package copy failed.' }
}

if(-not [string]::IsNullOrWhiteSpace($WorkDir)){
[void]$remoteCommand.Add('-WorkDir')
[void]$remoteCommand.Add((ConvertTo-CmdDoubleQuoted $WorkDir))
}
Write-Step 'Invoking the host-side installer'
& $SshExe -p $RemotePort $RemoteTarget $remoteCommand
if ($LASTEXITCODE -ne 0) { throw "Remote installer failed with exit code $LASTEXITCODE." }

if(-not [string]::IsNullOrWhiteSpace($PublicDir)){
[void]$remoteCommand.Add('-PublicDir')
[void]$remoteCommand.Add((ConvertTo-CmdDoubleQuoted $PublicDir))
Write-Step 'Remote operation completed successfully'
} finally {
if ($KeepPackage -and (Test-Path -LiteralPath $packagePath)) {
$keptPath = Join-Path (Get-Location) ([System.IO.Path]::GetFileName($packagePath))
Copy-Item -LiteralPath $packagePath -Destination $keptPath -Force
Write-Host "Package retained at $keptPath"
}

if(-not [string]::IsNullOrWhiteSpace($BaseUrl)){
[void]$remoteCommand.Add('-BaseUrl')
[void]$remoteCommand.Add((ConvertTo-CmdDoubleQuoted $BaseUrl))
if (Test-Path -LiteralPath $workRoot) {
Remove-Item -LiteralPath $workRoot -Recurse -Force
}

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

Loading…
取消
儲存

Powered by TurnKey Linux.