You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

486 lines
22KB

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

Powered by TurnKey Linux.