Skip to content

Commit 82fea5d

Browse files
Auto-file GitHub issues for nightly UT new failures
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.
1 parent 4086121 commit 82fea5d

9 files changed

Lines changed: 1699 additions & 3 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<!--
2+
Body template for issues filed by .github/scripts/ut_auto_issue.py.
3+
Placeholders use Python str.format() syntax: {field_name}. Every slot is filled
4+
with a fully pre-rendered string, so add no literal braces to this file.
5+
6+
The shape mirrors issue #5070. Two parts are load-bearing and must not change
7+
without updating their consumers:
8+
9+
* The `Cases:` block is parsed by fetch_issues.sh plus the awk filter in
10+
_linux_ut.yml. It must start with a line containing `Cases:`, carry one
11+
`<category>,<class name>,<test name>` line per case with no blank lines in
12+
between, and never be truncated - an incomplete block silently fails to
13+
skip the case. The cases:begin/end markers bound the region the bot is
14+
allowed to append to on a re-sighting.
15+
* The trailing ut-auto-issue marker carries the grouping signature and is how
16+
the bot recognises its own issues. Editing it will cause a duplicate issue.
17+
18+
Everything else is for humans and is safe to edit by hand.
19+
-->
20+
### 🐛 Describe the bug with skip template
21+
22+
<!-- cases:begin -->
23+
Cases:
24+
{cases}
25+
<!-- cases:end -->
26+
27+
{error_log}## Pytorch Version
28+
29+
{version_block}
30+
{evidence_block}### Versions
31+
32+
<details><summary>Detail</summary>
33+
34+
{collect_env}
35+
36+
</details>
37+
38+
<!-- {marker} -->

.github/actions/linux-testenv/action.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,16 @@ runs:
233233
python -c "import torchvision; print(torchvision.__version__)"
234234
python -c "import torchaudio; print(torchaudio.__version__)"
235235
python -c "import triton; print(triton.__version__)"
236-
python pytorch/torch/utils/collect_env.py
236+
# Land the environment and the exact shas under test in ut_log so they
237+
# ride along in the UT artifact; ut_auto_issue.py reads them to render
238+
# the issue body and to build baseline..current compare links.
239+
mkdir -p "${{ github.workspace }}/ut_log"
240+
python pytorch/torch/utils/collect_env.py |tee "${{ github.workspace }}/ut_log/collect_env.log"
241+
# collect_env only carries an abbreviated torch sha and no torch-xpu-ops sha.
242+
printf 'torch=%s\ntorch_xpu_ops=%s\n' \
243+
"${TORCH_COMMIT_ID}" \
244+
"$(git -C pytorch/third_party/torch-xpu-ops rev-parse HEAD 2>/dev/null || echo unknown)" \
245+
> "${{ github.workspace }}/ut_log/versions.txt"
237246
uv pip list |grep -E 'torch|intel'
238247
TORCH_CHECK_ID=$(python -c 'import torch; print(torch.version.git_version)')
239248
if [ "${TORCH_CHECK_ID}" != "${TORCH_COMMIT_ID}" ];then

.github/actions/linux-uttest/action.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,14 @@ runs:
201201
else
202202
echo -e "No Passed logs"
203203
fi
204+
# Copied the full case universe (includes skipped cases), used by
205+
# ut_auto_issue.py to classify a failure against its baseline.
206+
if ls all_cases_*.log 1> /dev/null 2>&1; then
207+
cp all_cases_*.log ${{ github.workspace }}/ut_log
208+
echo -e "All-cases logs Copied"
209+
else
210+
echo -e "No All-cases logs"
211+
fi
204212
# Copied the Summary logs
205213
if ls category*.log 1> /dev/null 2>&1; then
206214
cp category*.log ${{ github.workspace }}/ut_log

.github/scripts/check-ut.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
failures_by_category = defaultdict(list)
1616
passed_cases = []
1717
passed_by_category = defaultdict(list)
18+
# Every case seen, whatever its status. passed/failures alone cannot express
19+
# "skipped", which ut_auto_issue.py needs to tell a newly-enabled case from a
20+
# brand-new one.
21+
all_by_category = defaultdict(list)
1822
category_totals = defaultdict(lambda: {
1923
'Test cases': 0,
2024
'Passed': 0,
@@ -234,6 +238,7 @@ def parse_log_file(log_file):
234238
}
235239
failures.append(failure_case)
236240
failures_by_category[category].append(failure_case)
241+
all_by_category[category].append(failure_case)
237242
failures_number += 1
238243

239244
if failures_number > summary['Failures']:
@@ -299,11 +304,11 @@ def process_xml_file(xml_file):
299304
category_totals[category]['Errors'] += suite_summary['Errors']
300305

301306
for case in suite:
307+
case._file_category = category
308+
all_by_category[category].append(case)
302309
if get_result(case) not in ["passed", "skipped"]:
303-
case._file_category = category
304310
failures.append(case)
305311
elif get_result(case) == "passed":
306-
case._file_category = category
307312
passed_cases.append(case)
308313
passed_by_category[category].append(case)
309314
except Exception as e:
@@ -325,6 +330,19 @@ def generate_passed_log():
325330
status = get_result(case)
326331
log_file.write(f"{category},{class_name},{test_name}\n")
327332

333+
def generate_all_cases_log():
334+
"""Full case universe per category, including skipped cases."""
335+
for category, cases in all_by_category.items():
336+
if not cases:
337+
continue
338+
339+
log_filename = f"all_cases_{category}.log"
340+
with open(log_filename, "w", encoding='utf-8') as log_file:
341+
for case in cases:
342+
class_name = get_classname(case)
343+
test_name = get_name(case)
344+
log_file.write(f"{category},{class_name},{test_name}\n")
345+
328346
def generate_category_totals_log():
329347
"""Generate log files with category totals"""
330348
for category, totals in category_totals.items():
@@ -390,6 +408,7 @@ def main():
390408

391409
generate_failures_log()
392410
generate_passed_log()
411+
generate_all_cases_log()
393412
generate_category_totals_log()
394413
print_summary()
395414

0 commit comments

Comments
 (0)