Skip to content

Commit e0bcf58

Browse files
committed
ENH: Add fix-nightly-warnings skill and CDash query scripts
1 parent add7004 commit e0bcf58

File tree

3 files changed

+524
-0
lines changed

3 files changed

+524
-0
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
---
2+
name: fix-nightly-warnings
3+
description: 'Fix ITK nightly build errors or compilation warnings reported on CDash. Use when: addressing CDash nightly failures. Creates a branch, fixes warnings, and opens a PR upstream.'
4+
argument-hint: What warnings should this skill fix?
5+
---
6+
7+
# Fix ITK Nightly Build Errors and Warnings
8+
9+
Creates a focused branch containing fixes for errors or warnings reported on the ITK CDash nightly dashboard, then opens a PR upstream.
10+
11+
## When to Use
12+
13+
- CDash nightly build reports new errors, warnings, or Doxygen warnings
14+
- User says "fix nightly errors", "address CDash warnings", or "there are new Doxygen warnings"
15+
16+
## Available Scripts
17+
18+
- **`scripts/list_nightly_warnings.py`** — Lists CDash builds that have warnings or errors. Defaults to `Nightly` builds from the last 24 hours.
19+
- **`scripts/get_build_warnings.py`** — Fetches and summarizes warnings (or errors) for a specific CDash build ID, grouped by source file and warning flag.
20+
21+
Run `python3 scripts/<script>.py --help` for full usage.
22+
23+
## Procedure
24+
25+
### 1. Identify the Warnings
26+
27+
Use the provided scripts to fetch the current nightly builds and their warnings from CDash.
28+
29+
**Step 1a — List nightly builds with warnings:**
30+
31+
```bash
32+
python3 scripts/list_nightly_warnings.py --type Nightly -limit 25 --json | jq '.[] | select(.warnings > 0)'
33+
```
34+
35+
Note: `list_nightly_warnings.py` returns the builds with the most errors then warnings.
36+
37+
38+
**Step 1b — Inspect warnings for a specific build:**
39+
40+
```bash
41+
python3 scripts/get_build_warnings.py --limit 200 --json BUILD_ID | jq 'group_by(.flag) | .[] | {flag: .[0].flag, count: length}'
42+
```
43+
44+
---
45+
46+
For each build with errors and warnings, fetch the details and summarize the errors and warnings by type and source file.
47+
IGNORE ALL errors and warnings originating from `Modules/ThirdParty/` paths.
48+
49+
50+
If there are build errors, only fix those. If there are warnings, prioritize fixing the most common warning flag that affects the most files.
51+
52+
53+
### 2. Analyze the Root Cause
54+
55+
For each warning or error type identified in step 1, determine the root cause before editing files:
56+
- Look up the compiler flag (e.g. `-Wthread-safety-negative`) in the compiler documentation.
57+
- Read the affected source files to understand how they are structured.
58+
- Identify the minimal fix: a missing annotation, a suppression pragma, a corrected API usage, etc.
59+
- Confirm that warnings from `Modules/ThirdParty/` are skipped entirely.
60+
61+
### 3. Create a New Branch
62+
63+
```bash
64+
git fetch upstream
65+
git checkout -b fix-<warning-type>-warnings upstream/main
66+
```
67+
68+
Example: `fix-doxygen-group-warnings`
69+
70+
### 4. Fix the Source Files
71+
72+
Determine the root cause of each error or warning. Apply the necessary fixes to the affected files to resolve the warnings. Make the minimal changes needed to fix the warnings, avoid changing unrelated documentation, coding or formatting.
73+
74+
### 5. Verify No New Warnings Introduced
75+
76+
Build and test to confirm that the fixes removed the targeted errors and warnings and did not introduce new ones.
77+
78+
### 6. Commit the Changes
79+
80+
Follow the ITK commit message standards. Include a clear description of the fix and include the error or warning message being addressed.
81+
82+
### 7. Draft a Pull Request
83+
84+
Do the following:
85+
- Draft a pull request description that includes a summary of the changes, the warnings or errors fixed, and reference the CDash build if applicable.
86+
- Request the User to review and approve the description before submitting the PR.
87+
- Push the branch to the user's remote
88+
- Create a DRAFT pull request against the current `upstream/main` branch.
89+
90+
## Quality Checks
91+
92+
Before declaring done:
93+
[] All targeted warnings are fixed
94+
[] No new warnings or errors introduced
95+
[] Changes are limited to the files affected by the warnings
96+
[] Commit message clearly describes the fix and references the CDash issue if applicable
97+
98+
## Key Files for Reference
99+
100+
| File | Purpose |
101+
|------|---------|
102+
| `Documentation/docs/contributing/index.md` | Contributing guidelines |
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
#!/usr/bin/env python3
2+
# /// script
3+
# requires-python = ">=3.8"
4+
# ///
5+
"""
6+
get_build_warnings.py — Fetch and summarize warnings or errors for a CDash build.
7+
8+
Queries the CDash GraphQL API for all warning (or error) entries associated with
9+
a specific build ID, then groups them by source file and warning flag.
10+
11+
Exit codes:
12+
0 Success
13+
1 Argument error
14+
2 Network or API error
15+
"""
16+
17+
import argparse
18+
import json
19+
import re
20+
import sys
21+
import urllib.error
22+
import urllib.request
23+
24+
CDASH_GRAPHQL = "https://open.cdash.org/graphql"
25+
26+
QUERY_TEMPLATE = """
27+
{
28+
build(id: "%s") {
29+
name stamp startTime buildWarningsCount buildErrorsCount
30+
site { name }
31+
buildErrors(filters: { eq: { type: %s } }, first: %d%s) {
32+
pageInfo { hasNextPage endCursor }
33+
edges {
34+
node { sourceFile sourceLine stdError stdOutput }
35+
}
36+
}
37+
}
38+
}
39+
"""
40+
41+
WARNING_FLAG_RE = re.compile(r'\[(-W[^\]]+)\]')
42+
43+
44+
def graphql(query: str) -> dict:
45+
payload = json.dumps({"query": query}).encode()
46+
req = urllib.request.Request(
47+
CDASH_GRAPHQL,
48+
data=payload,
49+
headers={"Content-Type": "application/json"},
50+
)
51+
try:
52+
with urllib.request.urlopen(req, timeout=30) as r:
53+
return json.loads(r.read())
54+
except urllib.error.URLError as e:
55+
print(f"Error: network request failed: {e}", file=sys.stderr)
56+
sys.exit(2)
57+
58+
59+
def extract_flag(text: str) -> str:
60+
m = WARNING_FLAG_RE.search(text)
61+
return m.group(1) if m else "?"
62+
63+
64+
def fetch_entries(build_id: str, error_type: str, limit: int) -> tuple[dict, list]:
65+
"""Fetch all entries up to limit, following pagination if needed."""
66+
all_entries = []
67+
cursor = None
68+
build_meta = None
69+
70+
while True:
71+
after_clause = f', after: "{cursor}"' if cursor else ""
72+
query = QUERY_TEMPLATE % (build_id, error_type, min(limit, 200), after_clause)
73+
data = graphql(query)
74+
75+
if "errors" in data:
76+
print(f"Error: GraphQL returned errors: {data['errors']}", file=sys.stderr)
77+
sys.exit(2)
78+
79+
build = data["data"]["build"]
80+
if build_meta is None:
81+
build_meta = {
82+
"name": build["name"],
83+
"stamp": build["stamp"],
84+
"site": build["site"]["name"],
85+
"buildWarningsCount": build["buildWarningsCount"],
86+
"buildErrorsCount": build["buildErrorsCount"],
87+
}
88+
89+
page = build["buildErrors"]
90+
all_entries.extend(e["node"] for e in page["edges"])
91+
92+
if not page["pageInfo"]["hasNextPage"] or len(all_entries) >= limit:
93+
break
94+
cursor = page["pageInfo"]["endCursor"]
95+
96+
return build_meta, all_entries[:limit]
97+
98+
99+
def main() -> None:
100+
parser = argparse.ArgumentParser(
101+
prog="get_build_warnings.py",
102+
description="Fetch and summarize warnings or errors for a CDash build.",
103+
epilog=(
104+
"Examples:\n"
105+
" python3 scripts/get_build_warnings.py 11107692\n"
106+
" python3 scripts/get_build_warnings.py 11107692 --raw\n"
107+
" python3 scripts/get_build_warnings.py 11107692 --errors\n"
108+
" python3 scripts/get_build_warnings.py 11107692 --json | jq '.entries[] | select(.flag == \"-Wthread-safety-negative\")'\n"
109+
" python3 scripts/get_build_warnings.py 11107692 --limit 500"
110+
),
111+
formatter_class=argparse.RawDescriptionHelpFormatter,
112+
)
113+
parser.add_argument(
114+
"build_id",
115+
metavar="BUILD_ID",
116+
help="CDash build ID (integer from list_nightly_warnings.py output or CDash URL)",
117+
)
118+
parser.add_argument(
119+
"--errors",
120+
action="store_true",
121+
help="Fetch build errors instead of warnings (default: warnings)",
122+
)
123+
parser.add_argument(
124+
"--raw",
125+
action="store_true",
126+
help="Print one entry per line with file:line, flag, and message snippet instead of grouping",
127+
)
128+
parser.add_argument(
129+
"--json",
130+
action="store_true",
131+
dest="json_output",
132+
help="Output as JSON: {build: {...}, entries: [...]} for programmatic use",
133+
)
134+
parser.add_argument(
135+
"--limit",
136+
type=int,
137+
default=200,
138+
metavar="N",
139+
help="Maximum number of entries to retrieve (default: 200)",
140+
)
141+
args = parser.parse_args()
142+
143+
error_type = "ERROR" if args.errors else "WARNING"
144+
label = "error" if args.errors else "warning"
145+
146+
build_meta, entries = fetch_entries(args.build_id, error_type, args.limit)
147+
total = build_meta["buildErrorsCount" if args.errors else "buildWarningsCount"] or 0
148+
149+
if args.json_output:
150+
output = {
151+
"build": build_meta,
152+
"type": error_type,
153+
"totalCount": total,
154+
"fetchedCount": len(entries),
155+
"entries": [
156+
{
157+
"sourceFile": n["sourceFile"],
158+
"sourceLine": n["sourceLine"],
159+
"flag": extract_flag(n["stdError"] or n["stdOutput"] or ""),
160+
"stdError": n["stdError"],
161+
"stdOutput": n["stdOutput"],
162+
}
163+
for n in entries
164+
],
165+
}
166+
print(json.dumps(output, indent=2))
167+
return
168+
169+
# Diagnostics to stderr so stdout stays parseable
170+
print(f"Build: {build_meta['name']}", file=sys.stderr)
171+
print(f"Site: {build_meta['site']}", file=sys.stderr)
172+
print(f"Stamp: {build_meta['stamp']}", file=sys.stderr)
173+
print(f"Total {label}s: {total} (fetched: {len(entries)})", file=sys.stderr)
174+
if total > len(entries):
175+
print(
176+
f"Note: {total - len(entries)} entries not fetched; use --limit to increase.",
177+
file=sys.stderr,
178+
)
179+
print(file=sys.stderr)
180+
181+
if args.raw:
182+
print(f"{'FILE:LINE':<60} {'FLAG':<35} SNIPPET")
183+
print("-" * 120)
184+
for n in entries:
185+
loc = f"{n['sourceFile']}:{n['sourceLine']}"
186+
text = n["stdError"] or n["stdOutput"] or ""
187+
flag = extract_flag(text)
188+
# Extract a short message snippet after the flag
189+
idx = text.find(flag)
190+
if idx >= 0:
191+
snippet = text[idx + len(flag):].strip().replace("\n", " ")[:80]
192+
else:
193+
snippet = text.replace("\n", " ")[:80]
194+
print(f"{loc:<60} {flag:<35} {snippet}")
195+
else:
196+
# Group by (sourceFile, flag)
197+
groups: dict = {}
198+
for n in entries:
199+
src = n["sourceFile"] or "<unknown>"
200+
text = n["stdError"] or n["stdOutput"] or ""
201+
flag = extract_flag(text)
202+
key = (src, flag)
203+
groups[key] = groups.get(key, 0) + 1
204+
205+
print(f"{'COUNT':>6} {'FLAG':<35} SOURCE FILE")
206+
print("-" * 100)
207+
for (src, flag), count in sorted(groups.items(), key=lambda x: (-x[1], x[0][0])):
208+
print(f"{count:>6} {flag:<35} {src}")
209+
210+
211+
if __name__ == "__main__":
212+
main()

0 commit comments

Comments
 (0)