a1331212cb
Self-hosted Minecraft modpack distribution + administration system.
- launcher/ Avalonia 12 desktop client; single-file win-x64 publish.
Microsoft auth via XboxAuthNet, manifest+SHA-1 mod sync,
portable install path, sidecar settings.
- server/ brass-sigil-server daemon (.NET 8, linux-x64). Wraps the
MC subprocess, embedded Kestrel admin panel with cookie
auth + rate limiting, RCON bridge, scheduled backups,
BlueMap CLI integration with player markers + skin proxy,
friend-side whitelist request flow, world wipe with seed
selection (keep current / random / custom).
- pack/ pack.lock.json (Modrinth + manual CurseForge entries),
data-only tweak source under tweaks/, build outputs in
overrides/ (gitignored).
- scripts/ Build-Pack / Build-Tweaks / Update-Pack / Check-Updates
plus Deploy-Brass.ps1 unified one-shot deploy with
version-bump pre-flight and daemon-state detection.
232 lines
12 KiB
PowerShell
232 lines
12 KiB
PowerShell
#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 <Version> 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 <Version> 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
|