Consolidated ASP Classic MVC framework from best components
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

266 linhas
9.8KB

  1. <#
  2. .SYNOPSIS
  3. Packages this repository and deploys it to IIS over Tailscale/OpenSSH.
  4. .DESCRIPTION
  5. This is the controller/CI entry point. It creates a ZIP containing the full
  6. repository (excluding VCS and local deployment artifacts), computes SHA-256,
  7. copies the package and host installer with scp, then invokes the installer on
  8. the Windows host through ssh.
  9. -DryRun performs local validation and prints the remote operations without
  10. connecting. Gitea 1.11.4 does not run this itself; use a trusted external CI
  11. worker, a scheduled task, or an operator workstation.
  12. #>
  13. [CmdletBinding()]
  14. param(
  15. [Parameter(Mandatory = $true)]
  16. [ValidatePattern('^[A-Za-z0-9_. -]+$')]
  17. [string]$SiteName,
  18. [string]$RemoteTarget = 'webserver-1',
  19. [ValidateRange(1, 65535)]
  20. [int]$RemotePort = 22,
  21. [string]$SourcePath = (Split-Path $PSScriptRoot -Parent),
  22. [string]$DeployRoot = '',
  23. [string]$BaseUrl = '',
  24. [string]$ReleaseId = '',
  25. [string]$ExpectedBranch = 'master',
  26. [ValidateRange(2, 100)]
  27. [int]$KeepReleases = 5,
  28. [string]$SshExe = 'ssh',
  29. [string]$ScpExe = 'scp',
  30. [switch]$AllowAnyBranch,
  31. [switch]$AllowDirty,
  32. [switch]$RunMigrations,
  33. [switch]$SkipSmokeTest,
  34. [switch]$RemotePreflightOnly,
  35. [switch]$Rollback,
  36. [string]$RollbackTo = '',
  37. [switch]$DryRun,
  38. [switch]$KeepPackage
  39. )
  40. Set-StrictMode -Version 2.0
  41. $ErrorActionPreference = 'Stop'
  42. function Write-Step {
  43. param([string]$Message)
  44. Write-Host ('==> ' + $Message)
  45. }
  46. function Assert-Command {
  47. param([string]$Name)
  48. if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
  49. throw "$Name was not found on PATH."
  50. }
  51. }
  52. function ConvertTo-SingleQuotedPowerShell {
  53. param([string]$Value)
  54. return "'" + $Value.Replace("'", "''") + "'"
  55. }
  56. function Add-RemoteArgument {
  57. param(
  58. [System.Collections.Generic.List[string]]$Arguments,
  59. [string]$Name,
  60. [string]$Value
  61. )
  62. $Arguments.Add($Name)
  63. $Arguments.Add((ConvertTo-SingleQuotedPowerShell $Value))
  64. }
  65. function Copy-ReleaseSource {
  66. param(
  67. [string]$From,
  68. [string]$To
  69. )
  70. New-Item -ItemType Directory -Force -Path $To | Out-Null
  71. $excludedNames = @('.git', '.deployment', 'releases')
  72. Get-ChildItem -LiteralPath $From -Force | ForEach-Object {
  73. if ($excludedNames -notcontains $_.Name) {
  74. Copy-Item -LiteralPath $_.FullName -Destination $To -Recurse -Force
  75. }
  76. }
  77. }
  78. $SourcePath = [System.IO.Path]::GetFullPath($SourcePath)
  79. $installerPath = Join-Path $PSScriptRoot 'install-iis-release.ps1'
  80. if (-not (Test-Path -LiteralPath $installerPath -PathType Leaf)) {
  81. throw "Host installer is missing: $installerPath"
  82. }
  83. $requiredSourceFiles = @(
  84. 'public\Default.asp',
  85. 'public\web.config',
  86. 'core\autoload_core.asp',
  87. 'app\controllers\autoload_controllers.asp'
  88. )
  89. foreach ($relativePath in $requiredSourceFiles) {
  90. if (-not (Test-Path -LiteralPath (Join-Path $SourcePath $relativePath) -PathType Leaf)) {
  91. throw "Source tree is incomplete; missing $relativePath"
  92. }
  93. }
  94. try {
  95. [xml](Get-Content -LiteralPath (Join-Path $SourcePath 'public\web.config') -Raw) | Out-Null
  96. } catch {
  97. throw "Source public\web.config is not valid XML: $($_.Exception.Message)"
  98. }
  99. $directGitRoot = Test-Path -LiteralPath (Join-Path $SourcePath '.git')
  100. $commit = 'nogit'
  101. if ($directGitRoot) {
  102. Assert-Command 'git'
  103. $branch = (& git -C $SourcePath branch --show-current).Trim()
  104. if ($LASTEXITCODE -ne 0) {
  105. throw 'Could not determine the Git branch.'
  106. }
  107. if ((-not $AllowAnyBranch) -and $branch -ne $ExpectedBranch) {
  108. throw "Refusing to deploy branch '$branch'; expected '$ExpectedBranch'. Use -AllowAnyBranch only for an intentional exception."
  109. }
  110. $dirty = & git -C $SourcePath status --porcelain
  111. if ($LASTEXITCODE -ne 0) {
  112. throw 'Could not inspect the Git worktree.'
  113. }
  114. if ((-not $AllowDirty) -and $null -ne $dirty -and @($dirty).Count -gt 0) {
  115. throw 'Refusing to deploy a dirty worktree. Commit/stash changes or use -AllowDirty for an intentional, auditable exception.'
  116. }
  117. $commit = (& git -C $SourcePath rev-parse --short=12 HEAD).Trim()
  118. if ($LASTEXITCODE -ne 0) {
  119. throw 'Could not determine the Git commit.'
  120. }
  121. Write-Host "Source branch: $branch"
  122. Write-Host "Source commit: $commit"
  123. } else {
  124. Write-Warning 'SourcePath is not a standalone Git checkout; branch and dirty-worktree checks cannot be enforced.'
  125. if (-not $AllowAnyBranch) {
  126. throw 'Use a standalone CI checkout, or pass -AllowAnyBranch explicitly for a reviewed non-Git source tree.'
  127. }
  128. }
  129. if ([string]::IsNullOrWhiteSpace($ReleaseId)) {
  130. $ReleaseId = (Get-Date -Format 'yyyyMMdd-HHmmss') + '-' + $commit
  131. }
  132. if ($ReleaseId -notmatch '^[A-Za-z0-9._-]+$') {
  133. throw 'ReleaseId may contain only letters, numbers, dot, underscore, and hyphen.'
  134. }
  135. if ($Rollback -and [string]::IsNullOrWhiteSpace($RollbackTo)) {
  136. throw '-Rollback requires -RollbackTo <release-id>.'
  137. }
  138. if ((-not $Rollback) -and -not [string]::IsNullOrWhiteSpace($RollbackTo)) {
  139. throw '-RollbackTo is only valid with -Rollback.'
  140. }
  141. if ($RunMigrations -and $Rollback) {
  142. throw '-RunMigrations is not valid during rollback.'
  143. }
  144. $workRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('asp-iis-deploy-' + [Guid]::NewGuid().ToString('N'))
  145. $packageStage = Join-Path $workRoot 'package'
  146. $packagePath = Join-Path $workRoot ($ReleaseId + '.zip')
  147. $remoteDirectory = 'C:\Windows\Temp\asp-iis-deploy-' + $ReleaseId
  148. $remotePackage = $remoteDirectory + '\' + $ReleaseId + '.zip'
  149. $remoteInstaller = $remoteDirectory + '\install-iis-release.ps1'
  150. try {
  151. if (-not $Rollback -and -not $RemotePreflightOnly) {
  152. Write-Step 'Staging the full repository for packaging'
  153. Copy-ReleaseSource -From $SourcePath -To $packageStage
  154. Add-Type -AssemblyName System.IO.Compression.FileSystem
  155. [System.IO.Compression.ZipFile]::CreateFromDirectory(
  156. $packageStage,
  157. $packagePath,
  158. [System.IO.Compression.CompressionLevel]::Optimal,
  159. $false
  160. )
  161. $sha256 = (Get-FileHash -LiteralPath $packagePath -Algorithm SHA256).Hash
  162. Write-Host "Package: $packagePath"
  163. Write-Host "SHA-256: $sha256"
  164. } else {
  165. $sha256 = ''
  166. }
  167. $remoteArguments = New-Object 'System.Collections.Generic.List[string]'
  168. $remoteArguments.Add('&')
  169. $remoteArguments.Add((ConvertTo-SingleQuotedPowerShell $remoteInstaller))
  170. Add-RemoteArgument -Arguments $remoteArguments -Name '-SiteName' -Value $SiteName
  171. if ($Rollback) {
  172. Add-RemoteArgument -Arguments $remoteArguments -Name '-RollbackTo' -Value $RollbackTo
  173. } elseif (-not $RemotePreflightOnly) {
  174. Add-RemoteArgument -Arguments $remoteArguments -Name '-PackagePath' -Value $remotePackage
  175. Add-RemoteArgument -Arguments $remoteArguments -Name '-ReleaseId' -Value $ReleaseId
  176. Add-RemoteArgument -Arguments $remoteArguments -Name '-ExpectedSha256' -Value $sha256
  177. } else {
  178. $remoteArguments.Add('-PreflightOnly')
  179. }
  180. if (-not [string]::IsNullOrWhiteSpace($DeployRoot)) {
  181. Add-RemoteArgument -Arguments $remoteArguments -Name '-DeployRoot' -Value $DeployRoot
  182. }
  183. if (-not [string]::IsNullOrWhiteSpace($BaseUrl)) {
  184. Add-RemoteArgument -Arguments $remoteArguments -Name '-BaseUrl' -Value $BaseUrl
  185. }
  186. Add-RemoteArgument -Arguments $remoteArguments -Name '-KeepReleases' -Value $KeepReleases.ToString()
  187. if ($RunMigrations) { $remoteArguments.Add('-RunMigrations') }
  188. if ($SkipSmokeTest) { $remoteArguments.Add('-SkipSmokeTest') }
  189. $remoteScript = $remoteArguments -join ' '
  190. $remoteBytes = [Text.Encoding]::Unicode.GetBytes($remoteScript)
  191. $remoteEncodedCommand = [Convert]::ToBase64String($remoteBytes)
  192. $remoteCommand = 'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy RemoteSigned -EncodedCommand ' + $remoteEncodedCommand
  193. Write-Host "Remote target: $RemoteTarget (Tailscale/OpenSSH port $RemotePort)"
  194. if ($DryRun) {
  195. Write-Step 'Dry-run complete; no network connection was made'
  196. Write-Host "Would create remote directory: $remoteDirectory"
  197. Write-Host "Would copy installer: $installerPath"
  198. if (-not $Rollback -and -not $RemotePreflightOnly) {
  199. Write-Host "Would copy package: $packagePath"
  200. }
  201. Write-Host ('Would execute host script: ' + $remoteScript)
  202. Write-Host ('Transport command uses PowerShell -EncodedCommand to avoid remote-shell quoting ambiguity.')
  203. exit 0
  204. }
  205. Assert-Command $SshExe
  206. Assert-Command $ScpExe
  207. Write-Step 'Creating remote staging directory over Tailscale/OpenSSH'
  208. $mkdirScript = "New-Item -ItemType Directory -Force -Path $(ConvertTo-SingleQuotedPowerShell $remoteDirectory) | Out-Null"
  209. $mkdirBytes = [Text.Encoding]::Unicode.GetBytes($mkdirScript)
  210. $mkdirEncodedCommand = [Convert]::ToBase64String($mkdirBytes)
  211. & $SshExe -p $RemotePort $RemoteTarget ('powershell.exe -NoProfile -NonInteractive -EncodedCommand ' + $mkdirEncodedCommand)
  212. if ($LASTEXITCODE -ne 0) { throw 'Remote directory creation failed.' }
  213. Write-Step 'Copying the host installer'
  214. & $ScpExe -P $RemotePort $installerPath ($RemoteTarget + ':' + $remoteDirectory.Replace('\', '/') + '/install-iis-release.ps1')
  215. if ($LASTEXITCODE -ne 0) { throw 'Installer copy failed.' }
  216. if (-not $Rollback -and -not $RemotePreflightOnly) {
  217. Write-Step 'Copying the release package'
  218. & $ScpExe -P $RemotePort $packagePath ($RemoteTarget + ':' + $remoteDirectory.Replace('\', '/') + '/' + $ReleaseId + '.zip')
  219. if ($LASTEXITCODE -ne 0) { throw 'Package copy failed.' }
  220. }
  221. Write-Step 'Invoking the host-side installer'
  222. & $SshExe -p $RemotePort $RemoteTarget $remoteCommand
  223. if ($LASTEXITCODE -ne 0) { throw "Remote installer failed with exit code $LASTEXITCODE." }
  224. Write-Step 'Remote operation completed successfully'
  225. } finally {
  226. if ($KeepPackage -and (Test-Path -LiteralPath $packagePath)) {
  227. $keptPath = Join-Path (Get-Location) ([System.IO.Path]::GetFileName($packagePath))
  228. Copy-Item -LiteralPath $packagePath -Destination $keptPath -Force
  229. Write-Host "Package retained at $keptPath"
  230. }
  231. if (Test-Path -LiteralPath $workRoot) {
  232. Remove-Item -LiteralPath $workRoot -Recurse -Force
  233. }
  234. }

Powered by TurnKey Linux.