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

477 lines
21KB

  1. <#
  2. .SYNOPSIS
  3. Installs or rolls back an immutable release in a dedicated IIS site/app pool.
  4. .DESCRIPTION
  5. The complete repository is retained in each immutable release and IIS serves
  6. only <release>\public. The script never discovers or adopts another site.
  7. A missing dedicated target is created only after package extraction, layout,
  8. and XML validation succeed. Host preflight is read-only and does not require
  9. the dedicated target to exist.
  10. #>
  11. [CmdletBinding(DefaultParameterSetName = 'Deploy')]
  12. param(
  13. [ValidatePattern('^[A-Za-z0-9_. -]+$')]
  14. [string]$SiteName = 'AspClassicUnifiedFramework',
  15. [ValidatePattern('^[A-Za-z0-9_. -]+$')]
  16. [string]$AppPoolName = 'AspClassicUnifiedFramework',
  17. [string]$DeployRoot = 'D:\Deployments\AspClassicUnifiedFramework',
  18. [string]$BindingIpAddress = '100.97.39.23',
  19. [ValidateRange(1, 65535)]
  20. [int]$BindingPort = 8085,
  21. [AllowEmptyString()]
  22. [string]$HostHeader = '',
  23. [string]$InitialWebConfigPath = '',
  24. [Parameter(ParameterSetName = 'Deploy')]
  25. [string]$PackagePath = '',
  26. [Parameter(ParameterSetName = 'Deploy')]
  27. [ValidatePattern('^[A-Za-z0-9._-]+$')]
  28. [string]$ReleaseId = (Get-Date -Format 'yyyyMMdd-HHmmss'),
  29. [Parameter(Mandatory = $true, ParameterSetName = 'Rollback')]
  30. [ValidatePattern('^[A-Za-z0-9._-]+$')]
  31. [string]$RollbackTo,
  32. [string]$BaseUrl = '',
  33. [ValidateRange(2, 100)]
  34. [int]$KeepReleases = 5,
  35. [string]$ExpectedSha256 = '',
  36. [switch]$RunMigrations,
  37. [switch]$SkipSmokeTest,
  38. [switch]$PreflightOnly,
  39. [switch]$DryRun
  40. )
  41. Set-StrictMode -Version 2.0
  42. $ErrorActionPreference = 'Stop'
  43. function Write-Step { param([string]$Message) Write-Host ('==> ' + $Message) }
  44. function Get-NormalizedPath {
  45. param([string]$Path)
  46. return [System.IO.Path]::GetFullPath([Environment]::ExpandEnvironmentVariables($Path)).TrimEnd('\')
  47. }
  48. function Assert-Administrator {
  49. $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
  50. $principal = New-Object Security.Principal.WindowsPrincipal($identity)
  51. if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
  52. throw 'An elevated Administrator PowerShell session is required.'
  53. }
  54. }
  55. function Assert-SafeNameValue {
  56. param([string]$Name, [string]$Value)
  57. if ([string]::IsNullOrWhiteSpace($Value) -or $Value -notmatch '^[A-Za-z0-9_. -]+$') {
  58. throw "$Name contains unsupported characters."
  59. }
  60. if ($Value -match '(?i)schedulicious') {
  61. throw "$Name must not identify a Schedulicious resource."
  62. }
  63. }
  64. function Assert-DeploymentInputs {
  65. Assert-SafeNameValue -Name 'SiteName' -Value $SiteName
  66. Assert-SafeNameValue -Name 'AppPoolName' -Value $AppPoolName
  67. if ($DeployRoot -match '(?i)schedulicious') { throw 'DeployRoot must not reference Schedulicious.' }
  68. if ($HostHeader -match '(?i)schedulicious') { throw 'HostHeader must not reference Schedulicious.' }
  69. if ($HostHeader -match '[:/\\]') {
  70. throw 'HostHeader must be empty or a DNS host name without a scheme, port, slash, or backslash.'
  71. }
  72. $parsedAddress = $null
  73. if (-not [System.Net.IPAddress]::TryParse($BindingIpAddress, [ref]$parsedAddress)) {
  74. throw "BindingIpAddress is not a valid IP address: $BindingIpAddress"
  75. }
  76. }
  77. function Assert-ReleaseLayout {
  78. param([string]$ReleasePath)
  79. $required = @(
  80. 'public\Default.asp',
  81. 'public\web.config',
  82. 'core\autoload_core.asp',
  83. 'app\controllers\autoload_controllers.asp',
  84. 'scripts\install-iis-release.ps1'
  85. )
  86. foreach ($relativePath in $required) {
  87. if (-not (Test-Path -LiteralPath (Join-Path $ReleasePath $relativePath) -PathType Leaf)) {
  88. throw "Release is incomplete; missing $relativePath"
  89. }
  90. }
  91. foreach ($xmlFile in Get-ChildItem -LiteralPath $ReleasePath -Recurse -Force -Filter 'web.config') {
  92. try {
  93. [xml](Get-Content -LiteralPath $xmlFile.FullName -Raw) | Out-Null
  94. } catch {
  95. throw "$($xmlFile.FullName) is not valid XML: $($_.Exception.Message)"
  96. }
  97. }
  98. }
  99. function Assert-ArchiveEntries {
  100. param([string]$ZipPath)
  101. Add-Type -AssemblyName System.IO.Compression.FileSystem
  102. $archive = [System.IO.Compression.ZipFile]::OpenRead($ZipPath)
  103. try {
  104. foreach ($entry in $archive.Entries) {
  105. $name = $entry.FullName.Replace('/', '\')
  106. if ([System.IO.Path]::IsPathRooted($name) -or $name -match '(^|\\)\.\.(\\|$)') {
  107. throw "Package contains an unsafe path: $($entry.FullName)"
  108. }
  109. }
  110. } finally {
  111. $archive.Dispose()
  112. }
  113. }
  114. function Get-BindingInformation {
  115. return $BindingIpAddress + ':' + $BindingPort + ':' + $HostHeader
  116. }
  117. function Assert-HostCapabilities {
  118. # 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.