CI: intermittent coverage-gate failures on the GitHub mirror (6 of last 20 docker runs) — flaky repoimport tests + a global-state leak reproduced locally #397
Labels
No labels
actions
bug
cli
duplicate
enhancement
fork
forum
git storage
help wanted
insights
invalid
issues
moderation
oidc
ownership transfer
packages
pr/merge protection rules
projects
pull requests
question
releases
sponsorships
tags
webhooks
wiki
wontfix
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
crueber/walhub#397
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What's wrong
The GitHub
dockerworkflow fails intermittently on the coverage-gate step — 6 failures in the last 20 runs (runs #185, #187, #194, #196, #200, and one more), each blocking the GHCR publish. The failing package varies, but the latest instance (run 34706117288, docker #200) failed ininternal/repoimportright after:The same commit passes on re-run/retry (successes surround the failures in the run list — #199 passed 6 minutes after #200 failed), which is the definition of a flaky test, not a coverage regression.
What I verified while reproducing
go test -count=1 ./internal/repoimport/passes 3/3 locally (~23 s each) — the flake needs CI conditions (load, parallel package runs, runner variance).-count=3:TestRegisterKindPanics(internal/repoimport/import_test.go:533-541) panics outside its recover on the second iteration, becauseRegisterKind(internal/repoimport/repoimport.go:66-77) keeps a process-globalkindsmap with no test reset — the first call in iteration 2 hits the duplicate-registration panic from iteration 1's registration. With-count=1this can't fire, but it proves the package's tests carry hidden cross-run state, andmirror.RegisterKind(internal/mirror/sync.go:47-48) has the same shape. Any CI change (test shuffling,-countbump, added test that registers first) turns this latent leak into a live failure.make cover→ per-packagego test -count=1 -coverprofile), and the FAIL appears after a coverage line prints — i.e. an assertion/panic inside a test during the run, consistent with a timing/parallelism flake in the import resume/claim tests (they exercise the(repo, kind)claim single-flight, leases, and CAS retry loops under concurrency).What's needed
RegisterKindglobal-state panic family, the fix is below; if it's a timing assertion, the fix is de-flaking that test.RegisterKind/RegisterKind-shaped registries (repoimport.go:66-77,mirror/sync.go:47+) need a test-only reset (ResetKindsForTest()) or the duplicate-panic should become an idempotent no-op when re-registering the same name from tests; andTestRegisterKindPanicsmust wrap both calls in its recover, or the test must tolerate a pre-registered name.#178/#180flaky-test history suggests this class is recurring — the fan-out and merge-gate flakes were the same genre).resuming import of acme/drnrk from this source) is the claim-resume path — reviewresume_test.go's assertions for timing/order sensitivity (channel-driven task completion vs fixed expectations), and pin whatever is racy with the same treatment #178/#180 received.retryon the coverage step, orgo test -shuffle=offpinning — but the preference is fixing the flakes; note the ruling in the PR.Acceptance criteria
RegisterKindglobal-state leak is fixed (reset or idempotent registration) and demonstrated:go test -count=3 ./internal/repoimport/ ./internal/mirror/passes locally (currently fails at count ≥ 2).make cover(or the failing package's tests at CI parity) before merge.CI logs pulled via the GitHub API (auth worked — gh run log streaming was empty, job-log download via api.github.com returned the goods). Exact failing tests + outputs:
Fix: PR #404 (branch fix/issue-397, do NOT merge yet). All five are test-contract bugs, no production change: TempDir tests now join background clone/sync work (awaitDone/waitAsync); keepalive tests poll up to the expected goroutine count (#178 treatment); token tests use deterministic tamperWireMAC (decode/flip-bit/re-encode); RegisterKind maps gain test-only ResetKindsForTest (-count=3 green with -race, previously panicked). Resume/claim tests already polled — no change. Global-state sweep: only the two kinds maps. Retry-guardrail ruling: fix flakes, no retry stanza, no -shuffle pin (docs/go/15_testing.md). 10 consecutive -race green runs of repoimport/mirror/notify/server; coverage 95.6/96.8/95.3/98.4.
REVIEW: PR #404 (fix/issue-397,
bec2c52) — independent verification in a scratch worktree (removed afterward; main worktree untouched, still clean apart from pre-existing untracked .opencode/).(1) CI evidence: PASS. Issue comment 3997 captures all five flakes with run IDs + exact failure output (TestBeginOnMirrorIs409 TempDir cleanup; TestSSEWriterKeepaliveExitsOnClose goroutine count; TestCreateFromURLOwnerAdmission TempDir ×2 runs; tampered_mac ~1/16 with proven sextet mechanism; TestWgtTokenLifecycle ~1/256). Each is traced to a fix below.
(2) TempDir fixes: CORRECT pattern. mirror_gate_test.go:63 awaitDone + ownerbind_test.go:48,74 waitAsync join background work before TempDir cleanup — same precedent as TestCreateFromURL (http_test.go:167,263,322, all 30s waitAsync). No slowdown blowup: TestBeginOnMirrorIs409 -count=5 in 1.3s, ownerbind subset -count=3 in 1.3s (empty-dir clone fails fast; valid-upstream syncs complete quickly). mirror_gate_test.go:64-66 asserts Err != nil — deterministic: git clone of an empty non-repo dir fails on any git version.
(3) Keepalive polling: FAITHFUL to #178. stream_leak_test.go:41-54 awaitGoroutinesUp mirrors the existing awaitGoroutines shape (5s deadline, 10ms sleep); both keepalive tests converted, close/cancel leak pins retained. Bounds sane.
(4) tamperWireMAC: SOUND. x_gaps9_test.go:33-56 decodes MAC, flips mac[0]^0xFF, re-encodes — guarantees mismatch (valid token's MAC equals want, so flipped byte always differs), stays well-formed so VerifyToken (auth.go:424-425) returns 'invalid token', never 'malformed'. Encoding matches production (base64.RawURLEncoding both sides, auth.go:374-376). Cannot flake itself.
(5) ResetKindsForTest: TEST-ONLY, verified by grep — only 2 call sites (repoimport/import_test.go:538, mirror/mirror_test.go:318). Production never calls it; RegisterKind panic-on-duplicate contract untouched in both files. mirror's other registrations use sync.Once registerOnce (sync_test.go:24, heal_test.go:68) — safe under -count=N. repoimport has no other RegisterKind callers.
(6) resume/claim untouched: JUSTIFIED. resume_test.go already awaitDone-polls every completion path; the #200 'resuming import' line is the task.go:159 Notice log (interleaved output), not an assertion — matches the commit message's analysis.
(7) Production change: DISCLOSED, not zero-file but zero-behavior. internal/mirror/sync.go + internal/repoimport/repoimport.go each gain ONLY the additive ResetKindsForTest (11 lines each); diff minus comments/boilerplate is empty. No behavior change; the 'test-only reset beside RegisterKind' is exactly what #397 prescribed. All other files are *_test.go + docs/go/15_testing.md.
(8) Green runs, all with -race in scratch worktree: repoimport -count=3 PASS (83s); mirror -count=3 PASS (12s); notify -count=3 PASS (5.4s); server token tests (TestVerifyTokenWireNegatives|TestWgtTokenLifecycle) -count=10 PASS; TempDir spot-runs green (above).
(9) Global sweep: only the two kinds maps. RegisterKind exists only in repoimport + mirror; no other test-written process-global state found in touched packages. #178/#180 class (poll-don't-sleep) followed, not reintroduced.
(10) Gates/docs: coverage notify 95.3 / mirror 96.8 / repoimport 95.8 / server 98.3 — all >= 95. gofmt clean, go vet clean on all four packages. docs/go/15_testing.md ruling ('fix flakes, no retry stanza, no -shuffle pin') matches issue preference. One env note: full server suite in a fresh worktree fails ONLY on TestUIAssetConcepts (missing web concepts asset) — reproduced on main too, pre-existing and unrelated (scratch needed web/dist copied from main for the rest).
No fixes pushed — nothing to fix.
MERGE RECOMMENDATION: ready to merge — code-approved; land only after the docker workflow goes green on the fix commit, per the PR's own hold (acceptance criterion GHCR unblocked, which only CI can confirm).
Fixed by PR #404 (review clean — all five flakes traced + fixed, test-only, gates hold; GHCR-unblock confirmed by CI on this commit), merged. Closing.