Deflake CI: TUF warn mode, readiness diagnostics, test races - #6063
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6063 +/- ##
=======================================
Coverage 72.24% 72.25%
=======================================
Files 722 722
Lines 75125 75138 +13
=======================================
+ Hits 54277 54291 +14
+ Misses 16985 16983 -2
- Partials 3863 3864 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
/retest |
|
Using the proxy-log dump this PR adds to `DebugServerState`, I pulled the actual CI log for this PR's own `E2E Tests Core (core)` failure and found the real error underneath the "stuck in starting" / bare "error" symptom: ``` Root cause: the ingress proxy's host port was derived from the container's fixed internal listen port (`UpstreamPort + 1`), which is identical across every workload of the same image — so concurrently-started workloads of the same image (as these specs do) all raced for the same port, and whichever lost got exactly this Docker error. Opened #6069 with the fix (pick the ingress port at random instead). It's not a fixup of this PR — no file overlap, and this PR's diagnostics are what made finding it possible — so they can merge independently in either order. Flagging here since it's the missing piece for the readiness-stall this PR documents but doesn't claim to fix. |
In the default warn mode, VerifyImage returned verifier construction errors and non-signature verification errors as fatal, while an actual failed verification only warned. A sigstore TUF CDN connection reset therefore hard-failed 'thv run' for any registry server with provenance (observed repeatedly in CI, e.g. run 30297766499). Warn mode now warns and continues on every verification failure; enabled mode still fails closed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y5WXPQr5Da9Cox2nZWhg6n
startWorkload and the detached-spawn failure path wrote status error with an empty context, clobbering the failure reason RunWorkload's retry loop had just recorded; CI readiness timeouts then showed only a bare status "error". Record err.Error() instead. The readiness probe logged per-attempt outcomes at DEBUG only, so a workload stuck in starting left no visible trace of what the probe observed. Log an INFO progress line every 30s with the last observed outcome, and include it in the timeout error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y5WXPQr5Da9Cox2nZWhg6n
DebugServerState only printed container logs, which show a healthy server while the workload sits in starting or error: the failure is in the detached supervisor, whose output goes to the proxy log file. Dump 'thv logs --proxy' too so a CI timeout names the actual blocker. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y5WXPQr5Da9Cox2nZWhg6n
Suite v0.1.16 has four scenarios that drive a server->client request and are exposed to the upstream ordering race (conformance#407): tools-call-sampling, tools-call-elicitation, elicitation-sep1034-defaults and elicitation-sep1330-enums. The gate only ignored the first two, so runs where the race hit an elicitation-sep scenario (both hit on one PR run, each timing out at exactly 60s) still hard-failed. Extend the alternation to the set verified from the suite source; any other scenario still fails the job. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y5WXPQr5Da9Cox2nZWhg6n
The unrelated-composite-tool spec captured its Ready-condition timestamp baseline right after a gate that only proves ObservedGeneration > 0, so a controller still settling flipped the condition inside the 2s observation window and failed the spec (3 main-branch failures since 2026-07-06). Wait for the transition time to hold stable across ~2s of polls before taking the baseline. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y5WXPQr5Da9Cox2nZWhg6n
1f14126
Summary
The merge queue was being blocked by recurring CI flakes. Analysis of three weeks of
Main buildruns onmain(146 runs, 75 failed) ranked the group-workload readiness family as the top three flakes, with a conformance gating gap and an envtest watch-test race behind them. This PR fixes one proven product bug (TUF outages hard-failingthv runin warn mode), closes the conformance gating gap, fixes one racy test baseline, and adds the startup diagnosability that turned the group-readiness failures from mysteries into named causes.Correction — this PR body previously claimed a different mechanism for the group-readiness failures, and that claim is withdrawn. An earlier revision asserted that #6043's workload-name lengthening introduced a deterministic >63-char name-length failure, and that "every PR based on ≥
be0e0c34fails the core shard deterministically". Both statements were wrong. The instrumented run on this very PR refuted them: a 56-character workload failed the same way, and the 62-vs-66-char split observed across seven earlier runs was just which participant lost a 4-way startup race. What the diagnostics in this PR actually captured, on this PR's own first core-shard run:status "error"mode — the (previously clobbered, now visible) status context read:failed to create ingress container: ... Bind for 127.0.0.1:8081 failed: port is already allocated. The ingress proxy port was derived asUpstreamPort + 1, so four concurrently starting workloads all contended for 8081. Root-cause fixed on main by Stop deriving the ingress proxy port from a fixed upstream port #6069 ("Stop deriving the ingress proxy port from a fixed upstream port").status "starting"mode — the new probe-progress lines showedlast_observed: "HTTP 502"at attempts 20/35/50/65 across the full 121s while the workload's own container had been listening within ~1s: the ingress proxy never recovered its route to the backend. Root-cause fixed on main by Fix concurrent isolated-startup races (network create + ingress DNS latch) #6071 ("Fix concurrent isolated-startup races (network create + ingress DNS latch)").Credit for both product fixes goes to #6069/#6071; this PR's contribution to that story is the instrumentation that exposed the mechanisms (and will name the next one). Relatedly: issue #6040 tracked exactly these failures and was closed against #6043, which did not fix them — the failures had a genuine product cause that #6069/#6071 have now addressed.
What remains in this PR after rebasing onto main (
57e681f1):1. Sigstore TUF outages hard-failed
thv runin warn mode (product bug, root-caused here). In run 30297766499, five specs across THVIgnore, List Group, and the group suites failed in one job whiletuf-repo-cdn.sigstore.devwas resetting connections:thv run fetchexited 1 withfailed to create TUF client ... connection reset by peer. The cause is an inconsistency inVerifyImage: in the defaultwarnmode, an actual failed verification only warns, but a verifier construction failure (TUF unreachable) and any non-signature verification error were returned as fatal. Warn mode now warns and continues on every verification failure;enabledstill fails closed. The new unit test forces the same construction-failure path deterministically (aSigstoreURLwith no embedded TUF root) with no network involved — it fails against the old code and passes with the fix.2. Workload-startup diagnosability (proven useful by this PR's own CI run). Three gaps had made the group-readiness failures undiagnosable from CI output:
startWorkloadand the detached-spawn failure path set statuserrorwith an empty context, clobbering the failure reasonRunWorkload's retry loop had just recorded. They now recorderr.Error()— this is what surfaced the 8081 bind race.waitForInitializeSuccess) logged per-attempt outcomes at DEBUG only. It now logs an INFO progress line every 30s with the last observed outcome and includes it in the timeout error — this is what surfaced the persistent ingress 502.DebugServerStatedumped only container logs, which show a healthy server precisely in these failures; the supervisor's own output goes to the proxy log file. It now dumpsthv logs --proxytoo.3. Conformance quarantine list was incomplete. Suite v0.1.16 has exactly four scenarios that register client-side handlers for server→client requests and are therefore exposed to the upstream ordering race (modelcontextprotocol/conformance#407):
tools-call-sampling,tools-call-elicitation,elicitation-sep1034-defaults,elicitation-sep1330-enums— verified from the suite source (src/scenarios/server/tools.ts,elicitation-{defaults,enums}.ts), not by name-matching. The gate only ignored the first two; on #6054 the race hittools-call-samplingandelicitation-sep1034-defaults(both timing out at exactly 60s), so the job hard-failed. The intermittency was directly observed, not just inferred: the same commit failed onelicitation-sep1034-defaultson one run and passed it on the re-run. The alternation now covers all four; anything else still fails the job, and the block still documents its removal condition (#407 fixed upstream +CONFORMANCE_VERSIONbump).4. CompositeToolDefinition watch test races controller settle (3 main failures since 2026-07-06). The "should NOT trigger reconciliation" spec captures a Ready-condition-timestamp baseline right after a
BeforeAllgate that only provesObservedGeneration > 0, so a still-settling controller flips the condition inside the 2s observation window and fails the spec (confirmed in run 30250838615: timestamp changed within the 2.78s spec). The spec now waits for the Ready condition's transition time to hold stable across ~2s of polls before taking the baseline. This is unrelated to the #6069/#6071 fixes and still needed.Dropped during rebase: the
dual_era_k8s_test.gofix that was previously in this PR (bareExpecton reconcile-laggingMCPServer.Status) — fully superseded by #6057, which shipped the same Eventually-wrap plus stricter replica-count gates.References: #6040 (closed), #6043, #6069, #6071, #6057, #5886, #5888, modelcontextprotocol/conformance#407.
Type of change
Test plan
Unit tests
E2E tests (
task test-e2e)Linting (
task lint-fix)Manual testing (describe below)
After the rebase onto
57e681f1:go build ./...,go vet, andgo teston every touched package (pkg/runner/retriever,pkg/runner,pkg/workloads) are green;task lint-fixreports 0 issues.task testwas not used — it panics locally on the known gotestfmt v2.5.0 bug — so the underlyinggo testwas run directly; CI runs the real thing.The new
TestVerifyImagecasewarn with unavailable verifier continueswas proven to be a real regression guard: it fails against the pre-fixretriever.go(verified by stashing the fix) and passes after.The conformance gate change was exercised with a scripted reproduction feeding synthetic suite output through the exact
sed/greppipeline, old regex vs new: the Let Cedar read user claims the upstream asserts only in its ID token #6054 combination (sampling + sep1034) fails old / passes new; a quarantined-only failure passes both; a genuine regression — alone, alongside a quarantined flake, or as a prefix-lookalike scenario id (tools-call-sampling-extended) — fails both.The diagnostics commit was validated by this PR's own pre-rebase CI run (job 90109089450): it captured the 8081 bind-race error context and the ingress 502 probe history described above. A maintainer's merge of main (with Stop deriving the ingress proxy port from a fixed upstream port #6069/Fix concurrent isolated-startup races (network create + ingress DNS latch) #6071) into this branch then turned
E2E Tests Core (core)green.Not run locally (no cluster/docker in this environment): the k8s operator suites and the CLI e2e suites. The
virtualmcpserver_compositetool_watch_test.gochange is compile-verified (go vet) and runs in Operator CI on this PR.API Compatibility
v1beta1API, OR theapi-break-allowedlabel is applied and the migration guidance is described above.Changes
pkg/runner/retriever/retriever.goVerifyImagewarns and continues on verifier construction and verification errors;enabledunchanged (fails closed)pkg/runner/retriever/retriever_test.goTestVerifyImage: warn vs enabled vs disabled vs invalid, incl. a deterministic offline verifier-construction failurepkg/workloads/manager.goerr.Error()as status context instartWorkloadand the detached-spawn failure path instead of clobbering it with""pkg/runner/runner.gotest/e2e/helpers.goDebugServerStatealso dumpsthv logs --proxytest/conformance/run-conformance.shcmd/thv-operator/test-integration/virtualmcp/virtualmcpserver_compositetool_watch_test.goDoes this introduce a user-facing change?
Yes. With the default
--image-verification=warn,thv runno longer fails when the sigstore TUF repository is unreachable (e.g. offline or a CDN outage) — it logs a warning and continues, consistent with how a failed verification already behaved in warn mode.--image-verification=enabledstill fails closed. Additionally, a workload that fails to start now records the failure reason in its status context, and a slow-starting workload logs readiness-probe progress every 30s.Special notes for reviewers
retriever.gois the one behavioral change worth scrutiny: previously warn mode failed closed on infrastructure errors while failing open on verification errors. If anyone relies on warn mode blocking runs when sigstore is unreachable, this changes that — but that behavior is what broke CI during a CDN outage, and it contradicts the mode's documented intent ("prints a warning when image validation fails").57e681f1and force-pushed (dropping three interim "merge main" commits whose content is included in the new base, and the superseded dual_era commit); history before that reflects the withdrawn name-length analysis and should not be relied on.Generated with Claude Code