ci: annotate a red pytest job with the tests that failed, not a warning - #7479
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Root-cause fix at the right layer: annotations come from pytest's own report objects, with the lying matcher removed inline where each run happens. Suggestions
[DESIGN-REVIEWED] 4600c5b |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of First-Principles-Verdict: PASS Every item traces to one measured defect — six red jobs annotated with a warning and zero failing tests — and each fixes or guards that cause. What this change shipsIntent: make a red CI job's check-run annotations name the tests that actually failed. This is a FIX.
Counts run: [FIRST-PRINCIPLES-REVIEWED] 4600c5b |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
0775b41 to
be9b4dd
Compare
|
First Principles CONCERNS -- adopted in full at The finding was right and the inconsistency was mine:
Agreed on the reasoning too: a release-time red is read under pressure, which is the worst place to annotate a warning instead of the failing test. Design Review PASS, GPT 5.6 no blocking findings, Opus 4.8 no findings -- all on Remaining reds are main-owned and deliberately not folded in: (The one-word comment just above this was an accidental probe post; this is its intended content.) |
|
Note on the The lane's VERDICT is clean -- its comment reads
That file does not exist in this repository. It was the throwaway probe from the Manual verification section, and the annotation exists because that section pasted a raw workflow-command line starting at column 0. Any job that prints this description into its log therefore EXECUTES it. Fixed by rendering those examples as a table instead; the diff itself was already clean (no added line starts with a command introducer, checked). The same job also logged two runner errors from reading Adding to the pattern harvest, because it generalizes past this PR: a workflow command at the start of a line is executable by any job that echoes the text, so a PR description, an issue comment, or a fixture that carries one is a live annotation injection. The mechanical form is a lint over PR/issue bodies and test fixtures for a line matching the command introducers. No code change was needed for this; editing the description re-triggers the review lane, which is what should clear the red. |
A red Backend Tests shard is read from its check run's annotations, and
nothing in this repository wrote one -- so they came from the `python`
problem matcher `actions/setup-python` registers by default, whose
pattern is a traceback frame followed by `raise SomeError('msg')`.
Measured on the six jobs cited in #7296, that pattern matched pytest's
warnings summary every time and a pytest failure not once: all six reds
were annotated only `Event loop is closed` at line 545 -- which is
asyncio/base_events.py inside `_check_closed`, reached from a
PytestUnraisableExceptionWarning about a garbage-collected coroutine --
while the tests that actually failed appeared nowhere.
The rootdir conftest now emits one `::error` annotation per failing
report, naming the node id with its file and 1-based line, and the four
jobs that run pytest set `add-problem-matchers: false`.
Closes #7296
be9b4dd to
4600c5b
Compare
NicholasRBowers
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: chore/CI plumbing (5 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: CI failure-annotation plumbing (workflow matcher removal + conftest terminal-summary emitter + its test), no runtime impact.
Problem / Motivation
A red pytest job is read from its check run's ANNOTATIONS -- that is what the PR
page renders, what a fork contributor who cannot re-run a job has, and what a
triage report copies. Nothing in this repository wrote one, so the annotations
came from the
pythonproblem matcher thatactions/setup-pythonregisters bydefault (
add-problem-matchersdefaults to true). Its pattern is a tracebackframe (
File "...", line N, in f) followed byraise SomeError('msg'), appliedto the whole log -- including the part where pytest prints its warnings summary.
Measured on the six Backend Tests jobs cited in #7296, that pattern matched a
warning traceback every time and a pytest failure not once:
Event loop is closedtest_mcp_gatewayd_coverage.py::TestZombieDiagnostic::test_dead_accept_loop_is_dumped_and_stops_the_daemon(missing diag.jsonl)Event loop is closedtest_md_notebook.py::test_sync_refuses_rather_than_pushing_a_subset(git addtimed out after 120s)Event loop is closedtest_project_bundle_git/test_project_capabilitiesEvent loop is closedEvent loop is closedEvent loop is closedtest_data_home_not_relocatable.py, 2 alias casesThe annotated line number is the same on every platform because it is not a line
in this repository: 545 is
asyncio/base_events.py, inside_check_closed,reached from a
PytestUnraisableExceptionWarningabout a garbage-collectedcoroutine. So the reported symptom is real and its stated cause is not -- in all
six jobs
Event loop is closedwas a WARNING, the shard exit code came fromordinary named failures, and not one of those tests was named in an annotation.
Why it matters
Four unrelated PRs were filed and triaged as an event-loop teardown flake on that
evidence, and the class had already been "fixed" four times (#4764, #4784, #5491,
#5856) before this recurrence. The cost the issue describes -- "the red carries no
actionable test identity", "there is no test name to bisect from" -- is produced
by the annotation pipeline, not by asyncio. Until the annotations name the real
test, every future red of any cause gets read as this flake again, and on a fork
PR the CI red also skips all five
fork-*-reviewlanes, so a misread costs thereview round as well as the CI round.
What changed (motivation -> approach -> change)
Symptom: the check run names a warning and hides the red. Root cause: the only
thing writing annotations is a text matcher that cannot tell a warning traceback
from a failure and has no pattern for a pytest failure at all.
A better matcher cannot be the fix.
--color=yesis inaddopts, so the summaryline a matcher would have to scrape reads
\x1b[31mFAILED\x1b[0m test/x.py::\x1b[1mtest_y\x1b[0m - AssertionError: ...,with escape sequences inside the node id. The report objects already carry the
same facts as data, so:
conftest.pygainspytest_terminal_summary, which emits one::errorannotation per failed/errored report:
fileand 1-basedlinefromreport.location,titlefrom the node id, message from theE-marked line(
reprcrashis the preamble for a fixture error and would spend the wholeannotation saying
file <path>, line 5). Controller-only, so an xdist worker'sreports are not annotated twice; gated on
GITHUB_ACTIONS, so a local red isunchanged; capped at 10 with a
::notice::counting the rest, because GitHubdrops the excess silently.
test/conftest.py: the in-package appsuites never load the latter and they red the same jobs.
backend-test,backend-test-windows,backend-test-macos,backend-test-sandbox) echo::remove-matcher owner=python::from inside the step that runs pytest, so thematcher that lied cannot put a louder wrong answer next to the right one, and
the directive cannot be separated from the run it protects by a later
reordering.
add-problem-matchers: falsewas tried first and is a trap: thatinput does not exist on
actions/setup-pythonv7.0.0 at this pin, so theaction ignored it and the runner answered with an
Unexpected input(s)warning -- one more annotation saying nothing about the tests. Caught by this
PR's own first CI run and now pinned by a test.
Deliberately NOT changed: the
Event loop is closedunraisables themselves. Theyare real leaked coroutines (
turn_dispatch.py:385 _bounded_turnandgatewayd.py:3291 _drain_inbox_to_stub, both GC-finalized after their loopclosed) but they are warnings, they failed nothing here, and silencing them is a
per-site product change that would also have hidden this diagnosis. Widening
_drain_windows_proactor_finalizerspast its win32/sessionfinishscope wasconsidered and rejected for the same reason: it suppresses the evidence.
Tests
test/test_ci_failure_annotations.py, 20 cases. Mutation-verified: 18 of the 20fail with both production files reverted to
origin/main(the 2 that pass are thesubject-list guard and the not-an-input guard, neither of which depends on the fix).
line=42); setup/teardown errors are annotated too; a collection error with noline omits
line=rather than guessing 1fixture 'x' not found, not thefile <path>, line 5preamble; an indented
E = compute()source line is not mistaken for the verdict::and a comma inside a parametrized id are escaped (%3A%3A,%2C)so the runner does not truncate the annotation at the first separator;
%andcarriage-return and newline escaped; a 5000-char reason truncated
::notice::; exactly 10 emits no noticepytest (so a NEW pytest job is covered without editing a list); each must remove
the matcher at or before its FIRST pytest step (ordering asserted, not assumed --
a directive placed after the run leaves it fully matched); no workflow may pass
add-problem-matchersto a step at all; the hook must be in the rootdir conftestand not in
test/conftest.pyGates on the touched files: black, isort, flake8 clean;
check_black_formatting,check_testpaths_coverage,check_sync_io_in_async,check_loop_bound_locks,check_brand_name,check_focus_cue,check_changelog_historyall pass.Manual verification
Live proof on this PR's own CI. The first run's
Backend Tests (Windows) (3)and(3.10, 3)went red on a main-owned ratchet drift, and the check-run annotationnow reads:
Exact file, exact line, node id as the title -- against the
Event loop is closedat
.github:515that the old matcher put on the same job. That same run is whatexposed the
add-problem-matchersmistake, which is fixed in this revision.Before that, ran a local pytest invocation with two deliberately broken tests and
GITHUB_ACTIONS=trueagainst the real rootdir conftest. Emitted:filelinetitle(Rendered as a table on purpose. A raw
::error ...line pasted at the start of aline is a LIVE workflow command to any job that prints this description into its
log -- the first revision of this section made the GPT review job emit a bogus
test/test_zz_annotation_probe.py:1annotation for a file that does not exist inthe repository. Same defect class this PR is about, so it is not repeated here.)
The probe file was removed afterwards; the tree is clean.
Known remaining case, stated rather than fixed: the rootdir
pytest_sessionfinishresidue guard can turn a green run red AFTER
pytest_terminal_summaryhas run, soa residue-only red still carries no annotation. It prints its own explicit
repository root residuesection, and folding an annotation into that path needsits own test setup, so it is left for a follow-up rather than claimed here.
Related Issues
Closes #7296
Pattern harvest
Rule candidate: review-prompt. Pattern: "a CI diagnosis taken from check-run
annotations when no step in the repository writes them" -- if annotations are
produced by a generic text matcher rather than by the tool that knows what
failed, the annotation is a guess and the shard's own summary is the evidence.
Concretely reusable check: any job that runs a test framework should either emit
its own annotations or leave
add-problem-matchersoff; a job doing neither willeventually annotate a warning as its failure. The four predecessors of this issue
are what that class costs when it is not caught.