param( [switch]$Build, [int]$BackupRetention = 0, [string]$SshUser = "y2keui", [string]$SshHost = "comtropy.synology.me", [int]$SshPort = 50022, [string]$SshKey = "$env:USERPROFILE\.ssh\nas_msix_ed25519", [string]$RemoteRoot = "/volume1/web", [string]$SiteName = "heros_web" ) $ErrorActionPreference = "Stop" Set-StrictMode -Version Latest function Assert-Command { param([string]$Name) if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) { throw "Required command not found: $Name" } } function Quote-Sh { param([string]$Value) return "'" + ($Value -replace "'", "'\''") + "'" } function Get-ObjectPropertyValue { param( [object]$Object, [string]$Name ) if ($null -eq $Object) { return $null } $property = $Object.PSObject.Properties[$Name] if ($null -eq $property) { return $null } return $property.Value } function Test-ReleaseManifestRenderingGate { param([object]$Manifest) $renderingGate = Get-ObjectPropertyValue $Manifest "renderingGate" $viewport = Get-ObjectPropertyValue $renderingGate "viewport" return ( (Get-ObjectPropertyValue $renderingGate "requiredRenderer") -eq "webgl" -and (Get-ObjectPropertyValue $renderingGate "compatibilityRenderer") -eq "canvas" -and (Get-ObjectPropertyValue $viewport "width") -eq 1920 -and (Get-ObjectPropertyValue $viewport "height") -eq 1080 -and (Get-ObjectPropertyValue $viewport "deviceScaleFactor") -eq 1 ) } Assert-Command "ssh" Assert-Command "scp" Assert-Command "git" if ($Build) { Assert-Command "pnpm" pnpm build if ($LASTEXITCODE -ne 0) { throw "pnpm build failed with exit code $LASTEXITCODE" } } $dist = Join-Path (Get-Location) "dist" $index = Join-Path $dist "index.html" $assets = Join-Path $dist "assets" if (-not (Test-Path $index -PathType Leaf)) { throw "Missing dist/index.html. Run pnpm build first or pass -Build." } if (-not (Test-Path $assets -PathType Container)) { throw "Missing dist/assets. Run pnpm build first or pass -Build." } if ($BackupRetention -lt 0) { throw "BackupRetention must be 0 or greater." } $timestamp = Get-Date -Format "yyyyMMddHHmmss" $fullCommit = (git rev-parse HEAD).Trim() $commit = (git rev-parse --short=8 HEAD).Trim() $qaVersion = if ($env:PUBLIC_QA_VERSION) { $env:PUBLIC_QA_VERSION } else { "deploy-$timestamp" } $releaseManifestPath = Join-Path $dist "release-manifest.json" $writeReleaseManifest = $true if (Test-Path $releaseManifestPath -PathType Leaf) { try { $existingReleaseManifest = Get-Content -LiteralPath $releaseManifestPath -Raw | ConvertFrom-Json if ($existingReleaseManifest.app -ne "heros_web") { throw "Existing release manifest has unexpected app '$($existingReleaseManifest.app)'." } if ( $existingReleaseManifest.commit -eq $fullCommit -and (Test-ReleaseManifestRenderingGate $existingReleaseManifest) ) { $writeReleaseManifest = $false Write-Host "Using existing $releaseManifestPath for $commit" } elseif ($existingReleaseManifest.commit -eq $fullCommit) { Write-Host "Replacing incomplete $releaseManifestPath for $commit" } else { Write-Host "Replacing stale $releaseManifestPath commit $($existingReleaseManifest.commit) with $commit" } } catch { throw "Invalid existing release manifest at $releaseManifestPath. $_" } } if ($writeReleaseManifest) { $releaseManifestJson = @{ app = "heros_web" commit = $fullCommit shortCommit = $commit qaVersion = $qaVersion generatedAt = (Get-Date).ToUniversalTime().ToString("o") renderingGate = @{ requiredRenderer = "webgl" compatibilityRenderer = "canvas" viewport = @{ width = 1920 height = 1080 deviceScaleFactor = 1 } } } | ConvertTo-Json -Depth 4 $utf8NoBom = New-Object System.Text.UTF8Encoding $false [System.IO.File]::WriteAllText($releaseManifestPath, "$releaseManifestJson`n", $utf8NoBom) Write-Host "Wrote $releaseManifestPath for $commit ($qaVersion)" } $userHost = "$SshUser@$SshHost" $remoteTemp = "$RemoteRoot/.$($SiteName)_deploy_$timestamp" $remotePrevious = "$RemoteRoot/.$($SiteName)_previous_$timestamp" $remoteBackup = "$RemoteRoot/$SiteName.backup-$timestamp-$commit" $quotedRemoteRoot = Quote-Sh $RemoteRoot $quotedSiteName = Quote-Sh $SiteName $quotedTemp = Quote-Sh $remoteTemp $quotedPrevious = Quote-Sh $remotePrevious $quotedBackup = Quote-Sh $remoteBackup $retention = $BackupRetention $prepareRemote = @" set -e remote_root=$quotedRemoteRoot temp=$quotedTemp site_name=$quotedSiteName test -d "`$remote_root" rm -rf -- "`$temp" mkdir -p -- "`$temp" printf 'Prepared %s/%s via %s\n' "`$remote_root" "`$site_name" "`$temp" "@ ssh -o BatchMode=yes -o ConnectTimeout=8 -i $SshKey -p $SshPort $userHost $prepareRemote if ($LASTEXITCODE -ne 0) { throw "Failed to prepare remote deploy directory." } scp -O -i $SshKey -P $SshPort -r "$dist\*" "$userHost`:$remoteTemp/" if ($LASTEXITCODE -ne 0) { throw "Failed to upload dist to remote deploy directory." } $installRemote = @" set -e remote_root=$quotedRemoteRoot site_name=$quotedSiteName temp=$quotedTemp previous=$quotedPrevious backup=$quotedBackup retention=$retention site="`$remote_root/`$site_name" test -f "`$temp/index.html" test -d "`$temp/assets" rm -rf -- "`$previous" if [ -d "`$site" ]; then mv -- "`$site" "`$previous" fi if ! mv -- "`$temp" "`$site"; then if [ -d "`$previous" ]; then mv -- "`$previous" "`$site" fi exit 1 fi chmod -R a+rX "`$site" if [ -d "`$previous" ]; then if [ "`$retention" -gt 0 ]; then mv -- "`$previous" "`$backup" else rm -rf -- "`$previous" fi fi find "`$remote_root" -maxdepth 1 -type d \( -name ".`${site_name}_deploy_*" -o -name "`${site_name}_deploy_*" -o -name ".`${site_name}_previous_*" \) -exec rm -rf -- {} + if [ "`$retention" -eq 0 ]; then find "`$remote_root" -maxdepth 1 -type d -name "`${site_name}.backup-*" -exec rm -rf -- {} + else keep_count=`$retention find "`$remote_root" -maxdepth 1 -type d -name "`${site_name}.backup-*" -printf '%f\n' | sort -r | tail -n +`$((keep_count + 1)) | while IFS= read -r old_backup; do [ -n "`$old_backup" ] && rm -rf -- "`$remote_root/`$old_backup" done fi printf 'Deployed %s at %s\n' "`$site_name" "`$site" printf 'Backup retention: %s\n' "`$retention" printf 'Remaining site directories:\n' find "`$remote_root" -maxdepth 1 -type d -name "*`${site_name}*" -printf '%f\n' | sort "@ ssh -o BatchMode=yes -o ConnectTimeout=8 -i $SshKey -p $SshPort $userHost $installRemote if ($LASTEXITCODE -ne 0) { throw "Failed to install remote deploy directory." }