Files
jobtrackingapp/docs/ci-runner-investigation.md
T
cesnimda b7dc07b045
CI and Deploy / test (push) Failing after 1m8s
CI and Deploy / deploy (push) Has been skipped
docs(ci): report runner-only backend test failure investigation
Bisected the failure across four CI runs (job logs are not readable via the
Gitea API) down to the AiWorkspace test classes — 10 tests that pass on
Windows, in a clean Linux container, under a 1GB memory cap, in CI's exact
step order, with a custom-dir SDK and no DOTNET_ROOT, serially, and under a
hostile locale/timezone.

Ruled out: Linux behaviour, case sensitivity, path separators, locale/culture,
time zone, environment variables, parallel execution, test ordering, shared
state, memory. Not testable remotely: host permissions/limits.

Assessment is environmental: the workflow already documents three failure
modes on this same runner with an identical signature (processes dying with no
error output — SDK cache corruption, npm ci SIGSEGV, CRA build OOM/SIGSEGV).
Report includes evidence table and recommended infrastructure fix.

Removes the temporary bisection scaffolding; keeps the restore/build/test split
and the host smoke. No test was weakened, skipped, or filtered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 22:12:00 +02:00

6.1 KiB
Raw Blame History

CI Runner Investigation — backend suite fails only on Live-Runner

2026-07-18. Status: root cause narrowed to the runner host; not reproducible in any other environment. Final confirmation needs the job log, which the Gitea API will not serve without a token. Recommended infrastructure fix at the bottom.

Summary

The backend test suite (306 tests) passes everywhere it has been run except the self-hosted Gitea runner, where the test step dies after ~3 s. Restore and build succeed; the test host starts and can run a test. The failure is localised to the AiWorkspace test classes (10 tests).

Important context: this suite had never actually executed in CI. The workflow built only JobTrackerApi and then ran dotnet test --no-build, so the test project was never compiled and the step was a ~1 s no-op (fixed in cfba7fb). The failure is therefore newly surfaced, not a regression — it may have been present for a long time.

How it was narrowed

Job logs return 401 token is required from the Gitea API, so step boundaries were the only readable telemetry. The suite was split and bisected across CI steps over four runs.

Run Observation
524 Test backend fails at 8 s (restore+build+test in one step)
525 Split: restore 4 s ✓, build 4 s ✓, test 3 s ✗ → not a compile/restore error
526 One-test host smoke 1 s ✓, full suite (parallelism off) 3 s ✗ → host starts fine; not parallelism
527 Alphabetical quarters: slice 1 (AC) ✗ at 3 s, later slices never ran
528 Per class: T AiWorkspace ✗ at 3 s; all other AC classes never reached

The FullyQualifiedName~AiWorkspace filter matches 10 tests across AiWorkspaceTests (Phase 5) and AiWorkspaceNotePersistenceTests (pre-existing). Locally that filter completes in 1 s.

What was ruled out (evidence)

Each was reproduced against the same commit, all 306 tests passing unless noted.

Hypothesis Test performed Result
Linux-specific behaviour clean mcr.microsoft.com/dotnet/sdk:9.0 container 306 pass
File system case sensitivity same (Linux is case-sensitive) 306 pass
Path separators same 306 pass
CI step ordering reproduced CI's exact build-then-test order 306 pass
Memory pressure --memory=1g --memory-swap=1g 306 pass
Environment variables / SDK layout bare ubuntu:22.04, SDK via dotnet-install.sh to $HOME/.dotnet, PATH only, DOTNET_ROOT unset (mirrors the runner) 306 pass
Parallel execution xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1 passes locally; still fails on runner
Test ordering / shared state TestHostFactory.CreateInMemoryDb uses Guid.NewGuid() per test — no shared store isolated
Locale / culture LANG=LC_ALL=tr_TR.UTF-8 (Turkish-I trap) 10/10 pass in 1 s
Time zone TZ=Pacific/Kiritimati (UTC+14) 10/10 pass in 1 s
Docker availability these tests use EF InMemory + Moq; no Docker, network, or filesystem use n/a
Permissions not testable remotely unresolved

Assessment: environmental, not code

The AiWorkspace tests use EF InMemory with a per-test GUID database, Moq, and a fake summarizer. They touch no clock-sensitive assertion, no file, no socket, and no external process. They pass under every adverse condition that could be simulated. That points at the runner host itself.

The workflow already documents three separate failure modes on this exact runner, all with the same signature — a process dying with no usable error output:

  • actions/setup-dotnet "intermittently leaves a partial extraction in the shared tool-cache … or corrupts the SDK download" (hence the hand-rolled dotnet-install.sh + retry).
  • npm ci "occasionally segfaults on the runner (SIGSEGV/139, a memory/native flake)".
  • The frontend build "has repeatedly died silently on this runner with no error output (OOM/SIGSEGV signature — same resource-starved-runner class)".

A .NET test host exiting ~3 s into a 10-test run, on a host with a documented history of silently killed processes, is consistent with the same resource starvation — most plausibly the OOM killer or a cgroup limit, triggered while the runner shares the box with the production Docker stack. A 1 GB container cap was not enough to reproduce it, so the runner's available memory at that moment is likely lower, or the limit is on process/thread count rather than memory.

In priority order:

  1. Read the job log for step T AiWorkspace (run 528) — 20 lines settles this immediately. Either grant a read-scoped Gitea token so CI failures can be diagnosed without a human relay, or paste the step output. Everything below is contingent on that.
  2. Check the host for OOM kills around the run: dmesg -T | grep -i -E 'oom|killed process' and journalctl -u <gitea-runner> --since '1 hour ago'. A killed testhost/dotnet confirms it.
  3. Give the runner headroom / isolation. It currently appears to share the host with the prod stack. Either cap the prod containers' memory, give the runner its own cgroup allocation, or move it off the production host. This would also fix the pre-existing npm/CRA/SDK flakes the workflow works around with retries.
  4. Check ulimit/cgroup pids for the runner user (ulimit -a, cat /sys/fs/cgroup/pids.max). The .NET test host spawns more threads than npm ci, so a low pids limit would hit it first.

Repository-side changes made during the investigation

Kept, because they are correct independent of the outcome:

  • cfba7fb — CI actually runs the backend suite (dropped the no-op --no-build).
  • 45725ac — restore / build / test split into separate steps, restore retries once.
  • 2bdc4a9 — one-test host smoke step; collection parallelism disabled for determinism.
  • Bisection scaffolding was removed once it had served its purpose (7fa3080, 0f62dc4).

No test was weakened, skipped, or filtered at any point. CI remains red on purpose — the failure is real and should stay visible until the runner is fixed.