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.

478 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. $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.