Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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

Powered by TurnKey Linux.