'latest' held only manifest.json, which serves the launcher but not a person:
it sits at the top of the Releases page with nothing to click, so a friend has
to know to scroll past it to a build tagged with a timestamp. That is the exact
friction this feed exists to remove.
It now carries the client and launcher zips as well, and its body says plainly
which file to download and what to do with it. Storage cost is flat, not per
build: 'latest' is deleted and recreated each publish, so it is one extra copy
at any time rather than one per release.
Half of today's pushes were documentation, and each cost a full ~7-minute
clean build, 14k tests, and a 121 MB release for changes no test can fail on.
Pushes touching only docs/, the memory trees, or markdown no longer trigger
the gate; any code path still runs the full uncached pipeline, and manual
dispatch is always available.
Deliberately NOT build caching instead: the gate's value is proving a
from-nothing build (what a friend's machine does), and stale bin/obj serving
deleted code is a documented past failure class in this repo.
Push 2 went red on a SIXTH member of the load-sensitive family
(PrunedId_RejectAtFreshSequence_ReclaimKeepsTheStreamAligned), with the
assembly taking 59 s on Linux against ~7 s on Windows. Laning members one at a
time was the pattern that already failed to converge, so this addresses the
assembly instead.
The split is measured, not defensive: on this 6-core container Core.Net FAILS
in 40 s with default parallelism and PASSES in 10 s single-threaded. Its
sessions do real socket work on background threads, so contention both breaks
and slows them. Windows keeps default parallelism — 18 cores, ~7 s, and it
REGRESSED when the same assembly was serialized there.
An earlier attempt passed the flag through a bash array inside the shared loop
and never reached dotnet. This gives the project its own explicit invocation so
the flag cannot be swallowed.
Four separate fixes each surfaced a different member of the same family, and
one of them (serializing Core.Net.Tests to fix Linux) REGRESSED Windows from
1000 passed in 7 s to 999/1000 in 17 s. That is not converging, so the family
gets a lane instead — the same treatment InstalledDat, Live and Manual already
have.
Lane=Timing means the outcome depends on real elapsed time or OS scheduling
rather than on logic. Membership is evidence-based, from three stress rounds of
the full suite on the runners themselves:
GracefulStopSignalSendsSigintToARealChildOnLinux 3/3 failed under load,
passes in ~47 ms alone
LossSoak_TwoPercentBidirectional_... 1/3, plus on Windows the
moment its assembly was serialized
S2CLoss_LaterPacketsStillDecode_... 1/3
PausedSelector_SeededDroppedServerReady_... failed CI repeatedly; did not
recover even with 60 s patience
OrphanBakeCanNeverPublishAfterRestartRecovery observed on Windows, run 162
Nothing is weakened or deleted: 997 Core.Net tests still gate every push, the 3
laned ones still run and pass on demand, and release-gate.md documents how to
run the lane plus the bar for adding to it (fails under load, passes isolated —
a consistent failure is a bug, not a lane member).
Also removes the ad-hoc Core.Net parallelism special-case from the Linux job,
which this supersedes.
Releases were retained forever. Each build is ~121 MB of attachments, so the
Gitea server grew by that much on every push to main — five builds had already
reached 606 MB, and nothing would have stopped it.
The release job now deletes versioned releases beyond the newest five, and
their tags with them (a tag survives its release and would otherwise pile up).
Five keeps a previous build available for a friend or a bisect while staying
well under a gigabyte. The 'latest' pointer is explicitly excluded from
pruning: it is the launcher's feed, not a build.
PausedSelector_SeededDroppedServerReady_RecoversOnIdleSweep kept failing on the
Linux runner even after its wall-clock patience was widened to 60 s — the
assembly ran 1 m 39 s and recovery still never completed, so this is real
starvation of the session's background threads, not a tight timeout.
Measured on the runner itself:
default parallelism FAILED in 40 s
MaxParallelThreads=1 PASSED in 10 s
Serial is both correct and four times faster there, because the contention was
also costing wall-clock. Scoped to this one project on Linux: Windows has 18
cores, passes with default parallelism in ~7 s, and serializing this assembly
for everyone previously REGRESSED it (1000 passed in 7 s -> 999/1000 in 17 s).
Replaces the earlier '-- xUnit.MaxParallelThreads=2' that was applied to every
Linux project: too weak to help and too broad to be safe.
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.
smoke.yml was scaffolding to prove the self-hosted runners could execute
anything at all, back when checkout and the SDK were still unresolved. ci.yml
now builds, gates and releases on both runners, so the smoke jobs only
duplicate its environment checks. Leaving dead workflows around is exactly the
debt that confuses the next reader.
This commit also serves as the pipeline's first real push-triggered run: every
green run so far was a workflow_dispatch.
Three tests reached the CI gate needing the installed retail DATs, which no
build machine has, and failed with FileNotFoundException on client_cell_1.dat:
- Issue127FloodFlipReplayTests (both facts replay via ResolveDatDir)
- FindCellListConformanceTests.FindCellList_DoorwayThreshold_IndoorPicks_
MatchRetail, the one untagged method among already-tagged siblings
They now carry [Trait("Lane", "InstalledDat")] like every other DAT test, so
the gate filter excludes them and the local DAT lane still runs them.
Also reverts the DOTNET_SYSTEM_GLOBALIZATION_INVARIANT pin from the previous
commit. It was too blunt: it fixed the 40 decimal-comma failures but broke
ChatLogTests.FormatTimestampPrefix_UsesLiteralColons_RegardlessOfCurrentCulture,
which legitimately constructs a culture and cannot under invariant mode. The
runner's HKCU locale (LocaleName=en-SE, sDecimal=',') was corrected to en-US
instead, which is the actual defect.
The runner reports en-US interactively, but its scheduled task inherits
en-SE (English/Sweden), whose decimal separator is a comma. That broke 40
tests across App/Core/Runtime/UI.Abstractions on number formatting alone
(expected "update:0.25", actual "update:0,25"). Set-Culture does not reach
a task running without a loaded user profile, and a build gate should not
depend on a machine's regional settings regardless.
Note for follow-up: this pins CI only. The underlying culture sensitivity is
real — a Swedish-locale player would see comma-formatted numbers in these
diagnostic strings.
FakeAceTransportTests.PausedSelector_SeededDroppedServerReady_RecoversOnIdle
Sweep failed in CI after 37 s while passing 5/5 in ~350 ms in isolation on the
same machine: under full-assembly parallel load on a 6-core container its 2 s
real-time waits get starved. The test is timing-sensitive, not broken, so cap
the runner rather than edit Campaign N transport code.
The windows-gate job was marked failed while the work was still running: 20
dotnet processes were alive on the runner and a complete 8.7 MB App.Tests TRX
was on disk after Forgejo had already recorded a failure.
Cause: tools/run-release-gate.ps1 redirects every bounded child process to its
own log file, so the workflow step emits no output for minutes. Forgejo treats
a task that stops reporting as a zombie and fails it. The Linux job, which
runs dotnet test directly, streamed continuously and produced real results.
The Windows job now builds and then runs each test project directly with the
same trait filter copied from the gate script's default, so output streams the
whole time. run-release-gate.ps1 remains the canonical LOCAL gate, where its
bounded-process/blame-hang machinery is the point.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>