Consolidated ASP Classic MVC framework from best components
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

477 lignes
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. # Query IIS directly instead of Get-WindowsFeature/Get-WindowsOptionalFeature:
  119. # those feature cmdlets can stall while collecting server-manager state.
  120. $aspHandler = (Get-WebConfiguration -PSPath 'MACHINE/WEBROOT/APPHOST' -Filter 'system.webServer/handlers').Collection |
  121. Where-Object { $_.path -eq '*.asp' -and $_.modules -match '(^|,)IsapiModule(,|$)' -and $_.scriptProcessor -match '(?i)asp\.dll$' } |
  122. Select-Object -First 1
  123. if ($null -eq $aspHandler) {
  124. throw 'The IIS Classic ASP handler (*.asp through asp.dll) was not found.'
  125. }
  126. try {
  127. Get-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' -Filter 'system.webServer/asp' -Name 'enableParentPaths' -ErrorAction Stop | Out-Null
  128. } catch {
  129. throw "The IIS Classic ASP configuration section is unavailable: $($_.Exception.Message)"
  130. }
  131. if ($null -eq (Get-WebGlobalModule -Name RewriteModule -ErrorAction SilentlyContinue)) {
  132. throw 'IIS URL Rewrite is not installed (RewriteModule was not found).'
  133. }
  134. }
  135. function Assert-DeployRoot {
  136. param([string]$Path)
  137. $root = [System.IO.Path]::GetPathRoot($Path)
  138. if ([string]::IsNullOrWhiteSpace($root) -or -not (Test-Path -LiteralPath $root -PathType Container)) {
  139. throw "The deployment drive/root is unavailable: $root"
  140. }
  141. if ($Path.TrimEnd('\') -eq $root.TrimEnd('\')) { throw 'DeployRoot must not be a drive root.' }
  142. if ((Test-Path -LiteralPath $Path) -and -not (Test-Path -LiteralPath $Path -PathType Container)) {
  143. throw "DeployRoot exists but is not a directory: $Path"
  144. }
  145. $ancestor = $Path
  146. while (-not (Test-Path -LiteralPath $ancestor -PathType Container)) {
  147. $parent = Split-Path -Parent $ancestor
  148. if ([string]::IsNullOrWhiteSpace($parent) -or $parent -eq $ancestor) { break }
  149. $ancestor = $parent
  150. }
  151. if (-not (Test-Path -LiteralPath $ancestor -PathType Container)) {
  152. throw "No accessible ancestor exists for DeployRoot: $Path"
  153. }
  154. Get-Item -LiteralPath $ancestor -ErrorAction Stop | Out-Null
  155. }
  156. function Test-PathUnderRoot {
  157. param([string]$Path, [string]$Root)
  158. $normalizedPath = Get-NormalizedPath $Path
  159. $prefix = (Get-NormalizedPath $Root) + '\'
  160. return $normalizedPath.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)
  161. }
  162. function Get-TargetState {
  163. $site = Get-Website -Name $SiteName -ErrorAction SilentlyContinue
  164. $poolExists = Test-Path -LiteralPath ('IIS:\AppPools\' + $AppPoolName)
  165. if (($null -eq $site) -ne (-not $poolExists)) {
  166. throw 'Dedicated target is partial: the site and app pool must either both exist or both be absent.'
  167. }
  168. if ($null -eq $site) {
  169. return [pscustomobject]@{
  170. Exists = $false; Site = $null; PhysicalPath = ''; ParentPaths = $null
  171. SiteState = ''; PoolState = ''
  172. }
  173. }
  174. if ($site.applicationPool -ne $AppPoolName) {
  175. throw "Existing target site uses app pool '$($site.applicationPool)', expected '$AppPoolName'. Refusing adoption."
  176. }
  177. $otherPoolConsumer = Get-Website |
  178. Where-Object { $_.Name -ne $SiteName -and $_.applicationPool -eq $AppPoolName } |
  179. Select-Object -First 1
  180. if ($null -ne $otherPoolConsumer) {
  181. throw "App pool '$AppPoolName' is also used by site '$($otherPoolConsumer.Name)'. Refusing to alter a shared pool."
  182. }
  183. $bindings = @($site.Bindings.Collection)
  184. $expectedBinding = Get-BindingInformation
  185. if ($bindings.Count -ne 1 -or $bindings[0].protocol -ne 'http' -or $bindings[0].bindingInformation -ne $expectedBinding) {
  186. throw "Existing target binding does not exactly match http/$expectedBinding. Refusing adoption or binding changes."
  187. }
  188. $physicalPath = Get-NormalizedPath $site.physicalPath
  189. $releasesRoot = Join-Path $DeployRoot 'releases'
  190. if (-not (Test-PathUnderRoot -Path $physicalPath -Root $releasesRoot) -or
  191. -not $physicalPath.EndsWith('\public', [StringComparison]::OrdinalIgnoreCase)) {
  192. throw "Existing target physicalPath is outside this pipeline's release public directories: $physicalPath"
  193. }
  194. if (-not (Test-Path -LiteralPath $physicalPath -PathType Container)) {
  195. throw "Existing target physicalPath does not exist: $physicalPath"
  196. }
  197. Assert-ReleaseLayout -ReleasePath (Split-Path -Parent $physicalPath)
  198. $parentPaths = (Get-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' -Location $SiteName -Filter 'system.webServer/asp' -Name 'enableParentPaths').Value
  199. return [pscustomobject]@{
  200. Exists = $true; Site = $site; PhysicalPath = $physicalPath; ParentPaths = [bool]$parentPaths
  201. SiteState = (Get-WebsiteState -Name $SiteName).Value
  202. PoolState = (Get-WebAppPoolState -Name $AppPoolName).Value
  203. }
  204. }
  205. function Assert-NoBindingConflict {
  206. param($TargetState)
  207. $expectedBinding = Get-BindingInformation
  208. foreach ($candidate in Get-Website) {
  209. if ($TargetState.Exists -and $candidate.Name -eq $SiteName) { continue }
  210. foreach ($binding in @($candidate.Bindings.Collection)) {
  211. if ($binding.protocol -ne 'http') { continue }
  212. if ($binding.bindingInformation -notmatch '^(.*):(\d+):(.*)$') { continue }
  213. $candidateIp = $Matches[1]
  214. $candidatePort = [int]$Matches[2]
  215. $candidateHost = $Matches[3]
  216. $ipOverlaps = ($candidateIp -eq '*' -or $candidateIp -eq '0.0.0.0' -or $candidateIp -eq $BindingIpAddress)
  217. if ($candidatePort -eq $BindingPort -and $candidateHost -eq $HostHeader -and $ipOverlaps) {
  218. throw "Requested binding http/$expectedBinding conflicts with existing site '$($candidate.Name)' binding '$($binding.bindingInformation)'."
  219. }
  220. }
  221. }
  222. }
  223. function Get-SmokeUrl {
  224. if (-not [string]::IsNullOrWhiteSpace($BaseUrl)) { return $BaseUrl }
  225. $hostPart = $BindingIpAddress
  226. if ($hostPart.Contains(':')) { $hostPart = '[' + $hostPart + ']' }
  227. return 'http://' + $hostPart + ':' + $BindingPort
  228. }
  229. function Invoke-SmokeTest {
  230. if ($SkipSmokeTest) {
  231. Write-Step 'Smoke test skipped by explicit request'
  232. return
  233. }
  234. $target = (Get-SmokeUrl).TrimEnd('/') + '/'
  235. Write-Step ('Smoke testing ' + $target)
  236. $headers = @{}
  237. if (-not [string]::IsNullOrWhiteSpace($HostHeader)) { $headers['Host'] = $HostHeader }
  238. $response = Invoke-WebRequest -UseBasicParsing -Uri $target -Headers $headers -TimeoutSec 30
  239. if ($response.StatusCode -lt 200 -or $response.StatusCode -ge 400) {
  240. throw "Smoke test returned HTTP $($response.StatusCode)"
  241. }
  242. Write-Host ('Smoke test returned HTTP ' + $response.StatusCode)
  243. }
  244. function Set-IisRelease {
  245. param([string]$PhysicalPath)
  246. Set-ItemProperty -Path ('IIS:\Sites\' + $SiteName) -Name physicalPath -Value $PhysicalPath
  247. $poolState = (Get-WebAppPoolState -Name $AppPoolName).Value
  248. if ($poolState -eq 'Started') { Restart-WebAppPool -Name $AppPoolName }
  249. else { Start-WebAppPool -Name $AppPoolName }
  250. }
  251. function Restore-ExistingTarget {
  252. param($TargetState)
  253. if (-not $TargetState.Exists) { return }
  254. Set-ItemProperty -Path ('IIS:\Sites\' + $SiteName) -Name physicalPath -Value $TargetState.PhysicalPath
  255. Set-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' -Location $SiteName -Filter 'system.webServer/asp' -Name 'enableParentPaths' -Value $TargetState.ParentPaths
  256. if ($TargetState.PoolState -eq 'Started') {
  257. if ((Get-WebAppPoolState -Name $AppPoolName).Value -eq 'Started') { Restart-WebAppPool -Name $AppPoolName }
  258. else { Start-WebAppPool -Name $AppPoolName }
  259. } elseif ((Get-WebAppPoolState -Name $AppPoolName).Value -eq 'Started') {
  260. Stop-WebAppPool -Name $AppPoolName
  261. }
  262. if ($TargetState.SiteState -eq 'Started') {
  263. if ((Get-WebsiteState -Name $SiteName).Value -ne 'Started') { Start-Website -Name $SiteName }
  264. } elseif ((Get-WebsiteState -Name $SiteName).Value -eq 'Started') {
  265. Stop-Website -Name $SiteName
  266. }
  267. }
  268. if ($env:OS -ne 'Windows_NT') { throw 'This script must run on Windows.' }
  269. Assert-Administrator
  270. Import-Module WebAdministration -ErrorAction Stop
  271. Assert-DeploymentInputs
  272. $DeployRoot = Get-NormalizedPath $DeployRoot
  273. Assert-DeployRoot -Path $DeployRoot
  274. if (-not [string]::IsNullOrWhiteSpace($InitialWebConfigPath)) {
  275. $preflightInitialConfig = Get-NormalizedPath $InitialWebConfigPath
  276. if (-not (Test-Path -LiteralPath $preflightInitialConfig -PathType Leaf)) {
  277. throw "InitialWebConfigPath not found: $preflightInitialConfig"
  278. }
  279. try { [xml](Get-Content -LiteralPath $preflightInitialConfig -Raw) | Out-Null }
  280. catch { throw "InitialWebConfigPath is not valid XML: $($_.Exception.Message)" }
  281. }
  282. Assert-HostCapabilities
  283. $targetState = Get-TargetState
  284. Assert-NoBindingConflict -TargetState $targetState
  285. Write-Step "Dedicated site: $SiteName"
  286. Write-Host "Dedicated app pool: $AppPoolName"
  287. Write-Host "Binding: http/$(Get-BindingInformation)"
  288. Write-Host "Deployment root: $DeployRoot"
  289. if ($targetState.Exists) {
  290. Write-Host 'Target state: existing and exactly matched'
  291. } else {
  292. Write-Host 'Target state: absent; eligible for isolated creation after release validation'
  293. }
  294. if ($PreflightOnly -or $DryRun) {
  295. Write-Step 'Host preflight passed; no IIS or filesystem changes were made'
  296. exit 0
  297. }
  298. $releasesRoot = Join-Path $DeployRoot 'releases'
  299. $sharedRoot = Join-Path $DeployRoot 'shared'
  300. $sharedConfig = Join-Path $sharedRoot 'public.web.config'
  301. $statePath = Join-Path $DeployRoot 'deployment-state.json'
  302. if ($PSCmdlet.ParameterSetName -eq 'Rollback') {
  303. if (-not $targetState.Exists) { throw 'Rollback requires the dedicated target site and app pool to exist.' }
  304. if (-not (Test-Path -LiteralPath $sharedConfig -PathType Leaf)) { throw "Shared configuration is missing: $sharedConfig" }
  305. $rollbackRoot = Get-NormalizedPath (Join-Path $releasesRoot $RollbackTo)
  306. if (-not (Test-PathUnderRoot -Path $rollbackRoot -Root $releasesRoot)) { throw 'Rollback target escaped the releases directory.' }
  307. Assert-ReleaseLayout -ReleasePath $rollbackRoot
  308. try { [xml](Get-Content -LiteralPath $sharedConfig -Raw) | Out-Null }
  309. catch { throw "Shared configuration is not valid XML: $($_.Exception.Message)" }
  310. try {
  311. Set-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' -Location $SiteName -Filter 'system.webServer/asp' -Name 'enableParentPaths' -Value $true
  312. Set-IisRelease -PhysicalPath (Join-Path $rollbackRoot 'public')
  313. Invoke-SmokeTest
  314. [ordered]@{
  315. siteName = $SiteName; appPoolName = $AppPoolName; currentRelease = $RollbackTo
  316. currentPhysicalPath = (Join-Path $rollbackRoot 'public'); previousPhysicalPath = $targetState.PhysicalPath
  317. binding = (Get-BindingInformation); switchedAtUtc = (Get-Date).ToUniversalTime().ToString('o'); operation = 'rollback'
  318. } | ConvertTo-Json | Set-Content -LiteralPath $statePath -Encoding UTF8
  319. } catch {
  320. Write-Warning 'Rollback failed; restoring the prior dedicated target path and parent-path setting.'
  321. Restore-ExistingTarget -TargetState $targetState
  322. throw
  323. }
  324. Write-Step "Rollback complete: $RollbackTo"
  325. exit 0
  326. }
  327. if ([string]::IsNullOrWhiteSpace($PackagePath)) { throw '-PackagePath is required for deployment.' }
  328. $PackagePath = Get-NormalizedPath $PackagePath
  329. if (-not (Test-Path -LiteralPath $PackagePath -PathType Leaf)) { throw "Package not found: $PackagePath" }
  330. if ([System.IO.Path]::GetExtension($PackagePath) -ne '.zip') { throw 'PackagePath must name a .zip package.' }
  331. if (-not [string]::IsNullOrWhiteSpace($ExpectedSha256)) {
  332. $actualHash = (Get-FileHash -LiteralPath $PackagePath -Algorithm SHA256).Hash
  333. if ($actualHash -ne $ExpectedSha256) { throw "Package SHA-256 mismatch. Expected $ExpectedSha256; got $actualHash" }
  334. Write-Host ('Package SHA-256 verified: ' + $actualHash)
  335. }
  336. Assert-ArchiveEntries -ZipPath $PackagePath
  337. $releaseRoot = Join-Path $releasesRoot $ReleaseId
  338. $stagingRoot = $releaseRoot + '.staging'
  339. if ((Test-Path -LiteralPath $releaseRoot) -or (Test-Path -LiteralPath $stagingRoot)) { throw "Release already exists: $ReleaseId" }
  340. $createdSite = $false
  341. $createdPool = $false
  342. $createdSharedConfig = $false
  343. $iisMutationStarted = $false
  344. try {
  345. New-Item -ItemType Directory -Force -Path $releasesRoot, $sharedRoot | Out-Null
  346. New-Item -ItemType Directory -Force -Path $stagingRoot | Out-Null
  347. Expand-Archive -LiteralPath $PackagePath -DestinationPath $stagingRoot -Force
  348. $unsafeExtractedItem = Get-ChildItem -LiteralPath $stagingRoot -Recurse -Force |
  349. Where-Object { ($_.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 } |
  350. Select-Object -First 1
  351. if ($null -ne $unsafeExtractedItem) {
  352. throw "Extracted release contains a reparse point: $($unsafeExtractedItem.FullName)"
  353. }
  354. Assert-ReleaseLayout -ReleasePath $stagingRoot
  355. if (-not (Test-Path -LiteralPath $sharedConfig -PathType Leaf)) {
  356. $initialConfig = Join-Path $stagingRoot 'public\web.config'
  357. if (-not [string]::IsNullOrWhiteSpace($InitialWebConfigPath)) {
  358. $initialConfig = Get-NormalizedPath $InitialWebConfigPath
  359. if (-not (Test-Path -LiteralPath $initialConfig -PathType Leaf)) { throw "InitialWebConfigPath not found: $initialConfig" }
  360. }
  361. try { [xml](Get-Content -LiteralPath $initialConfig -Raw) | Out-Null }
  362. catch { throw "Initial web.config is not valid XML: $($_.Exception.Message)" }
  363. Copy-Item -LiteralPath $initialConfig -Destination $sharedConfig -Force
  364. $createdSharedConfig = $true
  365. Write-Step "Initialized shared configuration from $initialConfig"
  366. }
  367. try { [xml](Get-Content -LiteralPath $sharedConfig -Raw) | Out-Null }
  368. catch { throw "Shared configuration is not valid XML: $($_.Exception.Message)" }
  369. Copy-Item -LiteralPath $sharedConfig -Destination (Join-Path $stagingRoot 'public\web.config') -Force
  370. Assert-ReleaseLayout -ReleasePath $stagingRoot
  371. if ($RunMigrations) {
  372. $migrationScript = Join-Path $stagingRoot 'scripts\runMigrations.vbs'
  373. if (-not (Test-Path -LiteralPath $migrationScript -PathType Leaf)) { throw "Migration script not found: $migrationScript" }
  374. Write-Warning 'Running migrations by explicit request; IIS rollback cannot undo data changes.'
  375. Push-Location $stagingRoot
  376. try {
  377. & cscript.exe //nologo $migrationScript up
  378. if ($LASTEXITCODE -ne 0) { throw "Migration command exited with code $LASTEXITCODE" }
  379. } finally { Pop-Location }
  380. }
  381. Move-Item -LiteralPath $stagingRoot -Destination $releaseRoot
  382. $newPublic = Join-Path $releaseRoot 'public'
  383. $iisMutationStarted = $true
  384. if (-not $targetState.Exists) {
  385. New-WebAppPool -Name $AppPoolName | Out-Null
  386. $createdPool = $true
  387. Set-ItemProperty -Path ('IIS:\AppPools\' + $AppPoolName) -Name managedRuntimeVersion -Value ''
  388. New-Website -Name $SiteName -PhysicalPath $newPublic -ApplicationPool $AppPoolName -IPAddress $BindingIpAddress -Port $BindingPort -HostHeader $HostHeader | Out-Null
  389. $createdSite = $true
  390. } else {
  391. Set-IisRelease -PhysicalPath $newPublic
  392. }
  393. Set-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' -Location $SiteName -Filter 'system.webServer/asp' -Name 'enableParentPaths' -Value $true
  394. if (-not $targetState.Exists) {
  395. $poolState = (Get-WebAppPoolState -Name $AppPoolName).Value
  396. if ($poolState -ne 'Started') { Start-WebAppPool -Name $AppPoolName }
  397. $siteState = (Get-WebsiteState -Name $SiteName).Value
  398. if ($siteState -ne 'Started') { Start-Website -Name $SiteName }
  399. }
  400. Invoke-SmokeTest
  401. [ordered]@{
  402. siteName = $SiteName; appPoolName = $AppPoolName; currentRelease = $ReleaseId
  403. currentPhysicalPath = $newPublic; previousPhysicalPath = $targetState.PhysicalPath
  404. binding = (Get-BindingInformation); packageSha256 = (Get-FileHash -LiteralPath $PackagePath -Algorithm SHA256).Hash
  405. switchedAtUtc = (Get-Date).ToUniversalTime().ToString('o'); migrationsRun = [bool]$RunMigrations; operation = 'deploy'
  406. } | ConvertTo-Json | Set-Content -LiteralPath $statePath -Encoding UTF8
  407. $protectedPaths = @($newPublic, $targetState.PhysicalPath)
  408. $oldReleases = Get-ChildItem -LiteralPath $releasesRoot -Directory |
  409. Where-Object { $_.Name -notlike '*.staging' } |
  410. Sort-Object LastWriteTimeUtc -Descending |
  411. Select-Object -Skip $KeepReleases
  412. foreach ($oldRelease in $oldReleases) {
  413. $oldPublic = Join-Path $oldRelease.FullName 'public'
  414. if ($protectedPaths -notcontains $oldPublic) { Write-Step ('Retention candidate (not deleted): ' + $oldRelease.FullName) }
  415. }
  416. Write-Step "Deployment complete: $ReleaseId"
  417. } catch {
  418. $failure = $_
  419. if ($iisMutationStarted) {
  420. if ($createdSite -and (Get-Website -Name $SiteName -ErrorAction SilentlyContinue)) {
  421. Write-Warning "Removing site created by this invocation: $SiteName"
  422. Remove-Website -Name $SiteName
  423. }
  424. if ($createdPool -and (Test-Path -LiteralPath ('IIS:\AppPools\' + $AppPoolName))) {
  425. Write-Warning "Removing app pool created by this invocation: $AppPoolName"
  426. Remove-WebAppPool -Name $AppPoolName
  427. }
  428. if ($targetState.Exists) {
  429. Write-Warning 'Restoring the prior dedicated target state.'
  430. Restore-ExistingTarget -TargetState $targetState
  431. }
  432. }
  433. if ($createdSharedConfig -and (Test-Path -LiteralPath $sharedConfig -PathType Leaf)) {
  434. Write-Warning 'Removing shared configuration initialized by this failed invocation.'
  435. Remove-Item -LiteralPath $sharedConfig -Force
  436. }
  437. if (Test-Path -LiteralPath $stagingRoot) { Write-Warning "Incomplete staging retained for inspection: $stagingRoot" }
  438. throw $failure
  439. }

Powered by TurnKey Linux.