FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Switch build-and-summarize to deterministic log parsing, fix parsing … · DataDog/java-profiler@dc33515 · GitHub

Repository navigation

Commit dc33515

Browse files
committed
Switch build-and-summarize to deterministic log parsing, fix parsing bugs
Replaces the headless claude-agent spawn with a self-contained Python parser, and fixes empty-list indexing, parallel-build task misattribution, and positional headline/task pairing in that parser.
1 parent 6f9cf99 commit dc33515

1 file changed

Lines changed: 172 additions & 9 deletions

File tree

‎.claude/commands/build-and-summarize‎

Lines changed: 172 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,176 @@ else
6767
echo "✖ Gradle failed with status $status. Full log at: $LOG"
6868
fi
6969

70-
# Hand over to your logs analyst agent — keep the main session output tiny.
70+
# Generate the summary artifacts deterministically — no LLM, no claude spawn.
7171
echo
72-
echo "Delegating to gradle-logs-analyst agent…"
73-
# If your CLI supports non-streaming, set it here to avoid verbose output.
74-
# Example (uncomment if supported): export CLAUDE_NO_STREAM=1
75-
76-
# Sub-agents would inherit the full parent env, including CLAUDECODE, which
77-
# apparently causes problems with some versions of Claude (e.g. 4.6).
78-
unset CLAUDECODE
79-
claude "Act as the gradle-logs-analyst agent to parse the build log at: $LOG. Generate the required Gradle summary artifacts as specified in the gradle-logs-analyst agent definition."
72+
echo "Generating gradle summary…"
73+
python3 - "$LOG" <<'PYEOF' || echo "⚠ gradle summary generation failed — full log at: $LOG"
74+
import json, os, re, sys
75+
76+
log_path = sys.argv[1] if len(sys.argv) > 1 else ""
77+
OUT_DIR = os.path.join("build", "reports", "claude")
78+
MD = os.path.join(OUT_DIR, "gradle-summary.md")
79+
JS = os.path.join(OUT_DIR, "gradle-summary.json")
80+
81+
status, total_time, cur_task, cur_test_task = "UNKNOWN", None, None, None
82+
failed_tasks, headlines, dep_issues, test_failures = [], [], [], []
83+
warnings, seen_warnings = [], set()
84+
tests = {"total": 0, "failed": 0, "skipped": 0, "modules": {}}
85+
86+
RE_STATUS = re.compile(r"^BUILD (SUCCESSFUL|FAILED)(?: in (.+?))?\s*$")
87+
RE_TASK = re.compile(r"^> Task (\S+)")
88+
RE_TEST_TASK = re.compile(r"(?i)test|gtest")
89+
RE_GSUM = re.compile(r"(\d+) tests? (successful|failed|skipped)")
90+
RE_COMP = re.compile(r"(\d+) tests? completed(?:, (\d+) failed)?(?:, (\d+) skipped)?")
91+
RE_GTEST_P = re.compile(r"\[ PASSED \] (\d+) tests?")
92+
RE_GTEST_F = re.compile(r"\[ FAILED \] (\d+) tests?")
93+
RE_DEP = re.compile(r"Could not (?:resolve|find|get)|timed out|status code: 40[13]|artifact .* not found", re.I)
94+
RE_WARN = re.compile(r"\bw: |warning:|deprecat", re.I)
95+
RE_FAILURE = re.compile(r"^FAILURE: Build (failed with an exception|completed with \d+ failures)")
96+
RE_TASK_IN_MSG = re.compile(r"task '(:[^']+)'")
97+
98+
def mod(name):
99+
return tests["modules"].setdefault(name, {"total": 0, "failed": 0, "skipped": 0})
100+
101+
def add_counts(name, ok, failed, skipped):
102+
m = mod(name)
103+
m["total"] += ok + failed + skipped
104+
m["failed"] += failed
105+
m["skipped"] += skipped
106+
tests["total"] += ok + failed + skipped
107+
tests["failed"] += failed
108+
tests["skipped"] += skipped
109+
110+
if not log_path or not os.path.isfile(log_path) or os.path.getsize(log_path) == 0:
111+
why = f"log not found: {log_path!r}" if not log_path or not os.path.isfile(log_path) else "log is empty"
112+
os.makedirs(OUT_DIR, exist_ok=True)
113+
open(MD, "w").write(f"# Gradle Summary\n\n- Status: UNKNOWN ({why})\n\nFull log unavailable or empty.\n")
114+
json.dump({"status": "UNKNOWN", "totalTime": None, "failedTasks": [], "warnings": [],
115+
"tests": tests, "slowTasks": [], "depIssues": [why], "actions": []}, open(JS, "w"), indent=1)
116+
print(f"Gradle log unusable ({why}); wrote {MD} and {JS}")
117+
sys.exit(0)
118+
119+
grab_headline = False
120+
collecting_headline = False
121+
with open(log_path, encoding="utf-8", errors="replace") as f:
122+
for line in f:
123+
line = line.rstrip("\n")
124+
m = RE_STATUS.match(line)
125+
if m:
126+
status = m.group(1)
127+
total_time = m.group(2)
128+
continue
129+
m = RE_TASK.match(line)
130+
if m:
131+
cur_task = m.group(1)
132+
if RE_TEST_TASK.search(cur_task):
133+
cur_test_task = cur_task
134+
if line.endswith("FAILED") and m.group(1) not in failed_tasks:
135+
failed_tasks.append(m.group(1))
136+
continue
137+
if RE_FAILURE.search(line):
138+
grab_headline = True
139+
continue
140+
if grab_headline and line.strip().startswith("* What went wrong"):
141+
headlines.append("")
142+
collecting_headline = True
143+
continue
144+
if grab_headline and collecting_headline:
145+
s = line.strip()
146+
if s.startswith("*") or not s:
147+
collecting_headline = False
148+
continue
149+
if headlines and len(headlines[-1]) < 200:
150+
headlines[-1] += (" " if headlines[-1] else "") + s
151+
continue
152+
m = RE_COMP.search(line)
153+
if m:
154+
total = int(m.group(1))
155+
failed = int(m.group(2) or 0)
156+
skipped = int(m.group(3) or 0)
157+
add_counts(cur_test_task or cur_task or "(unknown)", total - failed - skipped, failed, skipped)
158+
continue
159+
m = RE_GSUM.search(line)
160+
if m:
161+
n, verb = int(m.group(1)), m.group(2)
162+
mm = mod(cur_test_task or cur_task or "(unknown)")
163+
mm["total"] += n
164+
tests["total"] += n
165+
if verb == "failed":
166+
mm["failed"] += n
167+
tests["failed"] += n
168+
elif verb == "skipped":
169+
mm["skipped"] += n
170+
tests["skipped"] += n
171+
continue
172+
m = RE_GTEST_P.search(line)
173+
if m:
174+
add_counts(cur_test_task or cur_task or "(gtest)", int(m.group(1)), 0, 0)
175+
continue
176+
m = RE_GTEST_F.search(line)
177+
if m:
178+
add_counts(cur_test_task or cur_task or "(gtest)", 0, int(m.group(1)), 0)
179+
continue
180+
if re.search(r" > \S+(?:\.\S+)+.* FAILED$", line) or re.search(r"\[ FAILED \] (\S+)", line):
181+
test_failures.append(line.strip())
182+
continue
183+
if RE_DEP.search(line) and len(dep_issues) < 20:
184+
if line not in dep_issues:
185+
dep_issues.append(line.strip())
186+
continue
187+
wm = RE_WARN.search(line)
188+
if wm and len(warnings) < 40:
189+
key = line.strip()[:160]
190+
if key not in seen_warnings:
191+
seen_warnings.add(key)
192+
warnings.append(key)
193+
194+
headline_by_task = {}
195+
for h in headlines:
196+
tm = RE_TASK_IN_MSG.search(h)
197+
if tm and tm.group(1) in failed_tasks:
198+
headline_by_task[tm.group(1)] = h
199+
# Fallback for single-failure builds where the headline never names its task explicitly.
200+
if not headline_by_task and len(failed_tasks) == 1 and headlines:
201+
headline_by_task[failed_tasks[0]] = headlines[0]
202+
failed = [t + (" — " + headline_by_task[t] if t in headline_by_task else "") for t in failed_tasks]
203+
data = {"status": status, "totalTime": total_time, "failedTasks": failed, "warnings": warnings,
204+
"tests": tests, "slowTasks": [], "depIssues": dep_issues, "actions": []}
205+
206+
os.makedirs(OUT_DIR, exist_ok=True)
207+
with open(JS, "w") as f:
208+
json.dump(data, f, indent=1)
209+
210+
L = ["# Gradle Summary\n", f"- Status: {status}", f"- Total time: {total_time or 'UNKNOWN'}",
211+
f"- Log: {log_path}\n"]
212+
if failed:
213+
L.append("## Failing Tasks")
214+
L += [f"- {t}" for t in failed]
215+
L.append("")
216+
if headlines:
217+
L.append("## Primary Failure")
218+
L += [f"```text\n{h}\n```" for h in headlines[:3] if h]
219+
L.append("")
220+
if tests["total"]:
221+
L.append("## Tests")
222+
L.append(f"- Total: {tests['total']}, failed: {tests['failed']}, skipped: {tests['skipped']}")
223+
for name, m in tests["modules"].items():
224+
L.append(f" - {name}: {m['total']} total, {m['failed']} failed, {m['skipped']} skipped")
225+
if test_failures:
226+
L.append("- Top failing tests:")
227+
L += [f" - {t}" for t in test_failures[:10]]
228+
L.append("")
229+
if warnings:
230+
L.append("## Warnings")
231+
L += [f"- {w}" for w in warnings[:20]]
232+
L.append("")
233+
if dep_issues:
234+
L.append("## Dependency / Network Issues")
235+
L += [f"- {d}" for d in dep_issues[:10]]
236+
L.append("")
237+
L.append("- Per-task durations are not present in plain `-i` console logs; slowTasks omitted.")
238+
open(MD, "w").write("\n".join(L) + "\n")
239+
240+
print(f"{status} (time: {total_time or 'n/a'}); {len(failed)} failing task(s), {tests['failed']} test failure(s)")
241+
print(f"Wrote {MD} and {JS}")
242+
PYEOF

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL