Skip to content

Commit 7d5d073

Browse files
authored
Add revert category extractionand exclude ghfirst reverts from stats (#6882)
Adds revert category extraction from GitHub comments and excludes `ghfirst` reverts from precision/recall metrics. ## Changes ### 1. Added Revert Category Extraction - New method `extract_revert_categories_batch()` in `autorevert_checker.py` - Extracts categories (`nosignal`, `ignoredsignal`, `landrace`, `weird`, `ghfirst`) from GitHub issue comments - Single batch query for performance ### 2. Enhanced `get_commits_reverted_with_info()` - Now includes category information for each revert - Uses batch extraction for all reverts at once ### 3. Updated Metrics Calculation - Excludes `ghfirst` reverts from recall calculation - Shows category breakdown in summary statistics - Per-workflow precision now shows both total and non-ghfirst metrics ### 4. Fixed Pattern Detection Bug - Fixed `AttributeError: 'NoneType' object has no attribute 'head_sha'` - Created proper mapping between failures and their newer commits <details> <summary>Bug Fix Details</summary> **Problem**: `newer_commit_same_job` was used outside its loop scope **Solution**: Created `failure_to_newer_commit` dict to track mappings ```python # Map each failure to its newer commit failure_to_newer_commit = {} for (rule, job) in suspected_failures: newer_commit_same_job, newer_same_jobs = self._find_last_commit_with_job(...) if newer_commit_same_job and any(...): failure_to_newer_commit[(rule, job)] = newer_commit_same_job # Use mapping in pattern creation for (failure_rule, job_name), newer_commit in failure_to_newer_commit.items(): patterns.append({ "newer_commits": [newer_commit.head_sha, suspected_commit1.head_sha], ... }) ``` </details> ## Example Output ``` python -m pytorch_auto_revert autorevert-checker Lint trunk pull inductor linux-binary-manywheel --hours 720 --verbose ================================================== SUMMARY STATISTICS ================================================== Workflow(s): Lint, trunk, pull, inductor, linux-binary-manywheel Timeframe: 720 hours Commits checked: 6741 Auto revert patterns detected: 419 Actual reverts inside auto revert patterns detected (precision): 50 (11.9%) Total revert commits in period: 121 Revert categories: nosignal: 46 (38.0%) ghfirst: 28 (23.1%) uncategorized: 21 (17.4%) ignoredsignal: 16 (13.2%) weird: 9 (7.4%) landrace: 1 (0.8%) Total reverts excluding ghfirst: 93 Reverts (excluding ghfirst) that dont match any auto revert pattern detected (recall): 50 (53.8%) Per workflow precision: Lint: 6 reverts out of 17 patterns (35.3%) [excluding ghfirst: 6 (35.3%)] trunk: 2 reverts out of 14 patterns (14.3%) [excluding ghfirst: 2 (14.3%)] pull: 40 reverts out of 354 patterns (11.3%) [excluding ghfirst: 33 (9.3%)] inductor: 2 reverts out of 31 patterns (6.5%) [excluding ghfirst: 2 (6.5%)] linux-binary-manywheel: 0 reverts out of 3 patterns (0.0%) [excluding ghfirst: 0 (0.0%)] Reverted patterns: - Python RuntimeError: e1aee866 (ignoredsignal) - GitHub workflows weren't regenerated: 3b6569b1 (ignoredsignal) - GitHub workflows weren't regenerated: bbbced94 (landrace) - Python RuntimeError: 060838c2 (nosignal) - Lintrunner failure: 1a55fb0e (weird) - Lintrunner failure: 3239da0c (nosignal) - MSVC compiler error: eab45643 (ignoredsignal) - Bad response status code: ea7b2330 (uncategorized) - gtest failure: 347ace4c (nosignal) - pytest failure: 216bd609 (nosignal) - pytest failure: 84c588e5 (ignoredsignal) - GHA error: 863327ae (ignoredsignal) - GHA error: eb9efb37 (ghfirst) - GHA error: 9c39bc24 (ignoredsignal) - Python Test File RuntimeError: 6de41ce0 (uncategorized) - Fallback for other test failure rules: 3f920f3d (nosignal) - pytest failure: f179b719 (ghfirst) - pytest failure: 92409b6c (uncategorized) - pytest failure: d1b4e0fa (nosignal) - pytest failure: 099d0d61 (uncategorized) - pytest failure: c79c7bbe (nosignal) - pytest failure: c95f7fa8 (nosignal) - pytest failure: 08dae945 (weird) - pytest failure: fb75dea2 (uncategorized) - pytest failure: 9de23d0c (uncategorized) - pytest failure: 830a335a (ghfirst) - pytest failure: 6d3a4356 (ignoredsignal) - pytest failure: a6a3a441 (nosignal) - Python Test timeout (KeyboardInterrupt): 8142a028 (nosignal) - pr_time_benchmarks regression: 2b9d638e (weird) - pytest failure: dc5e8f79 (nosignal) - pytest failure: 5264f8cd (weird) - pytest failure: 8823138e (nosignal) - pytest failure: f154f9b3 (ignoredsignal) - pr_time_benchmarks regression: b07725a9 (ghfirst) - pr_time_benchmarks regression: d4d0ede6 (ignoredsignal) - Build error: 2596e3d0 (nosignal) - pytest failure: c1f531f0 (ghfirst) - GHA error: c6b4f986 (nosignal) - GHA error: 529e0357 (nosignal) - GHA error: e694280d (ghfirst) - GHA error: e1180c72 (weird) - GHA error: 7dcc77e4 (nosignal) - GHA error: a3098a74 (ignoredsignal) - GHA error: a14f427d (nosignal) - GHA error: bee9c70c (nosignal) - GHA error: 409c396a (ghfirst) - GHA error: 67fb9b7c (nosignal) - GHA error: 1b50c125 (nosignal) - pytest failure: 196c95d4 (nosignal) ```
1 parent c2c7250 commit 7d5d073

File tree

4 files changed

+276
-24
lines changed

4 files changed

+276
-24
lines changed

aws/lambda/pytorch-auto-revert/.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# ClickHouse Configuration
22
CLICKHOUSE_HOST=your_clickhouse_host
33
CLICKHOUSE_PORT=9000
4-
CLICKHOUSE_USER=default
4+
CLICKHOUSE_USERNAME=default
55
CLICKHOUSE_PASSWORD=your_password
66
CLICKHOUSE_DATABASE=pytorch
77

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# PyTorch Auto Revert
2+
3+
A tool for detecting autorevert patterns in PyTorch CI workflows.
4+
5+
## Installation
6+
7+
1. Navigate to the project directory:
8+
```bash
9+
cd test-infra/aws/lambda/pytorch-auto-revert
10+
```
11+
12+
2. Install dependencies:
13+
```bash
14+
pip install -r requirements.txt
15+
```
16+
17+
## Configuration
18+
19+
Set the following environment variables or pass them as command-line arguments:
20+
21+
### Required:
22+
- **ClickHouse connection:**
23+
- `CLICKHOUSE_HOST` (or `--clickhouse-host`)
24+
- `CLICKHOUSE_USERNAME` (or `--clickhouse-username`)
25+
- `CLICKHOUSE_PASSWORD` (or `--clickhouse-password`)
26+
27+
- **GitHub credentials (one of):**
28+
- `GITHUB_TOKEN` (or `--github-access-token`)
29+
- GitHub App credentials:
30+
- `GITHUB_APP_ID` (or `--github-app-id`)
31+
- `GITHUB_APP_SECRET` (or `--github-app-secret`)
32+
- `GITHUB_INSTALLATION_ID` (or `--github-installation-id`)
33+
34+
### Optional:
35+
- `CLICKHOUSE_PORT` (default: 8443)
36+
- `CLICKHOUSE_DATABASE` (default: default)
37+
- `LOG_LEVEL` (default: INFO)
38+
39+
## Usage
40+
41+
Run the autorevert checker from the project directory:
42+
43+
```bash
44+
python -m pytorch_auto_revert autorevert-checker <workflows> [options]
45+
```
46+
47+
### Parameters:
48+
- `workflows`: One or more workflow names (space separated)
49+
- `--hours`: Lookback window in hours (default: 48)
50+
- `--verbose` or `-v`: Show detailed output including commit summaries
51+
52+
### Examples:
53+
54+
1. **Single workflow with default 48-hour lookback:**
55+
```bash
56+
python -m pytorch_auto_revert autorevert-checker pull
57+
```
58+
59+
2. **Single workflow with custom lookback:**
60+
```bash
61+
python -m pytorch_auto_revert autorevert-checker trunk --hours 72
62+
```
63+
64+
3. **Multiple workflows (space separated):**
65+
```bash
66+
python -m pytorch_auto_revert autorevert-checker pull trunk inductor --hours 24
67+
```
68+
69+
4. **With verbose output:**
70+
```bash
71+
python -m pytorch_auto_revert autorevert-checker pull --hours 48 --verbose
72+
```
73+
74+
5. **With explicit credentials:**
75+
```bash
76+
python -m pytorch_auto_revert autorevert-checker pull \
77+
--clickhouse-host your-host \
78+
--clickhouse-username your-user \
79+
--clickhouse-password your-pass \
80+
--github-access-token your-token
81+
```
82+
83+
## Other Commands
84+
85+
The tool also supports:
86+
- `workflow-restart-checker`: Check for restarted workflows
87+
- `do-restart`: Restart a workflow for a specific commit
88+
89+
Run with `--help` for more information:
90+
```bash
91+
python -m pytorch_auto_revert --help
92+
```

aws/lambda/pytorch-auto-revert/pytorch_auto_revert/autorevert_checker.py

Lines changed: 115 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,9 @@ def detect_autorevert_pattern_workflow(self, workflow_name: str) -> List[Dict]:
258258
for j in suspected_commit1.failed_jobs
259259
}
260260

261-
common_failures = set()
261+
# Map to track newer commits for each failure
262+
failure_to_newer_commit = {}
263+
262264
for (
263265
suspected_failure_class_rule,
264266
suspected_failure_job_name,
@@ -270,25 +272,27 @@ def detect_autorevert_pattern_workflow(self, workflow_name: str) -> List[Dict]:
270272
)
271273
)
272274
if not newer_commit_same_job or not newer_same_jobs:
273-
# No older commit with the same job found
275+
# No newer commit with the same job found
274276
continue
275277

276278
if any(
277279
j.classification_rule == suspected_failure_class_rule
278280
for j in newer_same_jobs
279281
):
280282
# The newer commit has the same job failing
281-
common_failures.add(
282-
(
283-
suspected_failure_class_rule,
284-
suspected_failure_job_name,
285-
)
283+
failure_key = (
284+
suspected_failure_class_rule,
285+
suspected_failure_job_name,
286286
)
287+
failure_to_newer_commit[failure_key] = newer_commit_same_job
287288

288-
if not common_failures:
289+
if not failure_to_newer_commit:
289290
continue
290291

291-
for failure_rule, job_name in common_failures:
292+
for (
293+
failure_rule,
294+
job_name,
295+
), newer_commit in failure_to_newer_commit.items():
292296
last_commit_with_same_job, last_same_jobs = (
293297
self._find_last_commit_with_job(
294298
(commits[j] for j in range(i + 1, len(commits))), job_name
@@ -319,11 +323,11 @@ def detect_autorevert_pattern_workflow(self, workflow_name: str) -> List[Dict]:
319323
"workflow_name": workflow_name,
320324
"failure_rule": failure_rule,
321325
"newer_commits": [
322-
"newer_commit_same_job.head_sha",
326+
newer_commit.head_sha,
323327
suspected_commit1.head_sha,
324328
],
325-
"older_commit": "last_commit_with_same_job.head_sha",
326-
"failed_job_names": list("last_same_job.name"),
329+
"older_commit": last_commit_with_same_job.head_sha,
330+
"failed_job_names": [j.name for j in last_same_jobs],
327331
"older_job_coverage": [],
328332
}
329333
)
@@ -434,3 +438,102 @@ def is_commit_reverted(self, target_commit_sha: str) -> Optional[Dict]:
434438
}
435439

436440
return None # No revert found
441+
442+
def extract_revert_categories_batch(self, messages: List[str]) -> Dict[str, str]:
443+
"""
444+
Extract categories from multiple revert commit messages in a single batch query.
445+
446+
Categories are specified with -c flag like:
447+
- nosignal
448+
- ignoredsignal
449+
- landrace
450+
- weird
451+
- ghfirst
452+
453+
Args:
454+
messages: List of revert commit messages
455+
456+
Returns:
457+
Dict mapping message to category
458+
"""
459+
# Extract all comment IDs
460+
comment_ids = []
461+
message_to_comment_id = {}
462+
463+
for message in messages:
464+
comment_match = re.search(r"#issuecomment-(\d+)", message)
465+
if comment_match:
466+
comment_id = int(comment_match.group(1))
467+
comment_ids.append(comment_id)
468+
message_to_comment_id[message] = comment_id
469+
470+
# Batch query for all comment bodies
471+
comment_id_to_category = {}
472+
if comment_ids:
473+
try:
474+
query = """
475+
SELECT id, body
476+
FROM issue_comment
477+
WHERE id IN {comment_ids:Array(Int64)}
478+
"""
479+
result = CHCliFactory().client.query(
480+
query, parameters={"comment_ids": comment_ids}
481+
)
482+
483+
for row in result.result_rows:
484+
comment_id, body = row
485+
# Look for -c flag in comment body
486+
match = re.search(r"-c\s+(\w+)", body)
487+
if match:
488+
category = match.group(1).lower()
489+
if category in [
490+
"nosignal",
491+
"ignoredsignal",
492+
"landrace",
493+
"weird",
494+
"ghfirst",
495+
]:
496+
comment_id_to_category[comment_id] = category
497+
except Exception:
498+
# If query fails, continue without error
499+
pass
500+
501+
# Map messages to categories
502+
result = {}
503+
for message in messages:
504+
comment_id = message_to_comment_id.get(message)
505+
if comment_id and comment_id in comment_id_to_category:
506+
result[message] = comment_id_to_category[comment_id]
507+
else:
508+
result[message] = "uncategorized"
509+
510+
return result
511+
512+
def get_commits_reverted_with_info(self) -> Dict[str, Dict]:
513+
"""
514+
Get all commits that were reverted with detailed information including categories.
515+
516+
Returns:
517+
Dict mapping commit SHA to revert information with category
518+
"""
519+
reverted_commits = {}
520+
revert_messages = []
521+
522+
# First pass: collect all reverted commits and their messages
523+
for commit in self.commit_history:
524+
revert_info = self.is_commit_reverted(commit["sha"])
525+
if revert_info:
526+
reverted_commits[commit["sha"]] = revert_info
527+
revert_messages.append(revert_info["revert_message"])
528+
529+
# Batch extract categories
530+
if revert_messages:
531+
message_to_category = self.extract_revert_categories_batch(revert_messages)
532+
533+
# Update revert info with categories
534+
for _, info in reverted_commits.items():
535+
info["category"] = message_to_category.get(
536+
info["revert_message"], "uncategorized"
537+
)
538+
539+
return reverted_commits

0 commit comments

Comments
 (0)