#requires -Version 5 <# .SYNOPSIS One-shot deploy: build launcher + server, regenerate manifest, mirror the deploy share, scp the server binary. .DESCRIPTION Reads `deploy.config.ps1` (sibling file, gitignored) for local paths + SSH details. Stages run in order; -Stage limits which stages run. The script does NOT auto-restart the production daemon. After a server binary deploy it prompts you to do that yourself. .PARAMETER Stage All | Launcher | Server | Pack. Defaults to All. Launcher = build launcher + regenerate manifest + push to deploy share Server = build server + scp binary (atomic swap) Pack = regenerate manifest + mirror pack/overrides/* to share All = everything, in order .PARAMETER SkipBuild Skip dotnet publish steps. Use when you've already built and just want to push artifacts. .PARAMETER DryRun Print each action without executing. No files copied, no SSH, no build. #> [CmdletBinding()] param( [ValidateSet('All','Launcher','Server','Pack')] [string]$Stage = 'All', [switch]$SkipBuild, [switch]$DryRun, # Skip the version-bump check. Use only for cosmetic/internal-only changes # where you're SURE clients don't need to re-sync. The default is to refuse # if the local pack/launcher version matches what's already deployed. [switch]$Force ) $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' # ─── Resolve repo + load config ──────────────────────────────────────────── $here = Split-Path -Parent $MyInvocation.MyCommand.Path $repoRoot = Resolve-Path (Join-Path $here '..') $cfgPath = Join-Path $here 'deploy.config.ps1' if (-not (Test-Path $cfgPath)) { throw "Missing $cfgPath. Copy deploy.config.template.ps1 -> deploy.config.ps1 and fill in real values." } . $cfgPath # Sanity-check required vars actually got set (template ships with CHANGE_ME). foreach ($v in 'DeployShare','ServerSshHost','ServerSshKey','ServerBinaryRemote') { $val = Get-Variable -Name $v -ValueOnly -ErrorAction Stop if ($val -match 'CHANGE_ME' -or [string]::IsNullOrWhiteSpace($val)) { throw "deploy.config.ps1 has placeholder/empty `$$v. Fill it in." } } $shouldRunLauncher = $Stage -in @('All','Launcher') $shouldRunServer = $Stage -in @('All','Server') $shouldRunPack = $Stage -in @('All','Launcher','Pack') # manifest needs launcher exe meta # ─── Pre-flight: version-bump check ──────────────────────────────────────── # The launcher caches the pack by version: a client that already synced # pack v0.9.2 will short-circuit at "already on 0.9.2" if you re-deploy # under the same version, even if the file list / SHAs changed. Same idea # applies to launcherVersion (drives the in-launcher upgrade banner). # We fetch the currently-deployed manifest and refuse to deploy if the # matching version field hasn't been bumped. Use -Force to override (e.g. # for cosmetic-only changes where re-sync isn't needed). if (($shouldRunPack -or $shouldRunLauncher) -and -not $DryRun -and -not $Force) { if (-not $ManifestPublicUrl -or $ManifestPublicUrl -match 'CHANGE_ME') { throw "deploy.config.ps1 is missing `$ManifestPublicUrl. Set it to the deployed manifest URL (e.g. https://example.com/pack/manifest.json)." } $deployed = $null try { $deployed = Invoke-RestMethod -Uri $ManifestPublicUrl -TimeoutSec 8 } catch { Write-Host "Pre-flight: couldn't fetch deployed manifest at $ManifestPublicUrl -- version check skipped (probably first deploy)." -ForegroundColor DarkGray } if ($deployed) { $errs = @() # Pack version check applies only when the user is explicitly deploying # pack content (Stage = All or Pack). A launcher-only deploy intentionally # leaves pack content alone, so the pack version SHOULD stay constant. $strictPackCheck = $Stage -in @('All','Pack') if ($strictPackCheck) { $lock = Get-Content (Join-Path $repoRoot 'pack\pack.lock.json') -Raw | ConvertFrom-Json if ($deployed.version -eq $lock.version) { $errs += "Pack version is unchanged ($($lock.version)). Clients cached at that version will SKIP the sync -- they won't pick up your pack changes. Bump 'version' in pack/pack.lock.json before deploying." } } if ($shouldRunLauncher) { $csprojPath = Join-Path $repoRoot 'launcher\ModpackLauncher.csproj' [xml]$csproj = Get-Content $csprojPath $localLauncherVersion = ($csproj.Project.PropertyGroup.Version | Where-Object { $_ }) | Select-Object -First 1 # Manifest stores 4-part FileVersion (e.g. "0.4.4.0"); csproj is 3-part ("0.4.4"). Compare normalised. $deployedNorm = ($deployed.launcherVersion -replace '\.0+$','') $localNorm = ($localLauncherVersion -replace '\.0+$','') if ($deployedNorm -eq $localNorm -and $localNorm) { $errs += "Launcher version is unchanged ($localLauncherVersion). Existing 0.4.x installs won't see an upgrade prompt -- bump in launcher/ModpackLauncher.csproj before deploying." } } if ($errs.Count -gt 0) { Write-Host "" Write-Host "VERSION-BUMP CHECK FAILED:" -ForegroundColor Red foreach ($e in $errs) { Write-Host " - $e" -ForegroundColor Red } Write-Host "" Write-Host "If you're re-deploying without any user-visible changes, pass -Force to skip this check." -ForegroundColor DarkGray exit 1 } } } # ─── Pre-flight: was the daemon already running? ─────────────────────────── # We don't auto-stop it (kicks active players to fix a problem that isn't # actually broken -- the atomic swap is safe with the daemon running). But # knowing the state up front lets us tailor the final "next steps" message # so a deploy success doesn't silently leave you on the old code. $daemonWasRunning = $false if ($shouldRunServer) { Write-Host "Pre-flight: checking daemon state on $ServerSshHost..." -ForegroundColor DarkGray # Single-quoted PS string so PowerShell doesn't try to interpret the # bash-side metacharacters. The remote shell sees the literal pgrep # command; the trailing $ anchors so we don't match run.sh wrappers. $remoteCmd = 'pgrep -f /brass-sigil-server$ 2>/dev/null' $pgrepOut = & ssh -i $ServerSshKey -o ConnectTimeout=5 -o BatchMode=yes $ServerSshHost $remoteCmd 2>$null $daemonWasRunning = -not [string]::IsNullOrWhiteSpace($pgrepOut) if ($daemonWasRunning) { Write-Host " Daemon is RUNNING. Atomic swap is safe -- but you'll need to" -ForegroundColor Yellow Write-Host " stop+start it after deploy for the new code to take effect." -ForegroundColor Yellow } else { Write-Host " Daemon is stopped. New binary will run as soon as you start it." -ForegroundColor DarkGray } } # ─── Helpers ─────────────────────────────────────────────────────────────── $stepNum = 0 function Step($desc, [scriptblock]$body) { $script:stepNum++ $start = Get-Date Write-Host "" Write-Host ("[{0}] {1}" -f $script:stepNum, $desc) -ForegroundColor Cyan if ($DryRun) { Write-Host " (dry-run, skipping)" -ForegroundColor DarkGray return } & $body $elapsed = (Get-Date) - $start Write-Host (" done in {0:N1}s" -f $elapsed.TotalSeconds) -ForegroundColor DarkGray } # ─── Stage 1: build launcher ─────────────────────────────────────────────── $launcherExe = Join-Path $repoRoot (Join-Path $LauncherPublishDir $LauncherExeName) if ($shouldRunLauncher -and -not $SkipBuild) { Step "Build launcher (dotnet publish launcher\)" { Push-Location (Join-Path $repoRoot 'launcher') try { dotnet publish -c Release -nologo | Out-Host } finally { Pop-Location } if (-not (Test-Path $launcherExe)) { throw "Launcher publish didn't produce $launcherExe" } } } # ─── Stage 2: build server ───────────────────────────────────────────────── $serverExe = Join-Path $repoRoot (Join-Path $ServerPublishDir $ServerExeName) if ($shouldRunServer -and -not $SkipBuild) { Step "Build server (dotnet publish server\ -r linux-x64)" { Push-Location (Join-Path $repoRoot 'server') try { dotnet publish -c Release -r linux-x64 --self-contained true -nologo | Out-Host } finally { Pop-Location } if (-not (Test-Path $serverExe)) { throw "Server publish didn't produce $serverExe" } } } # ─── Stage 3: regenerate manifest ────────────────────────────────────────── $manifestPath = Join-Path $here 'manifest.json' if ($shouldRunPack) { Step "Regenerate manifest (Build-Pack.ps1)" { $args = @{ OutputPath = $manifestPath } if ($shouldRunLauncher -and (Test-Path $launcherExe)) { $args.LauncherExePath = $launcherExe } & (Join-Path $here 'Build-Pack.ps1') @args | Out-Host } } # ─── Stage 4: mirror pack overrides to share ────────────────────────────── $overridesLocal = Join-Path $repoRoot 'pack\overrides' $shareFiles = Join-Path $DeployShare 'files' if ($shouldRunPack -and (Test-Path $overridesLocal)) { Step "Mirror pack/overrides/ -> $shareFiles" { # /MIR makes destination match source (deletes orphan files in $shareFiles). # /XJ skips junctions, /R:1 /W:1 keeps retry behaviour sane on flaky shares. robocopy $overridesLocal $shareFiles /MIR /XJ /R:1 /W:1 /NFL /NDL /NJH /NJS /NP | Out-Host # Robocopy returns 0-7 for success-with-info; 8+ is real failure. if ($LASTEXITCODE -ge 8) { throw "robocopy failed with exit $LASTEXITCODE" } } } # ─── Stage 5: deploy launcher exe + manifest to share ────────────────────── if ($shouldRunLauncher) { Step "Copy launcher.exe + manifest to $DeployShare" { Copy-Item $launcherExe (Join-Path $DeployShare $LauncherDeployedAs) -Force Copy-Item $manifestPath (Join-Path $DeployShare 'manifest.json') -Force } } # ─── Stage 6: scp + atomic swap server binary ────────────────────────────── if ($shouldRunServer) { Step "scp server binary -> $ServerSshHost`:$ServerBinaryRemote (atomic swap)" { $remoteNew = "$ServerBinaryRemote.new" & scp -i $ServerSshKey -o ConnectTimeout=15 $serverExe "$ServerSshHost`:$remoteNew" if ($LASTEXITCODE -ne 0) { throw "scp failed with exit $LASTEXITCODE" } $cmd = "chmod +x '$remoteNew' && mv '$remoteNew' '$ServerBinaryRemote' && md5sum '$ServerBinaryRemote'" & ssh -i $ServerSshKey -o ConnectTimeout=10 $ServerSshHost $cmd if ($LASTEXITCODE -ne 0) { throw "ssh swap failed with exit $LASTEXITCODE" } } Write-Host "" if ($daemonWasRunning) { Write-Host "Server binary swapped on disk, but the daemon was running before this" -ForegroundColor Yellow Write-Host "deploy and is still on the OLD code in memory (Linux preserves the running" -ForegroundColor Yellow Write-Host "inode through rename). Stop + start the daemon to pick up the new build." -ForegroundColor Yellow } else { Write-Host "Server binary swapped on disk. Daemon was stopped -- start it whenever" -ForegroundColor Green Write-Host "you're ready and it'll run the new build." -ForegroundColor Green } } Write-Host "" Write-Host "Deploy finished." -ForegroundColor Green