feat(launcher): Gitea-backed alpha update feed replaces the GitHub Releases source
The launcher reported "no client available" because its update source was
pinned to a GitHub Releases manifest in a PRIVATE repo — nothing anonymous
could ever be fetched from it. Switch the feed to the PUBLIC Gitea repo so a
friend needs no account, and add the two commands that publish it.
- ReleaseManifestClient.ProductionManifestUri now points at
git.snakedesert.se/erik/acdream raw on the `dist` branch. No update
machinery changed: the existing strict reader already accepts any HTTPS
manifest, so this is a URL swap plus a build script.
- tools/publish-bin.ps1 publishes the payloads into /bin and writes
bin/manifest.json (schema v1, SHA-256 + size per artifact):
client-win-x64.zip AcDream.App + acdream-headless
launcher-win-x64.zip acdream-launcher + co-deployed acdream-bake
Stamps InformationalVersion ONLY — never -p:Version, which also rewrites
project-reference versions inside the committed packages.<rid>.lock.json
files and churned every one of them with a throwaway build stamp.
- tools/publish-dist.ps1 pushes /bin to the Gitea-only `dist` branch from a
throwaway worktree, leaving the developer's checkout, index, and HEAD
untouched. It refuses a GitHub remote outright.
Why `dist` and not main: the launcher payload is ~103 MB because the launcher
and its co-deployed bake CLI are each self-contained single files (deliberate,
see AcDream.Launcher.csproj). GitHub hard-rejects files over 100 MB, and all
three refs currently track main, so payloads on main would break every GitHub
push. `dist` is a single-commit orphan branch that each publish REPLACES, so
superseded builds never accumulate. /bin stays gitignored repo-wide and is
force-added only on that branch.
Verified live: manifest and both payloads serve anonymously over HTTPS, and a
downloaded client payload matches its declared SHA-256 and size byte for byte.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
8be14d3992
commit
600c331ac6
5 changed files with 410 additions and 6 deletions
226
tools/publish-bin.ps1
Normal file
226
tools/publish-bin.ps1
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Publishes the alpha distribution feed into the repo's /bin folder.
|
||||
|
||||
.DESCRIPTION
|
||||
Builds self-contained client and launcher payloads, zips them, and writes
|
||||
bin/manifest.json pointing at this repository's PUBLIC Gitea raw URLs.
|
||||
Commit and push /bin afterwards and the launcher's "Check for updates"
|
||||
finds the new build — no release server, no tokens, no CDN.
|
||||
|
||||
Payload contents (what the launcher expects to find at each zip root):
|
||||
client-<rid>.zip AcDream.App[.exe] + acdream-headless[.exe]
|
||||
launcher-<rid>.zip acdream-launcher[.exe] + acdream-bake[.exe]
|
||||
(the launcher csproj co-deploys the bake CLI)
|
||||
|
||||
The launcher's own published version is stamped to the manifest version so
|
||||
a freshly published launcher does NOT report itself as out of date.
|
||||
|
||||
.PARAMETER Version
|
||||
SemVer 2.0 release version. Defaults to a monotonic UTC build stamp,
|
||||
e.g. 0.1.0-build.202608181552. Must sort ABOVE the previous published
|
||||
version or the launcher will not offer it as an update.
|
||||
|
||||
.PARAMETER IncludeLinux
|
||||
Also publish linux-x64 payloads. Off by default: Linux graphical is parked
|
||||
at Slice L1, and each RID roughly doubles build time and feed size.
|
||||
|
||||
.EXAMPLE
|
||||
pwsh -NoProfile -File tools/publish-bin.ps1
|
||||
git add bin
|
||||
git commit -m "release: alpha build"
|
||||
git push origin main
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Version,
|
||||
[switch]$IncludeLinux,
|
||||
[string]$MinimumLauncherVersion = '0.0.1'
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if ($PSVersionTable.PSVersion.Major -lt 7) {
|
||||
throw 'publish-bin requires PowerShell 7 or newer.'
|
||||
}
|
||||
|
||||
$RepoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
||||
if (-not (Test-Path (Join-Path $RepoRoot 'AcDream.slnx'))) {
|
||||
throw "Could not locate AcDream.slnx above '$PSScriptRoot'."
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Version)) {
|
||||
$Version = '0.1.0-build.{0}' -f ([DateTime]::UtcNow.ToString('yyyyMMddHHmm'))
|
||||
}
|
||||
|
||||
$semver = '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$'
|
||||
foreach ($candidate in @($Version, $MinimumLauncherVersion)) {
|
||||
if ($candidate -notmatch $semver) {
|
||||
throw "Version '$candidate' is not SemVer 2.0 (build metadata '+' is not allowed here)."
|
||||
}
|
||||
}
|
||||
|
||||
# Raw-file base for the PUBLIC Gitea repo's dist branch. Must agree with
|
||||
# ReleaseManifestClient.ProductionManifestUri.
|
||||
$RawBase = 'https://git.snakedesert.se/erik/acdream/raw/branch/dist/bin'
|
||||
|
||||
$BinRoot = Join-Path $RepoRoot 'bin'
|
||||
$Staging = Join-Path $BinRoot 'payload'
|
||||
$Rids = @('win-x64')
|
||||
if ($IncludeLinux) { $Rids += 'linux-x64' }
|
||||
|
||||
Write-Host "acdream alpha feed" -ForegroundColor Cyan
|
||||
Write-Host " version : $Version"
|
||||
Write-Host " rids : $($Rids -join ', ')"
|
||||
Write-Host " output : $BinRoot"
|
||||
Write-Host ''
|
||||
|
||||
if (Test-Path $Staging) { Remove-Item -LiteralPath $Staging -Recurse -Force }
|
||||
$null = New-Item -ItemType Directory -Path $Staging -Force
|
||||
|
||||
function Invoke-Publish {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Project,
|
||||
[Parameter(Mandatory)][string]$Rid,
|
||||
[Parameter(Mandatory)][string]$OutputDirectory,
|
||||
[switch]$SingleFile
|
||||
)
|
||||
|
||||
$arguments = @(
|
||||
'publish', (Join-Path $RepoRoot $Project),
|
||||
'-c', 'Release',
|
||||
'-r', $Rid,
|
||||
'--self-contained', 'true',
|
||||
# InformationalVersion ONLY — never -p:Version. The launcher reads
|
||||
# AssemblyInformationalVersion (App.GetLauncherVersion), while Version
|
||||
# also rewrites project-reference versions inside the committed
|
||||
# packages.<rid>.lock.json files, so stamping it churned every lock
|
||||
# file with a throwaway build stamp (observed 2026-08-18).
|
||||
"-p:InformationalVersion=$Version",
|
||||
# No SourceLink '+<sha>' suffix: LauncherVersion parses this as SemVer.
|
||||
'-p:IncludeSourceRevisionInInformationalVersion=false',
|
||||
'-o', $OutputDirectory,
|
||||
'--nologo'
|
||||
)
|
||||
if ($SingleFile) { $arguments += '-p:PublishSingleFile=true' }
|
||||
|
||||
& dotnet @arguments | Out-Null
|
||||
if ($LASTEXITCODE) { throw "publish failed: $Project ($Rid)" }
|
||||
}
|
||||
|
||||
function New-PayloadZip {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$SourceDirectory,
|
||||
[Parameter(Mandatory)][string]$ZipPath,
|
||||
[Parameter(Mandatory)][string[]]$RequiredFiles
|
||||
)
|
||||
|
||||
foreach ($required in $RequiredFiles) {
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $SourceDirectory $required))) {
|
||||
throw "Payload '$SourceDirectory' is missing required file '$required'."
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $ZipPath) { Remove-Item -LiteralPath $ZipPath -Force }
|
||||
# includeBaseDirectory:$false -> entries sit at the zip ROOT, which is where
|
||||
# LauncherExecutableSet resolves the hosts after extraction.
|
||||
[IO.Compression.ZipFile]::CreateFromDirectory(
|
||||
$SourceDirectory,
|
||||
$ZipPath,
|
||||
[IO.Compression.CompressionLevel]::Optimal,
|
||||
$false)
|
||||
}
|
||||
|
||||
function Get-Artifact {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$ZipPath,
|
||||
[Parameter(Mandatory)][string]$Url
|
||||
)
|
||||
|
||||
$item = Get-Item -LiteralPath $ZipPath
|
||||
return [ordered]@{
|
||||
url = $Url
|
||||
sha256 = (Get-FileHash -LiteralPath $ZipPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
size = $item.Length
|
||||
}
|
||||
}
|
||||
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction SilentlyContinue
|
||||
|
||||
$clients = [ordered]@{}
|
||||
$launchers = [ordered]@{}
|
||||
|
||||
foreach ($rid in $Rids) {
|
||||
$suffix = if ($rid -like 'win-*') { '.exe' } else { '' }
|
||||
$clientDirectory = Join-Path $Staging "client-$rid"
|
||||
$launcherDirectory = Join-Path $Staging "launcher-$rid"
|
||||
|
||||
Write-Host "[$rid] publishing client (App + Headless)..." -ForegroundColor Yellow
|
||||
Invoke-Publish 'src/AcDream.App/AcDream.App.csproj' $rid $clientDirectory
|
||||
Invoke-Publish 'src/AcDream.Headless/AcDream.Headless.csproj' $rid $clientDirectory
|
||||
|
||||
# PublishSingleFile/SelfContained come from the launcher csproj itself, and
|
||||
# its PublishCoDeployedBakeTool target adds acdream-bake as a SECOND
|
||||
# self-contained single file in the same directory. Two independent .NET
|
||||
# runtimes is deliberate (see that csproj's comments: a framework-dependent
|
||||
# bake would scatter AcDream.Content assemblies into the launcher's output),
|
||||
# so this payload is ~103 MB and CANNOT be pushed to GitHub, whose hard
|
||||
# per-file limit is 100 MB. The alpha feed is Gitea-only by design.
|
||||
Write-Host "[$rid] publishing launcher (+ co-deployed bake)..." -ForegroundColor Yellow
|
||||
Invoke-Publish 'src/AcDream.Launcher/AcDream.Launcher.csproj' $rid $launcherDirectory
|
||||
|
||||
$clientZip = Join-Path $BinRoot "client-$rid.zip"
|
||||
$launcherZip = Join-Path $BinRoot "launcher-$rid.zip"
|
||||
|
||||
Write-Host "[$rid] packing..." -ForegroundColor Yellow
|
||||
New-PayloadZip $clientDirectory $clientZip @("AcDream.App$suffix", "acdream-headless$suffix")
|
||||
New-PayloadZip $launcherDirectory $launcherZip @("acdream-launcher$suffix", "acdream-bake$suffix")
|
||||
|
||||
$clients[$rid] = Get-Artifact $clientZip "$RawBase/client-$rid.zip"
|
||||
$launchers[$rid] = Get-Artifact $launcherZip "$RawBase/launcher-$rid.zip"
|
||||
}
|
||||
|
||||
$manifest = [ordered]@{
|
||||
schemaVersion = 1
|
||||
version = $Version
|
||||
minimumLauncherVersion = $MinimumLauncherVersion
|
||||
clients = $clients
|
||||
launchers = $launchers
|
||||
}
|
||||
|
||||
$manifestPath = Join-Path $BinRoot 'manifest.json'
|
||||
$json = $manifest | ConvertTo-Json -Depth 6
|
||||
[IO.File]::WriteAllText($manifestPath, $json + "`n", [Text.UTF8Encoding]::new($false))
|
||||
|
||||
Remove-Item -LiteralPath $Staging -Recurse -Force
|
||||
|
||||
Write-Host ''
|
||||
Write-Host 'Feed written:' -ForegroundColor Green
|
||||
$total = 0L
|
||||
foreach ($file in (Get-ChildItem -LiteralPath $BinRoot -File | Sort-Object Name)) {
|
||||
$total += $file.Length
|
||||
' {0,-26} {1,10:N1} MB' -f $file.Name, ($file.Length / 1MB) | Write-Host
|
||||
}
|
||||
' {0,-26} {1,10:N1} MB' -f 'TOTAL', ($total / 1MB) | Write-Host
|
||||
# A RID-specific restore rewrites packages.<rid>.lock.json for the projects it
|
||||
# touches, so a feed build can leave the working tree dirty even though nothing
|
||||
# about the source changed. Report it rather than silently reverting: the files
|
||||
# are the developer's, and a real dependency change must not be swallowed here.
|
||||
$dirtyLocks = @(
|
||||
& git -C $RepoRoot status --porcelain -- '*packages.*.lock.json' 2>$null
|
||||
) | Where-Object { $_ }
|
||||
if ($dirtyLocks.Count -gt 0) {
|
||||
Write-Host ''
|
||||
Write-Host 'Note: the RID restore modified these lock files:' -ForegroundColor Yellow
|
||||
$dirtyLocks | ForEach-Object { " $($_.Trim())" | Write-Host }
|
||||
Write-Host ' If you did not change dependencies, discard them:' -ForegroundColor DarkGray
|
||||
Write-Host " git checkout -- '*packages.*.lock.json'" -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host 'Next — publish the feed to Gitea:' -ForegroundColor Cyan
|
||||
Write-Host " pwsh -NoProfile -File tools/publish-dist.ps1"
|
||||
Write-Host ''
|
||||
Write-Host ' (bin/ is gitignored on purpose: publish-dist puts it on the' -ForegroundColor DarkGray
|
||||
Write-Host ' Gitea-only dist branch, never on main / GitHub.)' -ForegroundColor DarkGray
|
||||
161
tools/publish-dist.ps1
Normal file
161
tools/publish-dist.ps1
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Publishes the /bin alpha feed to the Gitea-only `dist` branch.
|
||||
|
||||
.DESCRIPTION
|
||||
Copies the payloads written by tools/publish-bin.ps1 onto an orphan `dist`
|
||||
branch and pushes it to Gitea (the `origin` remote). The launcher's update
|
||||
check reads that branch's raw URLs.
|
||||
|
||||
Why a separate branch, not main:
|
||||
* The launcher payload is ~103 MB. GitHub hard-rejects any file over
|
||||
100 MB, so payloads on main would break every GitHub push.
|
||||
* Each build is ~150 MB. On main that weight would land in the history
|
||||
every developer clones forever.
|
||||
`dist` is a single-commit ORPHAN branch — each publish REPLACES it, so the
|
||||
feed never accumulates old builds. Nothing on it is a parent of main.
|
||||
|
||||
.PARAMETER Remote
|
||||
Remote to publish to. Defaults to `origin` (Gitea). Never pass the GitHub
|
||||
remote: the payload exceeds its per-file limit.
|
||||
|
||||
.EXAMPLE
|
||||
pwsh -NoProfile -File tools/publish-bin.ps1
|
||||
pwsh -NoProfile -File tools/publish-dist.ps1
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Remote = 'origin',
|
||||
[switch]$KeepHistory
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if ($PSVersionTable.PSVersion.Major -lt 7) {
|
||||
throw 'publish-dist requires PowerShell 7 or newer.'
|
||||
}
|
||||
|
||||
$RepoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
||||
$BinRoot = Join-Path $RepoRoot 'bin'
|
||||
$ManifestPath = Join-Path $BinRoot 'manifest.json'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ManifestPath)) {
|
||||
throw "No feed found at '$ManifestPath'. Run tools/publish-bin.ps1 first."
|
||||
}
|
||||
|
||||
$manifest = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json
|
||||
$version = $manifest.version
|
||||
if ([string]::IsNullOrWhiteSpace($version)) {
|
||||
throw 'The manifest has no version.'
|
||||
}
|
||||
|
||||
$payloads = @(Get-ChildItem -LiteralPath $BinRoot -File -Filter *.zip)
|
||||
if ($payloads.Count -eq 0) {
|
||||
throw "No .zip payloads in '$BinRoot'. Run tools/publish-bin.ps1 first."
|
||||
}
|
||||
|
||||
$remoteUrl = (& git -C $RepoRoot remote get-url $Remote 2>&1)
|
||||
if ($LASTEXITCODE) { throw "Remote '$Remote' is not configured." }
|
||||
if ($remoteUrl -match 'github\.com') {
|
||||
throw "Refusing to publish payloads to '$Remote' ($remoteUrl): GitHub " +
|
||||
'rejects files over 100 MB and the alpha feed is Gitea-only.'
|
||||
}
|
||||
|
||||
Write-Host "Publishing alpha feed $version to $Remote ($remoteUrl)" -ForegroundColor Cyan
|
||||
foreach ($payload in $payloads) {
|
||||
' {0,-26} {1,8:N1} MB' -f $payload.Name, ($payload.Length / 1MB) | Write-Host
|
||||
}
|
||||
|
||||
# A throwaway worktree keeps the developer's checkout, index, and HEAD
|
||||
# completely untouched while the dist branch is built and pushed.
|
||||
$stamp = [DateTime]::UtcNow.ToString('yyyyMMddHHmmss')
|
||||
$workTree = Join-Path ([IO.Path]::GetTempPath()) "acdream-dist-$stamp"
|
||||
$branch = 'dist'
|
||||
# Build under a unique local branch and push it AS dist. Reusing the name
|
||||
# locally breaks the second publish outright: `checkout --orphan dist` fails
|
||||
# once a local dist ref exists (observed 2026-08-18).
|
||||
$stagingBranch = "dist-publish-$stamp"
|
||||
|
||||
try {
|
||||
if ($KeepHistory) {
|
||||
& git -C $RepoRoot fetch $Remote $branch 2>&1 | Out-Null
|
||||
$hasRemoteBranch = -not $LASTEXITCODE
|
||||
& git -C $RepoRoot worktree add --no-checkout -b $stagingBranch $workTree `
|
||||
$(if ($hasRemoteBranch) { "$Remote/$branch" } else { 'HEAD' }) 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE) { throw 'Could not create the dist worktree.' }
|
||||
& git -C $workTree checkout . 2>&1 | Out-Null
|
||||
}
|
||||
else {
|
||||
# Default: one commit, no ancestry. Each publish REPLACES the branch so
|
||||
# superseded payloads never pile up in the object store.
|
||||
& git -C $RepoRoot worktree add --detach $workTree 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE) { throw 'Could not create the dist worktree.' }
|
||||
& git -C $workTree checkout --orphan $stagingBranch 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE) { throw 'Could not start the dist branch.' }
|
||||
& git -C $workTree rm -rf --cached . 2>&1 | Out-Null
|
||||
Get-ChildItem -LiteralPath $workTree -Force |
|
||||
Where-Object { $_.Name -ne '.git' } |
|
||||
Remove-Item -Recurse -Force
|
||||
}
|
||||
|
||||
$targetBin = Join-Path $workTree 'bin'
|
||||
if (Test-Path -LiteralPath $targetBin) {
|
||||
Remove-Item -LiteralPath $targetBin -Recurse -Force
|
||||
}
|
||||
$null = New-Item -ItemType Directory -Path $targetBin -Force
|
||||
Copy-Item -LiteralPath $ManifestPath -Destination $targetBin
|
||||
foreach ($payload in $payloads) {
|
||||
Copy-Item -LiteralPath $payload.FullName -Destination $targetBin
|
||||
}
|
||||
|
||||
$readme = @"
|
||||
# acdream alpha feed
|
||||
|
||||
Published by ``tools/publish-dist.ps1``. This branch carries ONLY the launcher
|
||||
update feed — it has no source history and is never merged into ``main``.
|
||||
|
||||
Current release: **$version**
|
||||
|
||||
## For players
|
||||
|
||||
1. Download ``bin/launcher-win-x64.zip``.
|
||||
2. Unzip it anywhere and run ``acdream-launcher.exe``.
|
||||
3. The launcher installs the game client and keeps both up to date.
|
||||
|
||||
You need Asheron's Call's DAT files for first-run setup.
|
||||
"@
|
||||
[IO.File]::WriteAllText(
|
||||
(Join-Path $workTree 'README.md'),
|
||||
$readme,
|
||||
[Text.UTF8Encoding]::new($false))
|
||||
|
||||
# bin/ is gitignored repo-wide (so main can never take the payloads by
|
||||
# accident) — force-add it here, where it is the whole point of the branch.
|
||||
& git -C $workTree add -f bin README.md
|
||||
if ($LASTEXITCODE) { throw 'Could not stage the feed.' }
|
||||
|
||||
& git -C $workTree commit -q -m "release: acdream alpha $version" 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE) {
|
||||
Write-Host 'Nothing changed since the last publish.' -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
& git -C $workTree push --force $Remote "${stagingBranch}:${branch}"
|
||||
if ($LASTEXITCODE) { throw "Push to $Remote/$branch failed." }
|
||||
}
|
||||
finally {
|
||||
if (Test-Path -LiteralPath $workTree) {
|
||||
& git -C $RepoRoot worktree remove --force $workTree 2>&1 | Out-Null
|
||||
if (Test-Path -LiteralPath $workTree) {
|
||||
Remove-Item -LiteralPath $workTree -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
& git -C $RepoRoot worktree prune 2>&1 | Out-Null
|
||||
# The staging branch exists only to carry one publish to the remote.
|
||||
& git -C $RepoRoot branch -D $stagingBranch 2>&1 | Out-Null
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host "Published $version." -ForegroundColor Green
|
||||
Write-Host 'Feed: https://git.snakedesert.se/erik/acdream/raw/branch/dist/bin/manifest.json'
|
||||
Write-Host 'Players: https://git.snakedesert.se/erik/acdream/src/branch/dist'
|
||||
Loading…
Add table
Add a link
Reference in a new issue