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.
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.
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.
docs/ci-and-releases.md documents what happens on a push to main, why Gitea
rather than GitHub (billing-blocked, private repo, and Forgejo ships no hosted
runners), both runners and their prerequisites, the release/manifest layout,
and how to verify a release with the Lane=Live install test.
Its landmine table is the part worth keeping: every row cost a red pipeline —
Node for JS actions, setup-dotnet unmirrored on data.forgejo.org, the
zombie-task timeout caused by run-release-gate.ps1 redirecting child output,
an en-SE runner locale breaking 40 tests on decimal commas, DAT tests missing
their InstalledDat lane tag, parallel-load timing flakes, and the Avalonia
compositor threading failure that must NOT be 'fixed' by de-async-ing the test.
Also records the culture finding: config, parsing and the wire are all
invariant-safe, so US and European installs behave identically; only
diagnostic strings follow the current culture.
Cross-linked from docs/README.md and release-gate.md, which keeps ownership of
the local bounded gate.