Initial commit: Brass & Sigil monorepo
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.
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user