ci: put the launcher's update pointer in a release, and delete the dist branch
The dist branch existed to carry ~120 MB payloads that could not go on main. Once payloads became release attachments it held one 500-byte manifest.json, so it was a whole branch for a reason that no longer applied. The pointer is now a release asset too: each publish recreates a one-asset release naming the versioned build. Forgejo has no /releases/latest/download/ route (404), so a pointer is still required — but keeping it in a release means nothing about distribution lives in git: no payload branch, no bot commits on main, and no push that could retrigger the pipeline (which is why writing the manifest to main was not the answer either). Recreating the tag deletes the old release AND its tag; the tag outlives its release and would otherwise block recreation. Versioned releases are retained, so older builds stay downloadable. tools/publish-dist.ps1 is removed — publishing is CI's job now.
This commit is contained in:
parent
353231fe6e
commit
ff01423f3f
6 changed files with 87 additions and 218 deletions
|
|
@ -136,26 +136,42 @@ jobs:
|
|||
Invoke-RestMethod -Method Post -Headers $headers -Uri "$api/releases/$($release.id)/assets?name=$($f.Name)" -Form @{ attachment = Get-Item $f.FullName } | Out-Null
|
||||
}
|
||||
|
||||
- name: Point the launcher manifest at the new release
|
||||
- name: Republish the `latest` pointer release
|
||||
shell: pwsh
|
||||
env:
|
||||
TAG: ${{ steps.ver.outputs.version }}
|
||||
TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
# dist carries ONLY manifest.json (~500 bytes), force-replaced each
|
||||
# publish, never pushed to GitHub. Payloads stay in release
|
||||
# attachments, outside git history entirely.
|
||||
$url = "${{ github.server_url }}/${{ github.repository }}.git" -replace '^https://', "https://x:$env:TOKEN@"
|
||||
git config --global user.email 'ci@acdream.local'
|
||||
git config --global user.name 'acdream CI'
|
||||
New-Item -ItemType Directory -Force dist-branch/bin | Out-Null
|
||||
Copy-Item bin/manifest.json dist-branch/bin/manifest.json
|
||||
Push-Location dist-branch
|
||||
git init -q
|
||||
git checkout -q -b dist
|
||||
git add -f bin/manifest.json
|
||||
git commit -q -m "release: manifest for $env:TAG"
|
||||
git push -q --force $url dist:dist
|
||||
Pop-Location
|
||||
Write-Host "manifest published for $env:TAG"
|
||||
$api = "${{ github.server_url }}/api/v1/repos/${{ github.repository }}"
|
||||
$headers = @{ Authorization = "token $env:TOKEN" }
|
||||
|
||||
# Forgejo has no /releases/latest/download/ route, so the launcher
|
||||
# needs a pointer at a URL that never changes. A one-asset release on
|
||||
# the fixed `latest` tag is that pointer. Keeping it in a release
|
||||
# rather than in git means no payload branch, no bot commits on main,
|
||||
# and no push that would retrigger this workflow.
|
||||
$existing = Invoke-RestMethod -Method Get -Headers $headers `
|
||||
-Uri "$api/releases/tags/latest" -SkipHttpErrorCheck
|
||||
if ($existing.id) {
|
||||
Invoke-RestMethod -Method Delete -Headers $headers -Uri "$api/releases/$($existing.id)" | Out-Null
|
||||
# The tag outlives its release and would block recreation.
|
||||
Invoke-RestMethod -Method Delete -Headers $headers -Uri "$api/tags/latest" -SkipHttpErrorCheck | Out-Null
|
||||
Write-Host "removed the previous latest pointer"
|
||||
}
|
||||
|
||||
$body = @{
|
||||
tag_name = 'latest'
|
||||
name = "Update feed -> $env:TAG"
|
||||
body = "Pointer the launcher polls. The downloads live in the ``$env:TAG`` release."
|
||||
draft = $false
|
||||
prerelease = $false
|
||||
target_commitish = 'main'
|
||||
} | ConvertTo-Json
|
||||
$pointer = Invoke-RestMethod -Method Post -Uri "$api/releases" -Headers $headers `
|
||||
-ContentType 'application/json' -Body $body
|
||||
|
||||
Invoke-RestMethod -Method Post -Headers $headers `
|
||||
-Uri "$api/releases/$($pointer.id)/assets?name=manifest.json" `
|
||||
-Form @{ attachment = Get-Item bin/manifest.json } | Out-Null
|
||||
Write-Host "latest pointer now advertises $env:TAG"
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ git push origin main
|
|||
├─ linux-portable (eriktestLinux) portable closure, Linux lanes
|
||||
│
|
||||
└─ release (needs BOTH green) publish a Gitea Release
|
||||
+ repoint the launcher manifest
|
||||
+ republish the `latest` pointer
|
||||
```
|
||||
|
||||
Workflow: [`.gitea/workflows/ci.yml`](../.gitea/workflows/ci.yml). A red gate
|
||||
|
|
@ -58,28 +58,40 @@ Windows uses Gitea's `act_runner`. Forgejo speaks the same Actions protocol.
|
|||
|
||||
## Releases
|
||||
|
||||
Payloads are **release attachments**, deliberately outside git history: a build
|
||||
is ~120 MB and would otherwise land in a branch every push. Only the ~500-byte
|
||||
`manifest.json` is committed, to the payload-free `dist` branch.
|
||||
Everything about distribution lives under **Releases** — nothing in git. A build
|
||||
is ~120 MB, so payloads are release attachments; and the pointer the launcher
|
||||
polls is itself a release asset, so there is no payload branch, no bot commit on
|
||||
`main`, and no push that could retrigger the pipeline.
|
||||
|
||||
```
|
||||
Release 0.1.0-build.<yyyyMMddHHmm>
|
||||
Release 0.1.0-build.<yyyyMMddHHmm> <- the actual build
|
||||
client-win-x64.zip AcDream.App.exe + acdream-headless.exe
|
||||
launcher-win-x64.zip acdream-launcher.exe + acdream-bake.exe
|
||||
manifest.json
|
||||
|
||||
dist branch (manifest only, force-replaced each publish)
|
||||
bin/manifest.json -> points at the release attachment URLs
|
||||
Release latest <- pointer, replaced every publish
|
||||
manifest.json names the version above and its asset URLs
|
||||
```
|
||||
|
||||
The launcher polls the manifest at a fixed raw URL
|
||||
(`ReleaseManifestClient.ProductionManifestUri`). **Forgejo has no
|
||||
`/releases/latest/download/` route** (verified: 404), which is why the manifest
|
||||
cannot simply live in the release itself.
|
||||
The launcher polls the pointer at a URL that never changes
|
||||
(`ReleaseManifestClient.ProductionManifestUri`):
|
||||
|
||||
`tools/publish-bin.ps1 -BaseUrl <release asset base>` builds the payloads; the
|
||||
pipeline passes the tag's asset base. With no `-BaseUrl` it falls back to the
|
||||
dist-branch layout for a manual local publish.
|
||||
```
|
||||
https://git.snakedesert.se/erik/acdream/releases/download/latest/manifest.json
|
||||
```
|
||||
|
||||
A pointer is needed because **Forgejo has no `/releases/latest/download/`
|
||||
route** (verified: 404) — unlike GitHub, there is no built-in stable URL for
|
||||
"the newest release". Publishing it recreates the `latest` tag each time, which
|
||||
means deleting the old release *and* its tag; the tag outlives its release and
|
||||
would otherwise block recreation.
|
||||
|
||||
Versioned releases are kept, so an older build stays downloadable and the
|
||||
launcher's local rollback still has something to fall back to.
|
||||
|
||||
`tools/publish-bin.ps1 -BaseUrl <release asset base>` builds the payloads; CI
|
||||
passes the tag's asset base. Running it locally is for inspection only —
|
||||
publishing is CI's job.
|
||||
|
||||
### Verifying a release
|
||||
|
||||
|
|
|
|||
|
|
@ -24,16 +24,23 @@ public sealed class ReleaseManifestClient : IReleaseManifestClient, IDisposable
|
|||
public const int MaximumRedirects = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Alpha distribution feed: the public Gitea repository's <c>dist</c>
|
||||
/// branch, served as raw files (anonymous raw reads verified 2026-08-18,
|
||||
/// so a friend needs no account). GitHub stays private, and its 100 MB
|
||||
/// per-file limit could not carry the launcher payload anyway, so the
|
||||
/// payloads live on <c>dist</c> — pushed to Gitea only — instead of on
|
||||
/// <c>main</c>. <c>tools/publish-bin.ps1</c> builds them and
|
||||
/// <c>tools/publish-dist.ps1</c> publishes that branch.
|
||||
/// Alpha distribution feed: the manifest asset of the fixed <c>latest</c>
|
||||
/// release on the public Gitea repo (anonymous reads verified, so a friend
|
||||
/// needs no account). Every build publishes a versioned release carrying
|
||||
/// the payloads, then republishes this one-asset <c>latest</c> release as
|
||||
/// the pointer to it.
|
||||
///
|
||||
/// <para>
|
||||
/// A pointer is required because Forgejo has no
|
||||
/// <c>/releases/latest/download/</c> route (verified: 404), so there is no
|
||||
/// built-in stable URL for "the newest release". Keeping the pointer as a
|
||||
/// release asset means nothing about distribution lives in git: no payload
|
||||
/// branch, no bot commits on <c>main</c>, and no build loop to guard
|
||||
/// against.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static Uri ProductionManifestUri { get; } = new(
|
||||
"https://git.snakedesert.se/erik/acdream/raw/branch/dist/bin/manifest.json");
|
||||
"https://git.snakedesert.se/erik/acdream/releases/download/latest/manifest.json");
|
||||
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -91,14 +91,12 @@ public sealed class ReleaseManifestClientTests
|
|||
|
||||
Assert.Equal("2.1.0", manifest.Version.Value);
|
||||
Assert.Equal(client.LongLength, manifest.RequireClient("win-x64").Size);
|
||||
// Alpha distribution feed: the PUBLIC Gitea repo's `dist` branch
|
||||
// (GitHub stays private, so its Releases feed cannot serve anonymous
|
||||
// friend installs, and its 100 MB per-file limit could not carry the
|
||||
// launcher payload). tools/publish-bin.ps1 writes the payloads and
|
||||
// tools/publish-dist.ps1 pushes that branch — all three must agree on
|
||||
// this URL.
|
||||
// Alpha distribution feed: the manifest asset of the fixed `latest`
|
||||
// release. Nothing about distribution lives in git — the CI release job
|
||||
// republishes this pointer after each versioned release, so it must
|
||||
// agree with this URL.
|
||||
Assert.Equal(
|
||||
"https://git.snakedesert.se/erik/acdream/raw/branch/dist/bin/manifest.json",
|
||||
"https://git.snakedesert.se/erik/acdream/releases/download/latest/manifest.json",
|
||||
ReleaseManifestClient.ProductionManifestUri.AbsoluteUri);
|
||||
Assert.Equal(Uri.UriSchemeHttps, ReleaseManifestClient.ProductionManifestUri.Scheme);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,13 +62,12 @@ foreach ($candidate in @($Version, $MinimumLauncherVersion)) {
|
|||
}
|
||||
}
|
||||
|
||||
# Where the manifest says the payloads live. The CI pipeline passes the Gitea
|
||||
# RELEASE asset base for the tag it is publishing, so payloads live outside git
|
||||
# entirely; the default keeps the older dist-branch layout working for a manual
|
||||
# local publish. The manifest itself always stays at the stable dist raw URL
|
||||
# that ReleaseManifestClient.ProductionManifestUri points at.
|
||||
# Where the manifest says the payloads live. Publishing is CI's job: the release
|
||||
# workflow passes the Gitea RELEASE asset base for the tag it is creating. This
|
||||
# default only makes a local build self-describing — it names a release tag that
|
||||
# will not exist until CI publishes one.
|
||||
$RawBase = if ([string]::IsNullOrWhiteSpace($BaseUrl)) {
|
||||
'https://git.snakedesert.se/erik/acdream/raw/branch/dist/bin'
|
||||
"https://git.snakedesert.se/erik/acdream/releases/download/$Version"
|
||||
} else {
|
||||
$BaseUrl.TrimEnd('/')
|
||||
}
|
||||
|
|
@ -247,8 +246,6 @@ if ($dirtyLocks.Count -gt 0) {
|
|||
}
|
||||
|
||||
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
|
||||
Write-Host 'Releases are published by CI, not from here:' -ForegroundColor Cyan
|
||||
Write-Host ' push to main -> .gitea/workflows/ci.yml -> Gitea Release' -ForegroundColor DarkGray
|
||||
Write-Host ' This build is for local inspection; bin/ stays gitignored.' -ForegroundColor DarkGray
|
||||
|
|
|
|||
|
|
@ -1,161 +0,0 @@
|
|||
<#
|
||||
.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