Consolidated ASP Classic MVC framework from best components
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

478 lines
21KB

  1. <#
  2. .SYNOPSIS
  3. Installs or rolls back an immutable release in a dedicated IIS site/app pool.
  4. .DESCRIPTION
  5. The complete repository is retained in each immutable release and IIS serves
  6. only <release>\public. The script never discovers or adopts another site.
  7. A missing dedicated target is created only after package extraction, layout,
  8. and XML validation succeed. Host preflight is read-only and does not require
  9. the dedicated target to exist.
  10. #>
  11. [CmdletBinding(DefaultParameterSetName = 'Deploy')]
  12. param(
  13. [ValidatePattern('^[A-Za-z0-9_. -]+$')]
  14. [string]$SiteName = 'AspClassicUnifiedFramework',
  15. [ValidatePattern('^[A-Za-z0-9_. -]+$')]
  16. [string]$AppPoolName = 'AspClassicUnifiedFramework',
  17. [string]$DeployRoot = 'D:\Deployments\AspClassicUnifiedFramework',
  18. [string]$BindingIpAddress = '100.97.39.23',
  19. [ValidateRange(1, 65535)]
  20. [int]$BindingPort = 8085,
  21. [AllowEmptyString()]
  22. [string]$HostHeader = '',
  23. [string]$InitialWebConfigPath = '',
  24. [Parameter(ParameterSetName = 'Deploy')]
  25. [string]$PackagePath = '',
  26. [Parameter(ParameterSetName = 'Deploy')]
  27. [ValidatePattern('^[A-Za-z0-9._-]+$')]
  28. [string]$ReleaseId = (Get-Date -Format 'yyyyMMdd-HHmmss'),
  29. [Parameter(Mandatory = $true, ParameterSetName = 'Rollback')]
  30. [ValidatePattern('^[A-Za-z0-9._-]+$')]
  31. [string]$RollbackTo,
  32. [string]$BaseUrl = '',
  33. [ValidateRange(2, 100)]
  34. [int]$KeepReleases = 5,
  35. [string]$ExpectedSha256 = '',
  36. [switch]$RunMigrations,
  37. [switch]$SkipSmokeTest,
  38. [switch]$PreflightOnly,
  39. [switch]$DryRun
  40. )
  41. Set-StrictMode -Version 2.0
  42. $ErrorActionPreference = 'Stop'
  43. function Write-Step { param([string]$Message) Write-Host ('==> ' + $Message) }
  44. function Get-NormalizedPath {
  45. param([string]$Path)
  46. return [System.IO.Path]::GetFullPath([Environment]::ExpandEnvironmentVariables($Path)).TrimEnd('\')
  47. }
  48. function Assert-Administrator {
  49. $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
  50. $principal = New-Object Security.Principal.WindowsPrincipal($identity)
  51. if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
  52. throw 'An elevated Administrator PowerShell session is required.'
  53. }
  54. }
  55. function Assert-SafeNameValue {
  56. param([string]$Name, [string]$Value)
  57. if ([string]::IsNullOrWhiteSpace($Value) -or $Value -notmatch '^[A-Za-z0-9_. -]+$') {
  58. throw "$Name contains unsupported characters."
  59. }
  60. if ($Value -match '(?i)schedulicious') {
  61. throw "$Name must not identify a Schedulicious resource."
  62. }
  63. }
  64. function Assert-DeploymentInputs {
  65. Assert-SafeNameValue -Name 'SiteName' -Value $SiteName
  66. Assert-SafeNameValue -Name 'AppPoolName' -Value $AppPoolName
  67. if ($DeployRoot -match '(?i)schedulicious') { throw 'DeployRoot must not reference Schedulicious.' }
  68. if ($HostHeader -match '(?i)schedulicious') { throw 'HostHeader must not reference Schedulicious.' }
  69. if ($HostHeader -match '[:/\\]') {
  70. throw 'HostHeader must be empty or a DNS host name without a scheme, port, slash, or backslash.'
  71. }
  72. $parsedAddress = $null
  73. if (-not [System.Net.IPAddress]::TryParse($BindingIpAddress, [ref]$parsedAddress)) {
  74. throw "BindingIpAddress is not a valid IP address: $BindingIpAddress"
  75. }
  76. }
  77. function Assert-ReleaseLayout {
  78. param([string]$ReleasePath)
  79. $required = @(
  80. 'public\Default.asp',
  81. 'public\web.config',
  82. 'core\autoload_core.asp',
  83. 'app\controllers\autoload_controllers.asp',
  84. 'scripts\install-iis-release.ps1'
  85. )
  86. foreach ($relativePath in $required) {
  87. if (-not (Test-Path -LiteralPath (Join-Path $ReleasePath $relativePath) -PathType Leaf)) {
  88. throw "Release is incomplete; missing $relativePath"
  89. }
  90. }
  91. foreach ($xmlFile in Get-ChildItem -LiteralPath $ReleasePath -Recurse -Force -Filter 'web.config') {
  92. try {
  93. [xml](Get-Content -LiteralPath $xmlFile.FullName -Raw) | Out-Null
  94. } catch {
  95. throw "$($xmlFile.FullName) is not valid XML: $($_.Exception.Message)"
  96. }
  97. }
  98. }
  99. function Assert-ArchiveEntries {
  100. param([string]$ZipPath)
  101. Add-Type -AssemblyName System.IO.Compression.FileSystem
  102. $archive = [System.IO.Compression.ZipFile]::OpenRead($ZipPath)
  103. try {
  104. foreach ($entry in $archive.Entries) {
  105. $name = $entry.FullName.Replace('/', '\')
  106. if ([System.IO.Path]::IsPathRooted($name) -or $name -match '(^|\\)\.\.(\\|$)') {
  107. throw "Package contains an unsafe path: $($entry.FullName)"
  108. }
  109. }
  110. } finally {
  111. $archive.Dispose()
  112. }
  113. }
  114. function Get-BindingInformation {
  115. return $BindingIpAddress + ':' + $BindingPort + ':' + $HostHeader
  116. }
  117. function Assert-HostCapabilities {
  118. $getWindowsFeature = Get-Command Get-WindowsFeature -ErrorAction SilentlyContinue
  119. $getOptionalFeature = Get-Command Get-WindowsOptionalFeature -ErrorAction SilentlyContinue
  120. if ($null -ne $getWindowsFeature) {
  121. $aspFeature = Get-WindowsFeature -Name Web-ASP
  122. if ($null -eq $aspFeature -or -not $aspFeature.Installed) { throw 'The IIS Classic ASP feature (Web-ASP) is not installed.' }
  123. } elseif ($null -ne $getOptionalFeature) {
  124. $aspFeature = Get-WindowsOptionalFeature -Online -FeatureName IIS-ASP -ErrorAction SilentlyContinue
  125. if ($null -eq $aspFeature -or $aspFeature.State -ne 'Enabled') { throw 'The IIS-ASP Windows feature is not enabled.' }
  126. } else {
  127. throw 'Classic ASP feature state cannot be verified: no supported Windows feature cmdlet is available.'
  128. }
  129. if ($null -eq (Get-WebGlobalModule -Name AspModule -ErrorAction SilentlyContinue)) {
  130. throw 'The IIS Classic ASP module (AspModule) was not found.'
  131. }
  132. if ($null -eq (Get-WebGlobalModule -Name RewriteModule -ErrorAction SilentlyContinue)) {
  133. throw 'IIS URL Rewrite is not installed (RewriteModule was not found).'
  134. }
  135. }
  136. function Assert-DeployRoot {
  137. param([string]$Path)
  138. $root = [System.IO.Path]::GetPathRoot($Path)
  139. if ([string]::IsNullOrWhiteSpace($root) -or -not (Test-Path -LiteralPath $root -PathType Container)) {
  140. throw "The deployment drive/root is unavailable: $root"
  141. }
  142. if ($Path.TrimEnd('\') -eq $root.TrimEnd('\')) { throw 'DeployRoot must not be a drive root.' }
  143. if ((Test-Path -LiteralPath $Path) -and -not (Test-Path -LiteralPath $Path -PathType Container)) {
  144. throw "DeployRoot exists but is not a directory: $Path"
  145. }
  146. $ancestor = $Path
  147. while (-not (Test-Path -LiteralPath $ancestor -PathType Container)) {
  148. $parent = Split-Path -Parent $ancestor
  149. if ([string]::IsNullOrWhiteSpace($parent) -or $parent -eq $ancestor) { break }
  150. $ancestor = $parent
  151. }
  152. if (-not (Test-Path -LiteralPath $ancestor -PathType Container)) {
  153. throw "No accessible ancestor exists for DeployRoot: $Path"
  154. }
  155. Get-Item -LiteralPath $ancestor -ErrorAction Stop | Out-Null
  156. }
  157. function Test-PathUnderRoot {
  158. param([string]$Path, [string]$Root)
  159. $normalizedPath = Get-NormalizedPath $Path
  160. $prefix = (Get-NormalizedPath $Root) + '\'
  161. return $normalizedPath.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)
  162. }
  163. function Get-TargetState {
  164. $site = Get-Website -Name $SiteName -ErrorAction SilentlyContinue
  165. $poolExists = Test-Path -LiteralPath ('IIS:\AppPools\' + $AppPoolName)
  166. if (($null -eq $site) -ne (-not $poolExists)) {
  167. throw 'Dedicated target is partial: the site and app pool must either both exist or both be absent.'
  168. }
  169. if ($null -eq $site) {
  170. return [pscustomobject]@{
  171. Exists = $false; Site = $null; PhysicalPath = ''; ParentPaths = $null
  172. SiteState = ''; PoolState = ''
  173. }
  174. }
  175. if ($site.applicationPool -ne $AppPoolName) {
  176. throw "Existing target site uses app pool '$($site.applicationPool)', expected '$AppPoolName'. Refusing adoption."
  177. }
  178. $otherPoolConsumer = Get-Website |
  179. Where-Object { $_.Name -ne $SiteName -and $_.applicationPool -eq $AppPoolName } |
  180. Select-Object -First 1
  181. if ($null -ne $otherPoolConsumer) {
  182. throw "App pool '$AppPoolName' is also used by site '$($otherPoolConsumer.Name)'. Refusing to alter a shared pool."
  183. }
  184. $bindings = @($site.Bindings.Collection)
  185. $expectedBinding = Get-BindingInformation
  186. if ($bindings.Count -ne 1 -or $bindings[0].protocol -ne 'http' -or $bindings[0].bindingInformation -ne $expectedBinding) {
  187. throw "Existing target binding does not exactly match http/$expectedBinding. Refusing adoption or binding changes."
  188. }
  189. $physicalPath = Get-NormalizedPath $site.physicalPath
  190. $releasesRoot = Join-Path $DeployRoot 'releases'
  191. if (-not (Test-PathUnderRoot -Path $physicalPath -Root $releasesRoot) -or
  192. -not $physicalPath.EndsWith('\public', [StringComparison]::OrdinalIgnoreCase)) {
  193. throw "Existing target physicalPath is outside this pipeline's release public directories: $physicalPath"
  194. }
  195. if (-not (Test-Path -LiteralPath $physicalPath -PathType Container)) {
  196. throw "Existing target physicalPath does not exist: $physicalPath"
  197. }
  198. Assert-ReleaseLayout -ReleasePath (Split-Path -Parent $physicalPath)
  199. $parentPaths = (Get-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' -Location $SiteName -Filter 'system.webServer/asp' -Name 'enableParentPaths').Value
  200. return [pscustomobject]@{
  201. Exists = $true; Site = $site; PhysicalPath = $physicalPath; ParentPaths = [bool]$parentPaths
  202. SiteState = (Get-WebsiteState -Name $SiteName).Value
  203. PoolState = (Get-WebAppPoolState -Name $AppPoolName).Value
  204. }
  205. }
  206. function Assert-NoBindingConflict {
  207. param($TargetState)
  208. $expectedBinding = Get-BindingInformation
  209. foreach ($candidate in Get-Website) {
  210. if ($TargetState.Exists -and $candidate.Name -eq $SiteName) { continue }
  211. foreach ($binding in @($candidate.Bindings.Collection)) {
  212. if ($binding.protocol -ne 'http') { continue }
  213. if ($binding.bindingInformation -notmatch '^(.*):(\d+):(.*)$') { continue }
  214. $candidateIp = $Matches[1]
  215. $candidatePort = [int]$Matches[2]
  216. $candidateHost = $Matches[3]
  217. $ipOverlaps = ($candidateIp -eq '*' -or $candidateIp -eq '0.0.0.0' -or $candidateIp -eq $BindingIpAddress)
  218. if ($candidatePort -eq $BindingPort -and $candidateHost -eq $HostHeader -and $ipOverlaps) {
  219. throw "Requested binding http/$expectedBinding conflicts with existing site '$($candidate.Name)' binding '$($binding.bindingInformation)'."
  220. }
  221. }
  222. }
  223. }
  224. function Get-SmokeUrl {
  225. if (-not [string]::IsNullOrWhiteSpace($BaseUrl)) { return $BaseUrl }
  226. $hostPart = $BindingIpAddress
  227. if ($hostPart.Contains(':')) { $hostPart = '[' + $hostPart + ']' }
  228. return 'http://' + $hostPart + ':' + $BindingPort
  229. }
  230. function Invoke-SmokeTest {
  231. if ($SkipSmokeTest) {
  232. Write-Step 'Smoke test skipped by explicit request'
  233. return
  234. }
  235. $target = (Get-SmokeUrl).TrimEnd('/') + '/'
  236. Write-Step ('Smoke testing ' + $target)
  237. $headers = @{}
  238. if (-not [string]::IsNullOrWhiteSpace($HostHeader)) { $headers['Host'] = $HostHeader }
  239. $response = Invoke-WebRequest -UseBasicParsing -Uri $target -Headers $headers -TimeoutSec 30
  240. if ($response.StatusCode -lt 200 -or $response.StatusCode -ge 400) {
  241. throw "Smoke test returned HTTP $($response.StatusCode)"
  242. }
  243. Write-Host ('Smoke test returned HTTP ' + $response.StatusCode)
  244. }
  245. function Set-IisRelease {
  246. param([string]$PhysicalPath)
  247. Set-ItemProperty -Path ('IIS:\Sites\' + $SiteName) -Name physicalPath -Value $PhysicalPath
  248. $poolState = (Get-WebAppPoolState -Name $AppPoolName).Value
  249. if ($poolState -eq 'Started') { Restart-WebAppPool -Name $AppPoolName }
  250. else { Start-WebAppPool -Name $AppPoolName }
  251. }
  252. function Restore-ExistingTarget {
  253. param($TargetState)
  254. if (-not $TargetState.Exists) { return }
  255. Set-ItemProperty -Path ('IIS:\Sites\' + $SiteName) -Name physicalPath -Value $TargetState.PhysicalPath
  256. Set-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' -Location $SiteName -Filter 'system.webServer/asp' -Name 'enableParentPaths' -Value $TargetState.ParentPaths
  257. if ($TargetState.PoolState -eq 'Started') {
  258. if ((Get-WebAppPoolState -Name $AppPoolName).Value -eq 'Started') { Restart-WebAppPool -Name $AppPoolName }
  259. else { Start-WebAppPool -Name $AppPoolName }
  260. } elseif ((Get-WebAppPoolState -Name $AppPoolName).Value -eq 'Started') {
  261. Stop-WebAppPool -Name $AppPoolName
  262. }
  263. if ($TargetState.SiteState -eq 'Started') {
  264. if ((Get-WebsiteState -Name $SiteName).Value -ne 'Started') { Start-Website -Name $SiteName }
  265. } elseif ((Get-WebsiteState -Name $SiteName).Value -eq 'Started') {
  266. Stop-Website -Name $SiteName
  267. }
  268. }
  269. if ($env:OS -ne 'Windows_NT') { throw 'This script must run on Windows.' }
  270. Assert-Administrator
  271. Import-Module WebAdministration -ErrorAction Stop
  272. Assert-DeploymentInputs
  273. $DeployRoot = Get-NormalizedPath $DeployRoot
  274. Assert-DeployRoot -Path $DeployRoot
  275. if (-not [string]::IsNullOrWhiteSpace($InitialWebConfigPath)) {
  276. $preflightInitialConfig = Get-NormalizedPath $InitialWebConfigPath
  277. if (-not (Test-Path -LiteralPath $preflightInitialConfig -PathType Leaf)) {
  278. throw "InitialWebConfigPath not found: $preflightInitialConfig"
  279. }
  280. try { [xml](Get-Content -LiteralPath $preflightInitialConfig -Raw) | Out-Null }
  281. catch { throw "InitialWebConfigPath is not valid XML: $($_.Exception.Message)" }
  282. }
  283. Assert-HostCapabilities
  284. $targetState = Get-TargetState
  285. Assert-NoBindingConflict -TargetState $targetState
  286. Write-Step "Dedicated site: $SiteName"
  287. Write-Host "Dedicated app pool: $AppPoolName"
  288. Write-Host "Binding: http/$(Get-BindingInformation)"
  289. Write-Host "Deployment root: $DeployRoot"
  290. if ($targetState.Exists) {
  291. Write-Host 'Target state: existing and exactly matched'
  292. } else {
  293. Write-Host 'Target state: absent; eligible for isolated creation after release validation'
  294. }
  295. if ($PreflightOnly -or $DryRun) {
  296. Write-Step 'Host preflight passed; no IIS or filesystem changes were made'
  297. exit 0
  298. }
  299. $releasesRoot = Join-Path $DeployRoot 'releases'
  300. $sharedRoot = Join-Path $DeployRoot 'shared'
  301. $sharedConfig = Join-Path $sharedRoot 'public.web.config'
  302. $statePath = Join-Path $DeployRoot 'deployment-state.json'
  303. if ($PSCmdlet.ParameterSetName -eq 'Rollback') {
  304. if (-not $targetState.Exists) { throw 'Rollback requires the dedicated target site and app pool to exist.' }
  305. if (-not (Test-Path -LiteralPath $sharedConfig -PathType Leaf)) { throw "Shared configuration is missing: $sharedConfig" }
  306. $rollbackRoot = Get-NormalizedPath (Join-Path $releasesRoot $RollbackTo)
  307. if (-not (Test-PathUnderRoot -Path $rollbackRoot -Root $releasesRoot)) { throw 'Rollback target escaped the releases directory.' }
  308. Assert-ReleaseLayout -ReleasePath $rollbackRoot
  309. try { [xml](Get-Content -LiteralPath $sharedConfig -Raw) | Out-Null }
  310. catch { throw "Shared configuration is not valid XML: $($_.Exception.Message)" }
  311. try {
  312. Set-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' -Location $SiteName -Filter 'system.webServer/asp' -Name 'enableParentPaths' -Value $true
  313. Set-IisRelease -PhysicalPath (Join-Path $rollbackRoot 'public')
  314. Invoke-SmokeTest
  315. [ordered]@{
  316. siteName = $SiteName; appPoolName = $AppPoolName; currentRelease = $RollbackTo
  317. currentPhysicalPath = (Join-Path $rollbackRoot 'public'); previousPhysicalPath = $targetState.PhysicalPath
  318. binding = (Get-BindingInformation); switchedAtUtc = (Get-Date).ToUniversalTime().ToString('o'); operation = 'rollback'
  319. } | ConvertTo-Json | Set-Content -LiteralPath $statePath -Encoding UTF8
  320. } catch {
  321. Write-Warning 'Rollback failed; restoring the prior dedicated target path and parent-path setting.'
  322. Restore-ExistingTarget -TargetState $targetState
  323. throw
  324. }
  325. Write-Step "Rollback complete: $RollbackTo"
  326. exit 0
  327. }
  328. if ([string]::IsNullOrWhiteSpace($PackagePath)) { throw '-PackagePath is required for deployment.' }
  329. $PackagePath = Get-NormalizedPath $PackagePath
  330. if (-not (Test-Path -LiteralPath $PackagePath -PathType Leaf)) { throw "Package not found: $PackagePath" }
  331. if ([System.IO.Path]::GetExtension($PackagePath) -ne '.zip') { throw 'PackagePath must name a .zip package.' }
  332. if (-not [string]::IsNullOrWhiteSpace($ExpectedSha256)) {
  333. $actualHash = (Get-FileHash -LiteralPath $PackagePath -Algorithm SHA256).Hash
  334. if ($actualHash -ne $ExpectedSha256) { throw "Package SHA-256 mismatch. Expected $ExpectedSha256; got $actualHash" }
  335. Write-Host ('Package SHA-256 verified: ' + $actualHash)
  336. }
  337. Assert-ArchiveEntries -ZipPath $PackagePath
  338. $releaseRoot = Join-Path $releasesRoot $ReleaseId
  339. $stagingRoot = $releaseRoot + '.staging'
  340. if ((Test-Path -LiteralPath $releaseRoot) -or (Test-Path -LiteralPath $stagingRoot)) { throw "Release already exists: $ReleaseId" }
  341. $createdSite = $false
  342. $createdPool = $false
  343. $createdSharedConfig = $false
  344. $iisMutationStarted = $false
  345. try {
  346. New-Item -ItemType Directory -Force -Path $releasesRoot, $sharedRoot | Out-Null
  347. New-Item -ItemType Directory -Force -Path $stagingRoot | Out-Null
  348. Expand-Archive -LiteralPath $PackagePath -DestinationPath $stagingRoot -Force
  349. $unsafeExtractedItem = Get-ChildItem -LiteralPath $stagingRoot -Recurse -Force |
  350. Where-Object { ($_.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 } |
  351. Select-Object -First 1
  352. if ($null -ne $unsafeExtractedItem) {
  353. throw "Extracted release contains a reparse point: $($unsafeExtractedItem.FullName)"
  354. }
  355. Assert-ReleaseLayout -ReleasePath $stagingRoot
  356. if (-not (Test-Path -LiteralPath $sharedConfig -PathType Leaf)) {
  357. $initialConfig = Join-Path $stagingRoot 'public\web.config'
  358. if (-not [string]::IsNullOrWhiteSpace($InitialWebConfigPath)) {
  359. $initialConfig = Get-NormalizedPath $InitialWebConfigPath
  360. if (-not (Test-Path -LiteralPath $initialConfig -PathType Leaf)) { throw "InitialWebConfigPath not found: $initialConfig" }
  361. }
  362. try { [xml](Get-Content -LiteralPath $initialConfig -Raw) | Out-Null }
  363. catch { throw "Initial web.config is not valid XML: $($_.Exception.Message)" }
  364. Copy-Item -LiteralPath $initialConfig -Destination $sharedConfig -Force
  365. $createdSharedConfig = $true
  366. Write-Step "Initialized shared configuration from $initialConfig"
  367. }
  368. try { [xml](Get-Content -LiteralPath $sharedConfig -Raw) | Out-Null }
  369. catch { throw "Shared configuration is not valid XML: $($_.Exception.Message)" }
  370. Copy-Item -LiteralPath $sharedConfig -Destination (Join-Path $stagingRoot 'public\web.config') -Force
  371. Assert-ReleaseLayout -ReleasePath $stagingRoot
  372. if ($RunMigrations) {
  373. $migrationScript = Join-Path $stagingRoot 'scripts\runMigrations.vbs'
  374. if (-not (Test-Path -LiteralPath $migrationScript -PathType Leaf)) { throw "Migration script not found: $migrationScript" }
  375. Write-Warning 'Running migrations by explicit request; IIS rollback cannot undo data changes.'
  376. Push-Location $stagingRoot
  377. try {
  378. & cscript.exe //nologo $migrationScript up
  379. if ($LASTEXITCODE -ne 0) { throw "Migration command exited with code $LASTEXITCODE" }
  380. } finally { Pop-Location }
  381. }
  382. Move-Item -LiteralPath $stagingRoot -Destination $releaseRoot
  383. $newPublic = Join-Path $releaseRoot 'public'
  384. $iisMutationStarted = $true
  385. if (-not $targetState.Exists) {
  386. New-WebAppPool -Name $AppPoolName | Out-Null
  387. $createdPool = $true
  388. Set-ItemProperty -Path ('IIS:\AppPools\' + $AppPoolName) -Name managedRuntimeVersion -Value ''
  389. New-Website -Name $SiteName -PhysicalPath $newPublic -ApplicationPool $AppPoolName -IPAddress $BindingIpAddress -Port $BindingPort -HostHeader $HostHeader | Out-Null
  390. $createdSite = $true
  391. } else {
  392. Set-IisRelease -PhysicalPath $newPublic
  393. }
  394. Set-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' -Location $SiteName -Filter 'system.webServer/asp' -Name 'enableParentPaths' -Value $true
  395. if (-not $targetState.Exists) {
  396. $poolState = (Get-WebAppPoolState -Name $AppPoolName).Value
  397. if ($poolState -ne 'Started') { Start-WebAppPool -Name $AppPoolName }
  398. $siteState = (Get-WebsiteState -Name $SiteName).Value
  399. if ($siteState -ne 'Started') { Start-Website -Name $SiteName }
  400. }
  401. Invoke-SmokeTest
  402. [ordered]@{
  403. siteName = $SiteName; appPoolName = $AppPoolName; currentRelease = $ReleaseId
  404. currentPhysicalPath = $newPublic; previousPhysicalPath = $targetState.PhysicalPath
  405. binding = (Get-BindingInformation); packageSha256 = (Get-FileHash -LiteralPath $PackagePath -Algorithm SHA256).Hash
  406. switchedAtUtc = (Get-Date).ToUniversalTime().ToString('o'); migrationsRun = [bool]$RunMigrations; operation = 'deploy'
  407. } | ConvertTo-Json | Set-Content -LiteralPath $statePath -Encoding UTF8
  408. $protectedPaths = @($newPublic, $targetState.PhysicalPath)
  409. $oldReleases = Get-ChildItem -LiteralPath $releasesRoot -Directory |
  410. Where-Object { $_.Name -notlike '*.staging' } |
  411. Sort-Object LastWriteTimeUtc -Descending |
  412. Select-Object -Skip $KeepReleases
  413. foreach ($oldRelease in $oldReleases) {
  414. $oldPublic = Join-Path $oldRelease.FullName 'public'
  415. if ($protectedPaths -notcontains $oldPublic) { Write-Step ('Retention candidate (not deleted): ' + $oldRelease.FullName) }
  416. }
  417. Write-Step "Deployment complete: $ReleaseId"
  418. } catch {
  419. $failure = $_
  420. if ($iisMutationStarted) {
  421. if ($createdSite -and (Get-Website -Name $SiteName -ErrorAction SilentlyContinue)) {
  422. Write-Warning "Removing site created by this invocation: $SiteName"
  423. Remove-Website -Name $SiteName
  424. }
  425. if ($createdPool -and (Test-Path -LiteralPath ('IIS:\AppPools\' + $AppPoolName))) {
  426. Write-Warning "Removing app pool created by this invocation: $AppPoolName"
  427. Remove-WebAppPool -Name $AppPoolName
  428. }
  429. if ($targetState.Exists) {
  430. Write-Warning 'Restoring the prior dedicated target state.'
  431. Restore-ExistingTarget -TargetState $targetState
  432. }
  433. }
  434. if ($createdSharedConfig -and (Test-Path -LiteralPath $sharedConfig -PathType Leaf)) {
  435. Write-Warning 'Removing shared configuration initialized by this failed invocation.'
  436. Remove-Item -LiteralPath $sharedConfig -Force
  437. }
  438. if (Test-Path -LiteralPath $stagingRoot) { Write-Warning "Incomplete staging retained for inspection: $stagingRoot" }
  439. throw $failure
  440. }

Powered by TurnKey Linux.