|
- [CmdletBinding()]
- param(
- [Parameter(Mandatory = $true)][string]$ArchivePath,
- [Parameter(Mandatory = $true)][string]$ProjectPath,
- [Parameter(Mandatory = $true)][string]$InvocationId,
- [Parameter(Mandatory = $true)]
- [ValidatePattern('^[0-9A-Fa-f]{64}$')]
- [string]$ExpectedArchiveSha256,
- [Parameter(Mandatory = $true)][string]$ProductionSiteName,
- [Parameter(Mandatory = $true)][string]$ProductionPoolName,
- [Parameter(Mandatory = $true)][int]$ProductionPort,
- [Parameter(Mandatory = $true)][string]$ProductionBaseUrl,
- [Parameter(Mandatory = $true)][string]$TestSiteName,
- [Parameter(Mandatory = $true)][string]$TestPoolName,
- [Parameter(Mandatory = $true)][int]$TestPort,
- [Parameter(Mandatory = $true)][string]$TestBaseUrl,
- [switch]$RunTests
- )
-
- $ErrorActionPreference = 'Stop'
- Set-StrictMode -Version 2.0
-
- function Quote-PowerShellLiteral {
- param([Parameter(Mandatory = $true)][string]$Value)
- return "'" + $Value.Replace("'", "''") + "'"
- }
-
- function Invoke-WindowsPowerShell {
- param([Parameter(Mandatory = $true)][string]$Command)
- $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($Command))
- $process = Start-Process -FilePath 'powershell.exe' -ArgumentList @(
- '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', $encoded
- ) -Wait -PassThru -NoNewWindow
- return $process.ExitCode
- }
-
- function Assert-Administrator {
- $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
- $principal = New-Object Security.Principal.WindowsPrincipal($identity)
- if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
- throw 'Remote installation requires an elevated Windows account.'
- }
- }
-
- function Get-SiteSnapshot {
- param([string]$Name)
- if (-not (Test-Path "IIS:\Sites\$Name")) { return $null }
- $site = Get-Website -Name $Name
- $parentPaths = (Get-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' -Location $Name -Filter 'system.webServer/asp' -Name 'enableParentPaths').Value
- return [PSCustomObject]@{
- Name = $Name
- State = [string]$site.State
- PhysicalPath = [string]$site.PhysicalPath
- ApplicationPool = [string]$site.ApplicationPool
- EnableParentPaths = [bool]$parentPaths
- }
- }
-
- function Assert-SiteTarget {
- param(
- [string]$Name,
- [string]$PoolName,
- [string]$PhysicalPath,
- [int]$Port
- )
-
- $snapshot = Get-SiteSnapshot -Name $Name
- if ($snapshot) {
- if ([IO.Path]::GetFullPath($snapshot.PhysicalPath).TrimEnd('\') -ne [IO.Path]::GetFullPath($PhysicalPath).TrimEnd('\')) {
- throw "Existing site '$Name' has physical path '$($snapshot.PhysicalPath)', expected '$PhysicalPath'. Refusing to adopt or rewrite it."
- }
- if ($snapshot.ApplicationPool -ne $PoolName) {
- throw "Existing site '$Name' uses app pool '$($snapshot.ApplicationPool)', expected '$PoolName'. Refusing to adopt it."
- }
- $httpBinding = @(Get-WebBinding -Name $Name -Protocol http | Where-Object { $_.bindingInformation -match ":$Port`:" })
- if ($httpBinding.Count -ne 1) {
- throw "Existing site '$Name' must have exactly one HTTP binding on port $Port."
- }
- }
-
- $conflictingSiteNames = @()
- foreach ($otherSite in @(Get-Website | Where-Object { $_.Name -ne $Name })) {
- $matchingBindings = @(Get-WebBinding -Name $otherSite.Name -Protocol http | Where-Object { $_.bindingInformation -match ":$Port`:" })
- if ($matchingBindings.Count -gt 0) { $conflictingSiteNames += $otherSite.Name }
- }
- if ($conflictingSiteNames.Count -gt 0) {
- throw "HTTP port $Port is already used by unrelated site(s): $($conflictingSiteNames -join ', ')."
- }
- }
-
- function Assert-PoolTarget {
- param([string]$PoolName, [string]$AllowedSiteName)
- if (-not (Test-Path "IIS:\AppPools\$PoolName")) { return }
- $otherUsers = @(Get-Website | Where-Object { $_.ApplicationPool -eq $PoolName -and $_.Name -ne $AllowedSiteName })
- if ($otherUsers.Count -gt 0) {
- throw "App pool '$PoolName' is used by unrelated site(s): $($otherUsers.Name -join ', ')."
- }
- }
-
- function Get-ComponentRegistrationMode {
- param(
- [Parameter(Mandatory = $true)][string]$ExistingProjectPath,
- [Parameter(Mandatory = $true)][object[]]$Components
- )
-
- $absentCount = 0
- $ownedCount = 0
- foreach ($component in $Components) {
- $progIdKey = "HKLM:\SOFTWARE\Classes\$($component.ProgId)"
- $clsidKey = "HKLM:\SOFTWARE\Classes\CLSID\$($component.ClassId)"
- $progIdExists = Test-Path -LiteralPath $progIdKey
- $clsidExists = Test-Path -LiteralPath $clsidKey
-
- if (-not $progIdExists -and -not $clsidExists) {
- $absentCount++
- continue
- }
- if (-not $progIdExists -or -not $clsidExists) {
- throw "Component registration is partial for '$($component.ProgId)'. Refusing to overwrite or repair registry state that this invocation does not own."
- }
-
- $registeredClassId = (Get-ItemProperty -LiteralPath (Join-Path $progIdKey 'CLSID')).'(default)'
- if ($registeredClassId -ne $component.ClassId) {
- throw "ProgID '$($component.ProgId)' maps to '$registeredClassId', not this project's CLSID '$($component.ClassId)'."
- }
-
- $scriptletKey = Join-Path $clsidKey 'ScriptletURL'
- if (-not (Test-Path -LiteralPath $scriptletKey)) {
- throw "CLSID '$($component.ClassId)' has no ScriptletURL. Refusing to overwrite it."
- }
- $registeredPath = [string](Get-ItemProperty -LiteralPath $scriptletKey).'(default)'
- $expectedPath = [IO.Path]::GetFullPath((Join-Path $ExistingProjectPath $component.RelativePath))
- $registeredFilePath = $null
- if (-not [string]::IsNullOrWhiteSpace($registeredPath)) {
- $registeredUri = $null
- if ([Uri]::TryCreate($registeredPath, [UriKind]::Absolute, [ref]$registeredUri) -and $registeredUri.IsFile) {
- $registeredFilePath = [IO.Path]::GetFullPath($registeredUri.LocalPath)
- } elseif ([IO.Path]::IsPathRooted($registeredPath)) {
- $registeredFilePath = [IO.Path]::GetFullPath($registeredPath)
- }
- }
- if (-not $registeredFilePath -or $registeredFilePath.TrimEnd('\') -ne $expectedPath.TrimEnd('\')) {
- throw "CLSID '$($component.ClassId)' is registered from '$registeredPath', not the existing deployment path '$expectedPath'."
- }
- $ownedCount++
- }
-
- if ($absentCount -eq $Components.Count) { return 'Absent' }
- if ($ownedCount -eq $Components.Count) { return 'Owned' }
-
- # An upgrade may introduce a component that did not exist in the currently
- # deployed tree. Accept only that additive case: every existing registration
- # was proven project-owned above, and every absent registration must also
- # have no corresponding component file in the old release. An absent
- # registration for an old file remains a suspicious partial state.
- foreach ($component in $Components) {
- $progIdKey = "HKLM:\SOFTWARE\Classes\$($component.ProgId)"
- $clsidKey = "HKLM:\SOFTWARE\Classes\CLSID\$($component.ClassId)"
- if (-not (Test-Path -LiteralPath $progIdKey) -and -not (Test-Path -LiteralPath $clsidKey)) {
- $oldComponentPath = Join-Path $ExistingProjectPath $component.RelativePath
- if (Test-Path -LiteralPath $oldComponentPath) {
- throw "Component registration for existing file '$($component.RelativePath)' is absent. Refusing to repair a partial registration state."
- }
- }
- }
- return 'Owned'
- }
-
- function Restore-SiteState {
- param($Snapshot)
- if (-not $Snapshot -or -not (Test-Path "IIS:\Sites\$($Snapshot.Name)")) { return }
- $parentPathsValue = if ($Snapshot.EnableParentPaths) { 'True' } else { 'False' }
- $appcmd = "$env:windir\system32\inetsrv\appcmd.exe"
- & $appcmd set config $Snapshot.Name -section:system.webServer/asp "/enableParentPaths:$parentPathsValue" /commit:apphost | Out-Null
- if ($LASTEXITCODE -ne 0) { throw "Unable to restore enableParentPaths for site '$($Snapshot.Name)'." }
- if ($Snapshot.State -eq 'Started') {
- Start-Website -Name $Snapshot.Name -ErrorAction SilentlyContinue
- } else {
- Stop-Website -Name $Snapshot.Name -ErrorAction SilentlyContinue
- }
- }
-
- function Wait-WebAppPoolState {
- param(
- [Parameter(Mandatory = $true)][string]$Name,
- [Parameter(Mandatory = $true)][string]$DesiredState,
- [int]$TimeoutSeconds = 30
- )
- $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
- do {
- $current = [string](Get-WebAppPoolState -Name $Name).Value
- if ($current -eq $DesiredState) { return }
- Start-Sleep -Milliseconds 250
- } while ((Get-Date) -lt $deadline)
- throw "App pool '$Name' did not reach state '$DesiredState' within $TimeoutSeconds seconds (current: $current)."
- }
-
- function Sync-DirectoryTree {
- param(
- [Parameter(Mandatory = $true)][string]$Source,
- [Parameter(Mandatory = $true)][string]$Destination
- )
-
- if (-not (Test-Path -LiteralPath $Source -PathType Container)) {
- throw "Directory sync source does not exist: $Source"
- }
-
- & robocopy.exe $Source $Destination /MIR /COPY:DAT /DCOPY:DAT /R:2 /W:1 /NFL /NDL /NJH /NJS /NP
- $exitCode = $LASTEXITCODE
- if ($exitCode -ge 8) {
- throw "robocopy failed while mirroring '$Source' to '$Destination' (exit code $exitCode)."
- }
- }
-
- function Restore-PoolState {
- param([string]$Name, [string]$State)
- if (-not $State -or -not (Test-Path "IIS:\AppPools\$Name")) { return }
- $current = [string](Get-WebAppPoolState -Name $Name).Value
- if ($State -eq 'Started') {
- if ($current -eq 'Stopping') {
- Wait-WebAppPoolState -Name $Name -DesiredState 'Stopped'
- $current = 'Stopped'
- }
- if ($current -ne 'Started') { Start-WebAppPool -Name $Name }
- Wait-WebAppPoolState -Name $Name -DesiredState 'Started'
- } else {
- if ($current -eq 'Starting') {
- Wait-WebAppPoolState -Name $Name -DesiredState 'Started'
- $current = 'Started'
- }
- if ($current -ne 'Stopped') { Stop-WebAppPool -Name $Name }
- Wait-WebAppPoolState -Name $Name -DesiredState 'Stopped'
- }
- }
-
- Assert-Administrator
- Import-Module WebAdministration
-
- if (-not (Test-Path -LiteralPath $ArchivePath -PathType Leaf)) { throw "Deployment archive not found: $ArchivePath" }
- $actualArchiveSha256 = (Get-FileHash -LiteralPath $ArchivePath -Algorithm SHA256).Hash
- if ($actualArchiveSha256 -ne $ExpectedArchiveSha256) {
- throw "Deployment archive SHA-256 mismatch (expected $ExpectedArchiveSha256, got $actualArchiveSha256)."
- }
- Write-Output "Verified package SHA-256: $($actualArchiveSha256.ToLowerInvariant())"
- if (-not [IO.Path]::IsPathRooted($ProjectPath)) { throw 'ProjectPath must be an absolute Windows path.' }
- if ($ProductionSiteName -eq $TestSiteName -or $ProductionPoolName -eq $TestPoolName) { throw 'Production and test IIS names must differ.' }
- if ($ProductionPort -eq $TestPort) { throw 'Production and test ports must differ.' }
-
- $projectFullPath = [IO.Path]::GetFullPath($ProjectPath).TrimEnd('\')
- $projectParent = Split-Path -Parent $projectFullPath
- if (-not $projectParent -or $projectFullPath -eq [IO.Path]::GetPathRoot($projectFullPath).TrimEnd('\')) {
- throw "Unsafe ProjectPath: $ProjectPath"
- }
- $projectParentExisted = Test-Path -LiteralPath $projectParent -PathType Container
- $targetExisted = Test-Path -LiteralPath $projectFullPath
- if ($targetExisted) {
- $targetItem = Get-Item -LiteralPath $projectFullPath -Force
- if (-not $targetItem.PSIsContainer) { throw "ProjectPath exists but is not a directory: $projectFullPath" }
- if (($targetItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw "ProjectPath may not be a reparse point: $projectFullPath" }
- if (-not (Test-Path -LiteralPath (Join-Path $projectFullPath 'tools\Register-Components.ps1'))) {
- throw 'Existing ProjectPath is not a rollback-capable WSC-MVC deployment.'
- }
- }
-
- $stagingPath = "$projectFullPath.staging.$InvocationId"
- $backupPath = "$projectFullPath.rollback.$InvocationId"
- $failedPath = "$projectFullPath.failed.$InvocationId"
- foreach ($ownedPath in @($stagingPath, $backupPath, $failedPath)) {
- if (Test-Path -LiteralPath $ownedPath) { throw "Invocation-owned path already exists: $ownedPath" }
- }
-
- $productionPublicPath = Join-Path $projectFullPath 'public'
- $testPublicPath = Join-Path $projectFullPath 'test-app\public'
- $components = @(
- [PSCustomObject]@{ RelativePath = 'Framework\RequestContext.wsc'; ProgId = 'WscMvc.RequestContext'; ClassId = '{1C36FA55-34DF-4974-94B9-D657389362B2}' },
- [PSCustomObject]@{ RelativePath = 'Framework\Router.wsc'; ProgId = 'WscMvc.Router'; ClassId = '{C92F9338-B478-4EAD-B865-892FFB1E1C51}' },
- [PSCustomObject]@{ RelativePath = 'Framework\ViewRenderer.wsc'; ProgId = 'WscMvc.ViewRenderer'; ClassId = '{4948DF84-5DC6-448A-9F1B-EB596C28842B}' },
- [PSCustomObject]@{ RelativePath = 'Framework\Database.wsc'; ProgId = 'WscMvc.Database'; ClassId = '{E3F7C1A2-9B4D-4E6F-8C3A-2D5B7A9E1F04}' },
- [PSCustomObject]@{ RelativePath = 'Framework\Application.wsc'; ProgId = 'WscMvc.Application'; ClassId = '{851C7763-1638-42FE-A166-BF3DD3A96A88}' },
- [PSCustomObject]@{ RelativePath = 'Controllers\HomeController.wsc'; ProgId = 'WscMvc.HomeController'; ClassId = '{87488446-60BE-4068-8368-0B709BB68F3F}' },
- [PSCustomObject]@{ RelativePath = 'test-app\Controllers\SelfTestController.wsc'; ProgId = 'WscMvc.SelfTestController'; ClassId = '{D2634944-4646-4C55-956E-4C05E7E10904}' }
- )
-
- # Validate every archive entry before Expand-Archive can write anything.
- Add-Type -AssemblyName System.IO.Compression.FileSystem
- $zip = [IO.Compression.ZipFile]::OpenRead($ArchivePath)
- try {
- foreach ($entry in $zip.Entries) {
- $entryName = $entry.FullName.Replace('/', '\')
- if ([IO.Path]::IsPathRooted($entryName) -or $entryName -match '(^|\\)\.\.(\\|$)') {
- throw "Unsafe archive entry: $($entry.FullName)"
- }
- $entryDestination = [IO.Path]::GetFullPath((Join-Path $stagingPath $entryName))
- if (-not $entryDestination.StartsWith($stagingPath + '\', [StringComparison]::OrdinalIgnoreCase)) {
- throw "Archive entry escapes staging directory: $($entry.FullName)"
- }
- }
- } finally {
- $zip.Dispose()
- }
-
- # Read-only filesystem and IIS inspection completes before any target, registration, site, or pool mutation.
- $productionSnapshot = Get-SiteSnapshot -Name $ProductionSiteName
- $testSnapshot = Get-SiteSnapshot -Name $TestSiteName
- $productionPoolExisted = Test-Path "IIS:\AppPools\$ProductionPoolName"
- $testPoolExisted = Test-Path "IIS:\AppPools\$TestPoolName"
- $productionPoolState = if ($productionPoolExisted) { [string](Get-WebAppPoolState -Name $ProductionPoolName).Value } else { $null }
- $testPoolState = if ($testPoolExisted) { [string](Get-WebAppPoolState -Name $TestPoolName).Value } else { $null }
- Assert-SiteTarget -Name $ProductionSiteName -PoolName $ProductionPoolName -PhysicalPath $productionPublicPath -Port $ProductionPort
- Assert-SiteTarget -Name $TestSiteName -PoolName $TestPoolName -PhysicalPath $testPublicPath -Port $TestPort
- Assert-PoolTarget -PoolName $ProductionPoolName -AllowedSiteName $ProductionSiteName
- Assert-PoolTarget -PoolName $TestPoolName -AllowedSiteName $TestSiteName
- $componentRegistrationMode = Get-ComponentRegistrationMode -ExistingProjectPath $projectFullPath -Components $components
-
- $createdProductionSite = -not [bool]$productionSnapshot
- $createdTestSite = -not [bool]$testSnapshot
- $createdProductionPool = -not $productionPoolExisted
- $createdTestPool = -not $testPoolExisted
- $targetUpdated = $false
- $backupCreated = $false
- $installSucceeded = $false
-
- try {
- if (-not $projectParentExisted) {
- New-Item -ItemType Directory -Path $projectParent | Out-Null
- }
- Expand-Archive -LiteralPath $ArchivePath -DestinationPath $stagingPath
-
- $requiredRelativePaths = @(
- 'public\Default.asp',
- 'test-app\public\Default.asp',
- 'Framework\ViewRenderer.wsc',
- 'Framework\Database.wsc',
- 'tools\Register-Components.ps1',
- 'tools\Setup-Site.ps1',
- 'tests\Invoke-SelfTest.ps1',
- 'tests\Test-Http.ps1'
- )
- foreach ($relativePath in $requiredRelativePaths) {
- if (-not (Test-Path -LiteralPath (Join-Path $stagingPath $relativePath))) {
- throw "Package is missing required path: $relativePath"
- }
- }
-
- Get-ChildItem -LiteralPath $stagingPath -Recurse -Force | ForEach-Object {
- $resolved = [IO.Path]::GetFullPath($_.FullName)
- if (-not $resolved.StartsWith($stagingPath + '\', [StringComparison]::OrdinalIgnoreCase)) {
- throw "Archive entry escaped staging directory: $resolved"
- }
- if (($_.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
- throw "Archive contains a reparse point: $resolved"
- }
- }
-
- # Stop only this deployment's two sites and dedicated pools before copying
- # the release. The live root itself can remain open on IIS hosts even after
- # its dedicated pools stop, so keep the path stable and mirror a verified
- # backup/release tree instead of relying on a root-directory rename.
- foreach ($siteSnapshot in @($productionSnapshot, $testSnapshot)) {
- if ($siteSnapshot -and $siteSnapshot.State -eq 'Started') {
- Stop-Website -Name $siteSnapshot.Name
- }
- }
- foreach ($poolName in @($ProductionPoolName, $TestPoolName)) {
- if (Test-Path "IIS:\AppPools\$poolName") {
- $poolStateNow = [string](Get-WebAppPoolState -Name $poolName).Value
- if ($poolStateNow -eq 'Started') { Stop-WebAppPool -Name $poolName }
- Wait-WebAppPoolState -Name $poolName -DesiredState 'Stopped'
- }
- }
-
- if ($targetExisted) {
- Sync-DirectoryTree -Source $projectFullPath -Destination $backupPath
- $backupCreated = $true
- }
- Sync-DirectoryTree -Source $stagingPath -Destination $projectFullPath
- $targetUpdated = $true
- Remove-Item -LiteralPath $stagingPath -Recurse -Force
-
- & (Join-Path $projectFullPath 'tools\Register-Components.ps1') -ProjectRoot $projectFullPath
- & (Join-Path $projectFullPath 'tools\Setup-Site.ps1') -SiteName $ProductionSiteName -PoolName $ProductionPoolName -PhysicalPath $productionPublicPath -Port $ProductionPort
- & (Join-Path $projectFullPath 'tools\Setup-Site.ps1') -SiteName $TestSiteName -PoolName $TestPoolName -PhysicalPath $testPublicPath -Port $TestPort
-
- if ($RunTests) {
- & cscript.exe //nologo (Join-Path $projectFullPath 'tests\Test-Components.vbs')
- if ($LASTEXITCODE -ne 0) { throw "Test-Components.vbs failed with exit code $LASTEXITCODE" }
-
- $httpTestCommand = "& $(Quote-PowerShellLiteral (Join-Path $projectFullPath 'tests\Test-Http.ps1')) -BaseUrl $(Quote-PowerShellLiteral $ProductionBaseUrl) -TestBaseUrl $(Quote-PowerShellLiteral $TestBaseUrl)"
- $httpTestExitCode = Invoke-WindowsPowerShell -Command $httpTestCommand
- if ($httpTestExitCode -ne 0) { throw "Test-Http.ps1 failed with exit code $httpTestExitCode" }
-
- $apiTestCommand = "& $(Quote-PowerShellLiteral (Join-Path $projectFullPath 'tests\Invoke-SelfTest.ps1')) -BaseUrl $(Quote-PowerShellLiteral $TestBaseUrl)"
- $apiTestExitCode = Invoke-WindowsPowerShell -Command $apiTestCommand
- if ($apiTestExitCode -ne 0) { throw "Invoke-SelfTest.ps1 failed with exit code $apiTestExitCode" }
- }
-
- $installSucceeded = $true
- Write-Output "Deployment succeeded: $projectFullPath"
- if ($backupCreated) {
- Write-Output "Timestamped rollback retained at: $backupPath"
- } else {
- Write-Output 'No previous project tree existed; no rollback directory was created.'
- }
- } catch {
- $failure = $_
- $rollbackErrors = New-Object System.Collections.Generic.List[string]
- Write-Warning "Deployment failed; rolling back only changes owned by invocation $InvocationId."
-
- try {
- if ($targetUpdated -and (Test-Path -LiteralPath (Join-Path $projectFullPath 'tools\Unregister-Components.ps1'))) {
- & (Join-Path $projectFullPath 'tools\Unregister-Components.ps1') -ProjectRoot $projectFullPath
- }
- } catch { $rollbackErrors.Add("Unregister new components: $($_.Exception.Message)") }
-
- try {
- if ($createdProductionSite -and (Test-Path "IIS:\Sites\$ProductionSiteName")) { Remove-Website -Name $ProductionSiteName }
- } catch { $rollbackErrors.Add("Remove production site: $($_.Exception.Message)") }
- try {
- if ($createdTestSite -and (Test-Path "IIS:\Sites\$TestSiteName")) { Remove-Website -Name $TestSiteName }
- } catch { $rollbackErrors.Add("Remove test site: $($_.Exception.Message)") }
- try {
- if ($createdProductionPool -and (Test-Path "IIS:\AppPools\$ProductionPoolName")) { Remove-WebAppPool -Name $ProductionPoolName }
- } catch { $rollbackErrors.Add("Remove production pool: $($_.Exception.Message)") }
- try {
- if ($createdTestPool -and (Test-Path "IIS:\AppPools\$TestPoolName")) { Remove-WebAppPool -Name $TestPoolName }
- } catch { $rollbackErrors.Add("Remove test pool: $($_.Exception.Message)") }
-
- try {
- if ($targetUpdated -and (Test-Path -LiteralPath $projectFullPath)) {
- Sync-DirectoryTree -Source $projectFullPath -Destination $failedPath
- }
- } catch { $rollbackErrors.Add("Retain failed release: $($_.Exception.Message)") }
- try {
- if ($backupCreated -and (Test-Path -LiteralPath $backupPath)) {
- Sync-DirectoryTree -Source $backupPath -Destination $projectFullPath
- } elseif ($targetUpdated -and -not $targetExisted -and (Test-Path -LiteralPath $projectFullPath)) {
- Remove-Item -LiteralPath $projectFullPath -Recurse -Force
- }
- } catch { $rollbackErrors.Add("Restore previous project tree: $($_.Exception.Message)") }
- try {
- if ($backupCreated -and $componentRegistrationMode -eq 'Owned' -and
- (Test-Path -LiteralPath (Join-Path $projectFullPath 'tools\Register-Components.ps1'))) {
- & (Join-Path $projectFullPath 'tools\Register-Components.ps1') -ProjectRoot $projectFullPath
- }
- } catch { $rollbackErrors.Add("Restore previous component registrations: $($_.Exception.Message)") }
-
- try { Restore-PoolState -Name $ProductionPoolName -State $productionPoolState } catch { $rollbackErrors.Add("Restore production pool state: $($_.Exception.Message)") }
- try { Restore-PoolState -Name $TestPoolName -State $testPoolState } catch { $rollbackErrors.Add("Restore test pool state: $($_.Exception.Message)") }
- try { Restore-SiteState -Snapshot $productionSnapshot } catch { $rollbackErrors.Add("Restore production site state: $($_.Exception.Message)") }
- try { Restore-SiteState -Snapshot $testSnapshot } catch { $rollbackErrors.Add("Restore test site state: $($_.Exception.Message)") }
-
- if ($rollbackErrors.Count -gt 0) {
- throw "Deployment failed: $($failure.Exception.Message) Rollback also reported: $($rollbackErrors -join ' | ')"
- }
- throw $failure
- } finally {
- if (-not $installSucceeded -and (Test-Path -LiteralPath $stagingPath)) {
- # Staging was created by this invocation and never became the live target.
- Remove-Item -LiteralPath $stagingPath -Recurse -Force
- }
- if (-not $installSucceeded -and -not $projectParentExisted -and (Test-Path -LiteralPath $projectParent)) {
- $remaining = @(Get-ChildItem -LiteralPath $projectParent -Force)
- if ($remaining.Count -eq 0) { Remove-Item -LiteralPath $projectParent -Force }
- }
- if (Test-Path -LiteralPath $ArchivePath) {
- Remove-Item -LiteralPath $ArchivePath -Force
- }
- $selfPath = $MyInvocation.MyCommand.Path
- if ($selfPath -and $selfPath -like "$env:windir\Temp\*") {
- Remove-Item -LiteralPath $selfPath -Force -ErrorAction SilentlyContinue
- }
- }
|