The transport row said to cap xUnit.MaxParallelThreads; serializing that assembly actually REGRESSED Windows (1000 passed in 7 s -> 999/1000 in 17 s) and the real fix was widening the virtual-clock harness's wall-clock patience. The Avalonia row said to serialize via xunit.runner.json; that does not fix it either — proven twice, including with a compiled-in attribute so delivery could not be blamed. It needs a real desktop and is now Lane=Manual. Both rows now record what was tried and disproved, which is the part worth keeping.
6.6 KiB
Continuous integration and alpha releases (Gitea)
Single source of truth for how acdream builds, gates, and ships alpha builds.
Landed 2026-08-19. Companion to release-gate.md, which
owns the local bounded gate.
What happens on a push to main
git push origin main
│
├─ windows-gate (RARE-win) build + full lane-filtered suite
├─ linux-portable (eriktestLinux) portable closure, Linux lanes
│
└─ release (needs BOTH green) publish a Gitea Release
+ repoint the launcher manifest
Workflow: .gitea/workflows/ci.yml. A red gate
cannot publish: release uses needs:, not a workflow_run trigger, whose
Forgejo support is unreliable.
Why Gitea and not GitHub
GitHub Actions is billing-blocked on this account ("recent account payments
have failed"), and the repo is private, so hosted runners consume paid minutes.
Forgejo ships no hosted runners at all, so Actions there requires
self-hosted ones — which are free on both platforms. The same two machines can
serve GitHub later by registering a second agent; only the workflow's
runs-on labels change.
The runners
| Windows | Linux | |
|---|---|---|
| Host | RARE (10.6.0.3) |
eriktestLinux (10.0.0.202) |
| Agent | act_runner 0.2.13 |
forgejo-runner 13.0.0 |
| Persistence | Scheduled task ForgejoRunner, at logon of acbot |
systemd forgejo-runner, Restart=always |
| Labels | windows, windows-latest, windows-x64 |
ubuntu-latest, ubuntu, linux, ubuntu-slim |
| Execution | host mode (:host) — no Docker on either box |
host mode |
Both poll outbound over HTTPS. Gitea never connects to them, so no inbound
ports, no port forwarding, and no static IP; they work behind NAT. The runner
does not have to live next to the Gitea container (which runs on bluesnake,
a host we have no shell on).
forgejo-runner publishes no Windows binary in any release, which is why
Windows uses Gitea's act_runner. Forgejo speaks the same Actions protocol.
Prerequisites on a runner
- .NET SDK in the
global.jsonband — currently10.0.3xx.10.0.400is a different feature band androllForward: latestPatchrejects it. - Node.js —
actions/checkoutandactions/upload-artifactare JavaScript actions. Docker images normally supply Node; in host mode the machine must. - Git, and outbound HTTPS to
git.snakedesert.se. - PowerShell 7 on Windows (
pwsh);tools/*.ps1require it.
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.
Release 0.1.0-build.<yyyyMMddHHmm>
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
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.
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.
Verifying a release
dotnet test tests/AcDream.Launcher.Core.Tests --filter Lane=Live
LiveGiteaReleaseInstallTests installs the advertised client from the real feed
through the production updater — real SHA-256/size verification, extraction, and
atomic activation — then asserts both hosts resolve out of the activated
directory and current.json names the installed version.
Landmines
Each of these cost a red pipeline; none was a config typo. Two rows record a fix that was tried and disproved — read those before repeating it.
| Symptom | Cause |
|---|---|
Cannot find: node in PATH |
JS actions need Node on the host in :host mode |
actions/setup-dotnet never resolves |
data.forgejo.org does not mirror it (404). checkout and upload-artifact are mirrored. Self-hosted runners carry the SDK anyway |
| Job "failed" while dotnet processes still run | run-release-gate.ps1 redirects children to log files, so the step goes silent; Forgejo fails a non-reporting task as a zombie. CI runs dotnet test directly so output streams |
| ~40 tests fail on formatted numbers | Runner's HKCU locale was en-SE (comma decimal): expected "update:0.25", got "update:0,25". Set-Culture does not reach a scheduled task without a loaded profile — set the registry directly |
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 as the locale fix |
Too blunt — it breaks tests that legitimately construct a culture. Fix the machine locale instead |
FileNotFoundException: client_cell_1.dat |
DAT-dependent tests missing [Trait("Lane", "InstalledDat")]. Build machines have no DATs |
| Timing-sensitive test fails only under load | FakeAceTransportTests.PausedSelector_… took 37-42 s under load vs ~350 ms isolated. Its harness drives a VIRTUAL clock but asserted on 2 s wall-clock windows; those are patience, not assertions, and now share a 60 s HarnessPatience. Do not serialize the assembly to fix it — that regressed Windows from 1000 passed in 7 s to 999/1000 in 17 s, breaking a loss-soak test that had never failed |
| Avalonia "calling thread cannot access this object" in cleanup | MainWindowViewTests needs a real desktop session and is Lane=Manual. Measured: PASSES on a dev desktop and on the CI Windows box over SSH; FAILS under act_runner and on Linux. Serializing the assembly does not fix it (tried via xunit.runner.json and a compiled-in CollectionBehavior attribute), and de-async-ing the test actively causes the failure. The stack shows a compositor being constructed during teardown — it is the headless session lifecycle, not parallelism |
Culture note
The en-SE discovery is worth remembering beyond CI: config files, numeric
parsing, and the wire are all culture-safe (System.Text.Json is invariant by
spec, every float/double.TryParse passes CultureInfo.InvariantCulture, and
the protocol is binary). Only diagnostic strings format with the current
culture, so a European player sees local=(8,00; 191,00) in an F3 dump. The
client installs and runs correctly in both the US and Europe.