|
- [CmdletBinding()]
- param(
- [Parameter(Mandatory = $true)]
- [string]$RemoteHost,
-
- [Parameter(Mandatory = $true)]
- [string]$RemoteProjectPath,
-
- [string]$ProductionSiteName = 'WscMvc',
- [string]$ProductionPoolName = 'WscMvc',
- [int]$ProductionPort = 8090,
- [string]$ProductionBaseUrl = 'http://localhost:8090',
-
- [string]$TestSiteName = 'WscMvcTests',
- [string]$TestPoolName = 'WscMvcTests',
- [int]$TestPort = 8091,
- [string]$TestBaseUrl = 'http://localhost:8091',
-
- [string]$RemoteTempRoot = 'C:\Windows\Temp',
- [switch]$PullOriginMaster,
- [switch]$RunTests
- )
-
- $ErrorActionPreference = 'Stop'
- Set-StrictMode -Version 2.0
-
- function Invoke-Native {
- param(
- [Parameter(Mandatory = $true)][string]$FilePath,
- [Parameter(Mandatory = $true)][string[]]$ArgumentList
- )
-
- & $FilePath @ArgumentList
- if ($LASTEXITCODE -ne 0) {
- throw "$FilePath failed with exit code $LASTEXITCODE"
- }
- }
-
- function Quote-PowerShellLiteral {
- param([Parameter(Mandatory = $true)][string]$Value)
- return "'" + $Value.Replace("'", "''") + "'"
- }
-
- $projectRoot = Split-Path -Parent $PSScriptRoot
- $gitRoot = (& git -C $projectRoot rev-parse --show-toplevel 2>$null)
- if ($LASTEXITCODE -ne 0 -or -not $gitRoot) {
- throw "Project root is not a Git checkout: $projectRoot"
- }
- $gitRoot = [System.IO.Path]::GetFullPath($gitRoot.Trim())
- if ($gitRoot.TrimEnd('\') -ne ([System.IO.Path]::GetFullPath($projectRoot)).TrimEnd('\')) {
- throw "Deploy-Remote.ps1 must run from this repository checkout (expected $projectRoot, Git reported $gitRoot)."
- }
-
- if ($ProductionSiteName -eq $TestSiteName) { throw 'ProductionSiteName and TestSiteName must differ.' }
- if ($ProductionPoolName -eq $TestPoolName) { throw 'ProductionPoolName and TestPoolName must differ.' }
- if ($ProductionPort -lt 1 -or $ProductionPort -gt 65535) { throw 'ProductionPort must be between 1 and 65535.' }
- if ($TestPort -lt 1 -or $TestPort -gt 65535) { throw 'TestPort must be between 1 and 65535.' }
- if ($ProductionPort -eq $TestPort) { throw 'ProductionPort and TestPort must differ.' }
-
- if ($PullOriginMaster) {
- $dirty = (& git -C $gitRoot status --porcelain)
- if ($LASTEXITCODE -ne 0) { throw 'Unable to inspect Git worktree state.' }
- if ($dirty) {
- throw 'Refusing to pull with local changes. Commit, stash, or omit -PullOriginMaster.'
- }
- Invoke-Native -FilePath 'git' -ArgumentList @('-C', $gitRoot, 'pull', '--ff-only', 'origin', 'master')
- }
-
- $trackedFiles = @(& git -C $gitRoot ls-files)
- if ($LASTEXITCODE -ne 0 -or $trackedFiles.Count -eq 0) {
- throw 'git ls-files did not return the tracked deployment set.'
- }
-
- $required = @(
- 'tools/Register-Components.ps1',
- 'tools/Setup-Site.ps1',
- 'tools/Invoke-RemoteInstall.ps1',
- 'tests/Invoke-SelfTest.ps1',
- 'tests/Test-Http.ps1',
- 'Framework/ViewRenderer.wsc'
- )
- foreach ($requiredPath in $required) {
- if ($trackedFiles -notcontains $requiredPath) {
- throw "Required deployment file is not tracked by Git: $requiredPath"
- }
- if (-not (Test-Path -LiteralPath (Join-Path $gitRoot $requiredPath))) {
- throw "Required deployment file is missing: $requiredPath"
- }
- }
-
- $invocationId = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ') + '-' + [Guid]::NewGuid().ToString('N').Substring(0, 8)
- $localWork = Join-Path ([System.IO.Path]::GetTempPath()) "wsc-mvc-deploy-$invocationId"
- $packageRoot = Join-Path $localWork 'package'
- $archivePath = Join-Path $localWork 'wsc-mvc.zip'
- $remoteArchive = Join-Path $RemoteTempRoot "wsc-mvc-$invocationId.zip"
- $remoteInstaller = Join-Path $RemoteTempRoot "wsc-mvc-install-$invocationId.ps1"
-
- try {
- New-Item -ItemType Directory -Path $packageRoot -Force | Out-Null
-
- foreach ($relativePath in $trackedFiles) {
- if ([string]::IsNullOrWhiteSpace($relativePath)) { continue }
- if ([System.IO.Path]::IsPathRooted($relativePath) -or $relativePath -match '(^|[\\/])\.\.([\\/]|$)') {
- throw "Unsafe tracked path: $relativePath"
- }
-
- $source = Join-Path $gitRoot $relativePath
- $item = Get-Item -LiteralPath $source -Force
- if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
- throw "Refusing to package a reparse point or symbolic link: $relativePath"
- }
-
- $destination = Join-Path $packageRoot $relativePath
- $destinationParent = Split-Path -Parent $destination
- if (-not (Test-Path -LiteralPath $destinationParent)) {
- New-Item -ItemType Directory -Path $destinationParent -Force | Out-Null
- }
- Copy-Item -LiteralPath $source -Destination $destination -Force
- }
-
- Add-Type -AssemblyName System.IO.Compression.FileSystem
- [System.IO.Compression.ZipFile]::CreateFromDirectory($packageRoot, $archivePath, [System.IO.Compression.CompressionLevel]::Optimal, $false)
- $packageSha256 = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash.ToLowerInvariant()
-
- Write-Output "Package contains $($trackedFiles.Count) Git-tracked paths."
- Write-Output "Package SHA-256: $packageSha256"
- Write-Output "Inspecting and deploying to $RemoteHost without supplying credentials..."
-
- Invoke-Native -FilePath 'scp' -ArgumentList @($archivePath, "$RemoteHost`:$remoteArchive")
- Invoke-Native -FilePath 'scp' -ArgumentList @((Join-Path $PSScriptRoot 'Invoke-RemoteInstall.ps1'), "$RemoteHost`:$remoteInstaller")
-
- $arguments = @(
- '-ArchivePath', (Quote-PowerShellLiteral $remoteArchive),
- '-ProjectPath', (Quote-PowerShellLiteral $RemoteProjectPath),
- '-InvocationId', (Quote-PowerShellLiteral $invocationId),
- '-ExpectedArchiveSha256', (Quote-PowerShellLiteral $packageSha256),
- '-ProductionSiteName', (Quote-PowerShellLiteral $ProductionSiteName),
- '-ProductionPoolName', (Quote-PowerShellLiteral $ProductionPoolName),
- '-ProductionPort', $ProductionPort,
- '-ProductionBaseUrl', (Quote-PowerShellLiteral $ProductionBaseUrl),
- '-TestSiteName', (Quote-PowerShellLiteral $TestSiteName),
- '-TestPoolName', (Quote-PowerShellLiteral $TestPoolName),
- '-TestPort', $TestPort,
- '-TestBaseUrl', (Quote-PowerShellLiteral $TestBaseUrl)
- )
- if ($RunTests) { $arguments += '-RunTests' }
-
- $remoteCommand = "& $(Quote-PowerShellLiteral $remoteInstaller) " + ($arguments -join ' ')
- $encodedCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($remoteCommand))
- Invoke-Native -FilePath 'ssh' -ArgumentList @($RemoteHost, "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encodedCommand")
- } finally {
- if (Test-Path -LiteralPath $localWork) {
- Remove-Item -LiteralPath $localWork -Recurse -Force
- }
- }
|