Skip to content

Auto-file GitHub issues for nightly UT new failures - #5119

Open
RUIJIEZHONG66166 wants to merge 4 commits into
mainfrom
agent/nightly-ut-auto-issue
Open

Auto-file GitHub issues for nightly UT new failures#5119
RUIJIEZHONG66166 wants to merge 4 commits into
mainfrom
agent/nightly-ut-auto-issue

Conversation

@RUIJIEZHONG66166

Copy link
Copy Markdown
Contributor

Nightly UT new failures are today only visible as a red job and a CSV artifact, and muting one means a human hand-writing a skip issue. This adds a deterministic bot (no model calls) that files one GitHub issue per root cause, plus the artifacts it needs from the existing UT pipeline.

Filing an issue mutes a test. fetch_issues.sh subtracts the Cases: block of every open skipped issue from the next night's failures. Every decision in this change therefore fails in the direction of leaving a case running rather than silently muting it, and the job stays in --dry-run until the repo variable UT_AUTO_ISSUE_ENABLED is set to true. Merging this changes no issue state.

Review order

  1. The producers — these only add artifacts and change no existing behaviour. check-ut.py writes all_cases_<category>.log (the full case universe, which unlike passed+failures also carries skipped cases); ut_result_check.sh records its per-category count verdict to run_health.jsonl; the two composite actions land collect_env.log and versions.txt under ut_log so the shas actually under test travel with the artifact.
  2. The wiring_linux_ut.yml exports UT_RUN_HEALTH_FILE, _ut_auto_issue.yml is a new callable workflow, and nightly_ondemand.yml calls it after the UT matrix on !cancelled() rather than success(), since the UT job fails exactly when there is something to file.
  3. ut_auto_issue.py — its stage banners, Stage 0 through Stage 8, are the intended reading order.
  4. ut-auto-issue-body.md — the Cases: block and the trailing marker are both machine-consumed; the header comment says which consumer reads what.

Safety rails

Trigger Effect
Build job not successful (H1) File nothing for the run
Category below 95% of expected cases (H5) File nothing for that category
Infra signatures >30% of a leg's new failures (H7) Treat the leg as infra breakage, file nothing
Infra signature on a group (OOM, device lost, ...) Quarantine: reported, not filed, not muted
>5000 new failures Abort, create nothing
Issue body would exceed 60000 chars on append Refuse to append — the case stays unmuted
More root causes than MAX_ISSUES_PER_RUN (15) Umbrella issue, unlabelled, mutes nothing

Design choices with a real alternative

Grouping keys on (normalized error, test file), not the error alone. The error alone would collapse a generic message — a precision mismatch, or the sentinel used when a case dies without one — across unrelated files into a single issue and therefore a single very wide mute. Over-splitting is the safe direction, so one root cause spread over several files stays several issues, and they are linked to each other by a comment instead of being merged.

The baseline is resolved per category, not per run. There is no "last good nightly", only "the last nightly in which this category completed healthily": a run that truncated in op_ut is still a perfectly good op_extended baseline.

The per-run issue cap admits or defers a whole error family at once. A plain head/tail cut on the size-ordered list would file the big test files of one root cause and defer its small ones — muting half a bug and leaving the rest to fail every night.

Before enabling

UT_AUTO_ISSUE_ENABLED must stay unset until the job has been dry-run against a few real nightlies and the rendered bodies in the report artifact reviewed by hand. The normalization rules are the part most likely to need tuning. The labels skipped, skipped_bmg, regression and new_case_failure must exist in the repo first — ensure_labels() hard-fails otherwise.

Test: none (CI-tooling change with no device-side behaviour). Validation is the script's own dry-run mode against a real past nightly, which is also the gate before UT_AUTO_ISSUE_ENABLED is set:

python3 .github/scripts/ut_auto_issue.py --run-id <past_nightly_run_id> --dry-run

That command was not executed on this workstation — it needs a real nightly run id. During development the pure logic (normalization stability across nights, grouping, the family-atomic issue budget, and the Cases: parse/append contract that does the actual muting) was exercised against stubbed inputs, including a main() smoke test with every gh call replaced by a stub.


Authored with the assistance of an AI coding agent.

@RUIJIEZHONG66166
RUIJIEZHONG66166 force-pushed the agent/nightly-ut-auto-issue branch from 82fea5d to 5046519 Compare August 27, 2026 02:54
@RUIJIEZHONG66166 RUIJIEZHONG66166 added the disable_e2e Disable all e2e test jobs for the PR label Aug 27, 2026
@RUIJIEZHONG66166
RUIJIEZHONG66166 force-pushed the agent/nightly-ut-auto-issue branch from 5046519 to 2f0ec2e Compare August 27, 2026 04:00
New failures in the nightly UT run are today only visible as a red job and a
CSV artifact, and muting one means a human hand-writing a skip issue. This adds
a deterministic bot that files one issue per root cause, plus the artifacts it
needs from the existing UT pipeline.

Filing an issue mutes a test: fetch_issues.sh subtracts the `Cases:` block of
every open `skipped` issue from the next night's failures. Every decision here
therefore fails in the direction of leaving a case running rather than silently
muting it, and the job stays in --dry-run until the repo variable
UT_AUTO_ISSUE_ENABLED is set to `true`.

Review in this order:

1. The producers. These only add artifacts and change no existing behaviour:
   check-ut.py writes all_cases_<category>.log (the full case universe, which
   unlike passed+failures also carries skipped cases), ut_result_check.sh
   records its per-category count verdict to run_health.jsonl, and the two
   composite actions land collect_env.log and versions.txt under ut_log so the
   shas actually under test travel with the artifact.
2. The wiring: _linux_ut.yml exports UT_RUN_HEALTH_FILE, _ut_auto_issue.yml is
   a new callable workflow, and nightly_ondemand.yml calls it after the UT
   matrix - on !cancelled() rather than success(), since the UT job fails
   exactly when there is something to file.
3. ut_auto_issue.py. Its stage banners, Stage 0 through Stage 8, are the
   intended reading order.
4. ut-auto-issue-body.md, whose `Cases:` block and trailing marker are both
   machine-consumed; its header comment says which consumer reads what.

Three choices had a real alternative and are worth calling out.

Grouping keys on (normalized error, test file) rather than on the error alone.
The error alone would collapse a generic message - a precision mismatch, or the
sentinel used when a case dies without one - across unrelated files into a
single issue and therefore a single very wide mute. Over-splitting is the safe
direction, so one root cause spread over several files stays several issues,
and they are linked to each other by a comment instead of being merged.

The baseline is resolved per category, not per run. There is no "last good
nightly", only "the last nightly in which this category completed healthily":
a run that truncated in op_ut is still a perfectly good op_extended baseline.

The per-run issue cap admits or defers a whole error family at once. A plain
head/tail cut on the size-ordered list would file the big test files of one
root cause and defer its small ones, muting half a bug and leaving the rest to
fail every night.

Test Plan

No automated test ships with this change, so the pre-enable dry run below is
the gate, not a formality.

During development the pure logic was exercised against stubbed inputs -
normalization stability across nights, grouping, the family-atomic issue
budget, the Cases: parse/append contract that does the actual muting, and a
main() smoke test with every gh call replaced by a stub. Those harnesses were
ad hoc and are not part of the change.

Before UT_AUTO_ISSUE_ENABLED is set, the job must be dry-run against real past
nightlies and the rendered bodies in the report artifact reviewed by hand; the
normalization rules are the part most likely to need tuning:

```bash
python3 .github/scripts/ut_auto_issue.py --run-id <past_nightly_run_id> --dry-run
```

That command was not executed here: this workstation has no authenticated gh
and the script needs a real run id. The workflow's own default is --dry-run, so
merging this changes no issue state until the repo variable is set.

Authored with the assistance of an AI coding agent.
A nightly dry run against run 32733042537 classified
`third_party.torch-xpu-ops.test.xpu.test_dataloader_xpu` as a new case failure.
The file has been in the repo since #2989. Nothing about it is new.

The row is not a test case at all. pytest reports a module that failed to
import as a testcase with an empty classname and the dotted module path as the
name, because _pytest/junitxml.py:mangle_test_address has nothing before the
first `::` to put in classname. classify_case compares a case against its
baseline by exact set membership, and a healthy baseline records that module's
individual methods and never the module itself, so the row is in neither the
passed nor the failed set and falls through to CLS_NEW_CASE unconditionally -
for any file, of any age.

Three things follow from that, in increasing severity. The label sends triage
the wrong way: "upstream added a test we do not support yet" rather than
"upstream deleted a symbol we import". Filing the issue mutes that one row, so
the file stays dark every night with nobody told. And the cases the row hides
do not fail, they vanish - a module that fails to import contributes nothing to
passed_<cat>.log or failures_<cat>.log, so those cases never reach
new_ut_failure_list.csv, and a few hundred missing cases sit far below the 5%
count gate in ut_result_check.sh:check_test_cases.

Review in this order.

1. Case.is_collection_error / Case.module / Case.test_file. The identity fix.
   `module` is the granularity the two other stages need, and deriving
   test_file from it also repairs a second bug: a collection error used to
   yield test_file "unknown", which is part of the grouping key and of `sig`,
   so unrelated files collapsed into one group and one mute. For a real case
   test_file is unchanged, so no existing signature moves and MARKER_VERSION
   stays put.
2. build_groups. Collection errors join the infra denylist in being
   quarantined: reported, never filed, never muted. Group.quarantined becomes
   Group.quarantine, a reason, because the report now has two things to say.
3. Stage 4b. collection_error_context answers "how much stopped running" from
   the baseline's per-module index, and record_vanished_modules asks the same
   question independently of any error row.
4. The main() wiring and the finish() rendering.

classify_case itself is unchanged; it only gained a comment saying why.

Three choices had a real alternative.

Report-only rather than filing these as regressions. Looking the module up in
the baseline does establish that its cases used to pass, so a `regression`
label would be defensible and would get the issue triaged. But filing is what
mutes, and muting a row that stands for an entire dark file is the worst
outcome available here. The nightly stays red instead.

The baseline artifact, not git history, answers "did this file exist before".
File age is not case age, the rows are dotted module paths rather than repo
paths, and the op_ut leg also runs upstream files that are not in this repo.
The baseline is exact and is already downloaded.

Module granularity is confined to collection errors. Widening classify_case to
compare every case against its module would be wrong in the other direction: a
dtype parametrization added upstream is a genuine new case even though its
module is years old.

One cost worth naming: Stage 4b now runs before the nothing-to-file exit, so a
green nightly does a baseline walk it used to skip. That is deliberate. A green
night is exactly the night a silently emptied file has to be caught, and the
skip list is a plausible way to empty one - xpu_test_utils.py:launch_test turns
it into `pytest -k "not ..."`, and deselected cases are absent from the JUnit
XML entirely rather than recorded as skipped.

Test Plan

The pure logic was exercised locally against stubbed artifacts: the three rows
this run actually reported, plus the boundaries the fix must not get wrong - a
collection error on a file the baseline never saw is still reported as a new
test file, a newly parametrized case in an old module is still
new_case_failure, regression/persistent/skipped-then-run classification for
real cases is byte-identical to before, a row parsed out of a log by
check-ut.py:parse_log_file is not mistaken for a collection error, and the
infra denylist still wins. Those harnesses were ad hoc and are not part of this
change.

The gate is the dry run, which was not executed here: this workstation has no
authenticated gh.

```bash
python3 .github/scripts/ut_auto_issue.py --run-id 32733042537 --dry-run
```

Two things to check in its report. test_dataloader_xpu should have left the
issue list for the "Modules that produced no cases in this run" table, carrying
the count of cases it used to pass. And that table should be short - if a
routine skip-list edit fills it every night it needs a significance threshold,
which this change deliberately does not guess at.

lintrunner was not run: it is not installed on this workstation and there is no
.venv to activate. The file parses, has no line over 100 characters, no tabs,
no trailing whitespace and no non-ASCII.

Authored with the assistance of an AI coding agent.
Whether the bot files anything took three negations to read: the repo
variable was compared with `!= 'true'` to produce `dry_run`, the workflow
passed `--dry-run`, and every write site in the script was guarded by
`if not args.dry_run`. Reading the chain to answer "does a nightly file
issues" meant composing all three, and it is easy to mistake the
comparison in nightly_ondemand.yml for a place where the variable is set
rather than read.

Same behaviour, stated positively: UT_AUTO_ISSUE_ENABLED == 'true' feeds
a `create_issues` input, which passes --create-issues, which each write
site checks directly. Unset variable still means nothing is filed.

This also removes a footgun in the command line. --dry-run had to be
added to be safe, so a bare `ut_auto_issue.py --run-id N` filed issues
for real, and filing mutes tests. --create-issues has to be added to be
destructive, so a bare invocation now only reports.

The reusable workflow's input is renamed and inverted along with it
(default false), and the report key and rendered wording follow: "dry
run" becomes "report only".

Test Plan

lintrunner was not run: it is not installed on this workstation and
there is no .venv in the repo root or its parent to activate. The file
compiles, has no line over 120 characters and no non-ASCII.

Syntax of all three files:

```bash
python3 -m py_compile .github/scripts/ut_auto_issue.py
python3 -c "import yaml; [yaml.safe_load(open(f)) for f in \
  ['.github/workflows/_ut_auto_issue.yml','.github/workflows/nightly_ondemand.yml']]"
```

All seven write sites - ensure_labels, the two still-failing
comment_issue calls, edit_body, create_issue for a group, the cross-file
comment_issue, and create_issue for the umbrella - were enumerated and
their guard context read individually to confirm none became reachable
without --create-issues:

```bash
grep -n "create_issue(\|comment_issue(\|edit_body(\|ensure_labels(" \
  .github/scripts/ut_auto_issue.py
```

The flag semantics and the renamed report key were exercised directly,
asserting that a bare parse yields create_issues=False and that
report.json, stdout and GITHUB_STEP_SUMMARY agree on "report only"
versus "live". The harness was ad hoc and is not part of this change.

The end-to-end gate is unchanged and still pending: a report-only run
against a real past nightly, which needs an authenticated gh this
workstation does not have.

```bash
python3 .github/scripts/ut_auto_issue.py --run-id <past_nightly_run_id>
```

Note that the two earlier commits on this branch quote `--dry-run` in
their own Test Plan sections. Those commands are now spelled without a
flag; the messages were left alone rather than rewritten.

Authored with the assistance of an AI coding agent.
A report-only run against nightly 33207055708 filed nothing and said only
that op_ut was infra breakage. The leg had two new failures, one of which
matched the infra denylist, and 1/2 is above the 30% threshold, so H7
discarded the leg and took the other failure with it.

The gate had no minimum sample size. At a 30% threshold a single
infra-looking failure clears it on its own for any n <= 3: 1/1, 1/2 and
1/3 are all above 0.3. The night a leg produces two new failures is
exactly the night the ratio carries no information, and that is when the
gate was most likely to fire.

It also cost nothing to fire it wrongly, in the sense that the gate adds
no protection at that size. A case whose own error matches the denylist
is quarantined by build_groups regardless - reported, never filed, never
muted. H7 exists only to distrust the cases *around* those, which is a
claim about the machine, and two data points cannot support it. Here the
only thing it achieved was to hide a genuine-looking forward-AD failure
in test_ops_fwd_gradients_xpu.

So the ratio now applies only over INFRA_MIN_CASES or more failures.
Below that the infra-looking cases are still quarantined individually
and a note says the gate was not applied. 10 is where a lone infra
failure sits at 10%, comfortably under the threshold, while a runner
that actually lost its GPU produces far more than ten.

The alternative was to lower INFRA_SIGNATURE_RATIO instead, which is
backwards: the problem is not that 30% is the wrong share, it is that a
share of two is not a measurement.

Second change, in the report rather than the logic. H7 is the only gate
that drops failures without rendering them anywhere - collect_leg
returns an empty list, so the cases never reach report["quarantined"]
and no body is written for them - and it recorded only the string
"infra signature ratio (H7)". Reading the report could not answer which
failures were discarded or which of them matched the denylist. It now
records every dropped case with its error and its infra verdict, and
finish() lists them under the skipped leg.

`infra` becomes a set because the report needs the individual cases
rather than a count, and because `c in infra` over a list would be
quadratic in a leg that can carry up to ABORT_THRESHOLD cases.

Test Plan

lintrunner was not run: it is not installed on this workstation and
there is no .venv in the repo root or its parent. The file compiles, has
no line over 120 characters and no non-ASCII.

```bash
python3 -m py_compile .github/scripts/ut_auto_issue.py
```

The gate was exercised directly on the two rows from run 33207055708
and on a synthetic leg large enough for the ratio to mean something.
With n=2 and one infra match both cases now survive and no leg is
skipped; with n=20 and twelve infra matches the leg is still discarded,
all twenty dropped cases are recorded, twelve are flagged infra, and the
rendered summary lists each one. The firing table was checked across
n=1,2,3,4,9,10,20 to confirm a lone infra failure no longer trips the
gate at any size while 4/10 and 9/20 still do. That harness was ad hoc
and is not part of this change.

The end-to-end gate is the report-only run that surfaced this, repeated
against the same nightly:

```bash
python3 .github/scripts/ut_auto_issue.py --run-id 33207055708 \
  --test-type build-weekly \
  --work-dir ~/auto_file_github_issue/ut-auto-issue \
  --report-dir ~/auto_file_github_issue/ut-auto-issue-report
```

op_ut should no longer appear under skipped legs. The collection error
on test_prims_xpu should be quarantined as such and reported with the
count of cases that module stopped contributing; the forward-AD failure
should be classified against its baseline and rendered.

Authored with the assistance of an AI coding agent.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

disable_e2e Disable all e2e test jobs for the PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant