Every push to main now runs the gate on the self-hosted runners and, when
green, publishes a Gitea Release carrying the client, launcher+bake, and
manifest.
Pipeline (.gitea/workflows/ci.yml):
- windows-gate runs tools/run-release-gate.ps1, the project's own bounded
gate. A bare `dotnet test AcDream.slnx` is NOT usable as a gate: it fails
~36 tests by design, because the InstalledDat/Live/Manual/OS lanes assert
their own preconditions. The gate script's trait filter is what excludes
them.
- linux-portable runs the portable closure, where the Linux-lane tests
actually execute instead of failing on Windows.
- release depends on both, so a red gate cannot publish. It is a job in the
same workflow rather than a workflow_run trigger, whose Forgejo support is
unreliable; `needs` is guaranteed.
No actions/setup-dotnet: data.forgejo.org does not mirror it at all (404),
and both runners carry the pinned SDK band already. actions/checkout IS
mirrored and is used normally.
Release payloads become release ATTACHMENTS, outside git history, so ~120 MB
per build never enters a branch. Only the ~500-byte manifest.json is
committed, to the payload-free dist branch, because Forgejo has no
/releases/latest/download/ route (verified 404) for the launcher to poll.
publish-bin.ps1 takes -BaseUrl so the manifest points at the release tag.
Two real gate failures fixed:
- LauncherProjectBoundaryTests asserted four `**` path filters belonging to
the push triggers that 8be14d39 removed when workflows went manual-only.
The assertions about what the workflow DOES are untouched.
- MainWindowViewTests failed in Test Case Cleanup with "calling thread cannot
access this object" while passing in isolation: Avalonia's headless session
is thread-affine and xUnit ran collections in parallel. Serialized via
xunit.runner.json, the same settings AcDream.Core.Tests already uses.
Local gate: 12 projects, 14,346 tests, 0 failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
254 lines
10 KiB
PowerShell
254 lines
10 KiB
PowerShell
<#
|
|
.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,
|
|
[string]$BaseUrl,
|
|
[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)."
|
|
}
|
|
}
|
|
|
|
# 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.
|
|
$RawBase = if ([string]::IsNullOrWhiteSpace($BaseUrl)) {
|
|
'https://git.snakedesert.se/erik/acdream/raw/branch/dist/bin'
|
|
} else {
|
|
$BaseUrl.TrimEnd('/')
|
|
}
|
|
|
|
$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 Remove-DebugSymbols {
|
|
param([Parameter(Mandatory)][string]$Directory)
|
|
|
|
# Players never load these, and the native ones are enormous: a stock
|
|
# Avalonia publish ships libSkiaSharp.pdb (80 MB) and
|
|
# libHarfBuzzSharp.pdb (20 MB), which alone were 100 MB of a 278 MB
|
|
# launcher payload (measured 2026-08-18). MSBuild's DebugType switches
|
|
# only govern OUR managed symbols, not the native .pdb files that arrive
|
|
# as package runtime assets, so drop them from the payload directly.
|
|
$symbols = @(Get-ChildItem -LiteralPath $Directory -Recurse -File -Filter *.pdb)
|
|
if ($symbols.Count -eq 0) { return }
|
|
$freed = ($symbols | Measure-Object -Property Length -Sum).Sum
|
|
$symbols | Remove-Item -Force
|
|
' stripped {0} debug symbol file(s), {1:N1} MB' -f $symbols.Count, ($freed / 1MB) |
|
|
Write-Host -ForegroundColor DarkGray
|
|
}
|
|
|
|
function New-PayloadZip {
|
|
param(
|
|
[Parameter(Mandatory)][string]$SourceDirectory,
|
|
[Parameter(Mandatory)][string]$ZipPath,
|
|
[Parameter(Mandatory)][string[]]$RequiredFiles
|
|
)
|
|
|
|
Remove-DebugSymbols $SourceDirectory
|
|
|
|
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
|