Deflake CI: TUF warn mode, readiness diagnostics, test races - #6063
Open
JAORMX wants to merge 6 commits into
Open
Deflake CI: TUF warn mode, readiness diagnostics, test races#6063JAORMX wants to merge 6 commits into
JAORMX wants to merge 6 commits into
Conversation
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
The SessionStorageWarning condition and ReadyReplicas are written by the operator's reconcile loop, which lags pod readiness; asserting them bare right after the pods-ready polls flaked with ReadyReplicas 1 vs 2 observed 2ms after the pod check passed (PR #6055's run). Poll the status like every other eventually-consistent read in the spec. 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
JAORMX
requested review from
ChrisJBurns,
amirejaz,
aponcedeleonch,
blkt,
jerm-dro,
jhrozek,
rdimitrov,
reyortiz3 and
tgrunnagle
as code owners
July 27, 2026 20:58
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6063 +/- ##
==========================================
+ Coverage 72.18% 72.25% +0.06%
==========================================
Files 721 721
Lines 75062 75075 +13
==========================================
+ Hits 54183 54242 +59
+ Misses 17017 16953 -64
- Partials 3862 3880 +18 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
ChrisJBurns
approved these changes
Jul 27, 2026
jhrozek
approved these changes
Jul 27, 2026
11 tasks
Collaborator
|
/retest |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The merge queue is being blocked by a handful of recurring CI flakes. Analysis of the last three weeks of
Main buildruns onmain(146 runs, 75 failed) ranks the group-workload readiness family as the top three flakes (8, 6, and 4 failures respectively acrossrm_group_test.goandgroup_rm_test.go), with a conformance gating gap, an operator acceptance-test race, and an envtest watch-test race behind them. This PR fixes one proven product bug, closes the conformance gating gap, fixes two racy test assertions, and makes the remaining (not yet fully explained) readiness stall diagnosable from CI output instead of invisible.What the failure evidence actually shows, per flake:
1. Sigstore TUF outages hard-fail
thv runin warn mode (product bug, root-caused). In run 30297766499 (the push of #6043 itself), 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. Readiness timeouts (
should delete group with workloadsand siblings, #6040/#6043) — diagnosability, not a timeout bump. #6040 is closed and #6043 ("Harden group e2e workload naming and waits",be0e0c34) was its fix, but the failure is still live: PR #6054, whose base isbe0e0c34, failed identically. Mining the post-#6043 failures shows the 2-minute budget is not obviously the problem, but not for the reason previously assumed:running), so the failed workload had almost exactly its own 2m budget from launch.thv runreturning (Server listening on 8080at 20:22:27; timeout at 20:24:29). The stall is inside the detached supervisor, somewhere between transport start and therunningstatus flip.status "error"with no context, container again healthy.Neither CI log can say why, because of three diagnosability gaps this PR closes:
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().waitForInitializeSuccess) logged per-attempt outcomes at DEBUG only, invisible in CI; a workload stuck instartingleft no trace of what the probe observed. It now logs an INFO progress line every 30s with the last observed outcome and includes it in the timeout error.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.I deliberately did not raise
ServerReadyTimeout: the two observed post-#6043 failure modes are a harderror(a bump is irrelevant) and a stall of unknown depth (a bump is speculation). The product's own probe budget is 5m before it continues anyway, so if the next diagnosable failure shows slow-but-recovering supervisors,TOOLHIVE_E2E_SERVER_READY_TIMEOUT(already supported, no code change) is the follow-up knob.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 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.
dual_era_k8s_test.goasserted reconcile-lagging status bare.MCPServer.Status(theSessionStorageWarningcondition andReadyReplicas) is written by the operator's reconcile loop, which lags pod readiness; the spec asserted it immediately after the pods-ready polls and failed withReadyReplicas1 vs 2 observed 2ms after the pod check passed (PR #6055's run). The status read is now polled like every other eventually-consistent read in the file. The sibling specs and the rest oftest/e2e/thv-operatorwere checked for the same bare-assert-on-status pattern; this was the only instance.5. 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.References: #6040 (closed), #6043, #5886, #5888, modelcontextprotocol/conformance#407. Note for maintainers: #6040 is closed against a failure that is still live post-#6043 — per the analysis in point 2, the remaining
starting-stall mechanism is not yet root-caused; this PR makes the next occurrence name its cause. I have deliberately not reopened or commented on the issue.Type of change
Test plan
Unit tests
E2E tests (
task test-e2e)Linting (
task lint-fix)Manual testing (describe below)
go build ./...,go vetandgo teston every touched package (pkg/runner/retriever,pkg/runner,pkg/workloads) are green.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.task lint-fixreports 0 issues.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.Not run locally (no cluster/docker in this environment): the k8s operator suites and the CLI e2e suites. The
dual_era_k8s_test.goandvirtualmcpserver_compositetool_watch_test.gochanges are compile-verified (go vet) only; the former is CI-only by its own header comment. The watch-test change 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/e2e/thv-operator/acceptance_tests/dual_era_k8s_test.goMCPServer.Status(SessionStorageWarning + ReadyReplicas) instead of asserting bare after pod readinesstest/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
starting-stall — the honest state is that the stall point inside the detached supervisor is unknown because CI output hid it; this PR makes the next occurrence self-explanatory (status context + probe progress + proxy log dump). If reviewers prefer, the readiness budget can additionally be raised viaTOOLHIVE_E2E_SERVER_READY_TIMEOUTine2e-tests.ymlwith no code change, but the evidence does not currently justify it.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 has been failing CI, and it contradicts the mode's documented intent ("prints a warning when image validation fails").stablePolls >= 8settle window in the watch test trades ~2s of extra runtime for immunity to the observed settle race (the flake's transition landed ~2s after the baseline).Generated with Claude Code