@@ -67,13 +67,164 @@ else
6767 echo " ✖ Gradle failed with status $status . Full log at: $LOG "
6868fi
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.
71+ # (The old tail spawned a headless `claude` per build; managed settings pin that
72+ # to sonnet-5 via the AI gateway, billing real money for every log parse.)
7173echo
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."
74+ echo " Generating gradle summary…"
75+ python3 - " $LOG " << 'PYEOF ' || echo "⚠ gradle summary generation failed — full log at: $LOG"
76+ import json, os, re, sys
77+
78+ log_path = sys.argv[1] if len(sys.argv) > 1 else ""
79+ OUT_DIR = os.path.join("build", "reports", "claude")
80+ MD = os.path.join(OUT_DIR, "gradle-summary.md")
81+ JS = os.path.join(OUT_DIR, "gradle-summary.json")
82+
83+ status, total_time, cur_task = "UNKNOWN", None, None
84+ failed_tasks, headlines, dep_issues, test_failures = [], [], [], []
85+ warnings, seen_warnings = [], set()
86+ tests = {"total": 0, "failed": 0, "skipped": 0, "modules": {}}
87+
88+ RE_STATUS = re.compile(r"^BUILD (SUCCESSFUL|FAILED)(?: in (.+?))?\s*$")
89+ RE_TASK = re.compile(r"^> Task (\S+)")
90+ RE_GSUM = re.compile(r"(\d+) tests? (successful|failed|skipped)")
91+ RE_COMP = re.compile(r"(\d+) tests? completed(?:, (\d+) failed)?(?:, (\d+) skipped)?")
92+ RE_GTEST_P = re.compile(r"\[ PASSED \] (\d+) tests?")
93+ RE_GTEST_F = re.compile(r"\[ FAILED \] (\d+) tests?")
94+ RE_DEP = re.compile(r"Could not (?:resolve|find|get)|timed out|status code: 40[13]|artifact .* not found", re.I)
95+ RE_WARN = re.compile(r"\bw: |warning:|deprecat", re.I)
96+
97+ def mod(name):
98+ return tests["modules"].setdefault(name, {"total": 0, "failed": 0, "skipped": 0})
99+
100+ def add_counts(name, ok, failed, skipped):
101+ m = mod(name)
102+ m["total"] += ok + failed + skipped
103+ m["failed"] += failed
104+ m["skipped"] += skipped
105+ tests["total"] += ok + failed + skipped
106+ tests["failed"] += failed
107+ tests["skipped"] += skipped
108+
109+ if not log_path or not os.path.isfile(log_path) or os.path.getsize(log_path) == 0:
110+ why = f"log not found: {log_path!r}" if not log_path or not os.path.isfile(log_path) else "log is empty"
111+ os.makedirs(OUT_DIR, exist_ok=True)
112+ open(MD, "w").write(f"# Gradle Summary\n\n- Status: UNKNOWN ({why})\n\nFull log unavailable or empty.\n")
113+ json.dump({"status": "UNKNOWN", "totalTime": None, "failedTasks": [], "warnings": [],
114+ "tests": tests, "slowTasks": [], "depIssues": [why], "actions": []}, open(JS, "w"), indent=1)
115+ print(f"Gradle log unusable ({why}); wrote {MD} and {JS}")
116+ sys.exit(0)
117+
118+ grab_headline = False
119+ with open(log_path, encoding="utf-8", errors="replace") as f:
120+ for line in f:
121+ line = line.rstrip("\n")
122+ m = RE_STATUS.match(line)
123+ if m:
124+ status = m.group(1)
125+ total_time = m.group(2)
126+ continue
127+ m = RE_TASK.match(line)
128+ if m:
129+ cur_task = m.group(1)
130+ if line.endswith("FAILED") and m.group(1) not in failed_tasks:
131+ failed_tasks.append(m.group(1))
132+ continue
133+ if "FAILURE: Build failed with an exception" in line:
134+ grab_headline = True
135+ continue
136+ if grab_headline and line.strip().startswith("* What went wrong"):
137+ headlines.append("")
138+ continue
139+ if grab_headline:
140+ s = line.strip()
141+ if s.startswith("*") or not s:
142+ if headlines and headlines[-1]:
143+ grab_headline = False
144+ continue
145+ if len(headlines[-1]) < 200:
146+ headlines[-1] += (" " if headlines[-1] else "") + s
147+ continue
148+ m = RE_COMP.search(line)
149+ if m:
150+ total = int(m.group(1))
151+ failed = int(m.group(2) or 0)
152+ skipped = int(m.group(3) or 0)
153+ add_counts(cur_task or "(unknown)", total - failed - skipped, failed, skipped)
154+ continue
155+ m = RE_GSUM.search(line)
156+ if m:
157+ n, verb = int(m.group(1)), m.group(2)
158+ mm = mod(cur_task or "(unknown)")
159+ mm["total"] += n
160+ tests["total"] += n
161+ if verb == "failed":
162+ mm["failed"] += n
163+ tests["failed"] += n
164+ elif verb == "skipped":
165+ mm["skipped"] += n
166+ tests["skipped"] += n
167+ continue
168+ m = RE_GTEST_P.search(line)
169+ if m:
170+ add_counts(cur_task or "(gtest)", int(m.group(1)), 0, 0)
171+ continue
172+ m = RE_GTEST_F.search(line)
173+ if m:
174+ add_counts(cur_task or "(gtest)", 0, int(m.group(1)), 0)
175+ continue
176+ if re.search(r" > \S+(?:\.\S+)+.* FAILED$", line) or re.search(r"\[ FAILED \] (\S+)", line):
177+ test_failures.append(line.strip())
178+ continue
179+ if RE_DEP.search(line) and len(dep_issues) < 20:
180+ if line not in dep_issues:
181+ dep_issues.append(line.strip())
182+ continue
183+ wm = RE_WARN.search(line)
184+ if wm and len(warnings) < 40:
185+ key = line.strip()[:160]
186+ if key not in seen_warnings:
187+ seen_warnings.add(key)
188+ warnings.append(key)
189+
190+ failed = [t + (" — " + h if h else "") for t, h in zip(failed_tasks, headlines + [""] * len(failed_tasks))]
191+ data = {"status": status, "totalTime": total_time, "failedTasks": failed, "warnings": warnings,
192+ "tests": tests, "slowTasks": [], "depIssues": dep_issues, "actions": []}
193+
194+ os.makedirs(OUT_DIR, exist_ok=True)
195+ with open(JS, "w") as f:
196+ json.dump(data, f, indent=1)
197+
198+ L = ["# Gradle Summary\n", f"- Status: {status}", f"- Total time: {total_time or 'UNKNOWN'}",
199+ f"- Log: {log_path}\n"]
200+ if failed:
201+ L.append("## Failing Tasks")
202+ L += [f"- {t}" for t in failed]
203+ L.append("")
204+ if headlines:
205+ L.append("## Primary Failure")
206+ L += [f"```text\n{h}\n```" for h in headlines[:3] if h]
207+ L.append("")
208+ if tests["total"]:
209+ L.append("## Tests")
210+ L.append(f"- Total: {tests['total']}, failed: {tests['failed']}, skipped: {tests['skipped']}")
211+ for name, m in tests["modules"].items():
212+ L.append(f" - {name}: {m['total']} total, {m['failed']} failed, {m['skipped']} skipped")
213+ if test_failures:
214+ L.append("- Top failing tests:")
215+ L += [f" - {t}" for t in test_failures[:10]]
216+ L.append("")
217+ if warnings:
218+ L.append("## Warnings")
219+ L += [f"- {w}" for w in warnings[:20]]
220+ L.append("")
221+ if dep_issues:
222+ L.append("## Dependency / Network Issues")
223+ L += [f"- {d}" for d in dep_issues[:10]]
224+ L.append("")
225+ L.append("- Per-task durations are not present in plain `-i` console logs; slowTasks omitted.")
226+ open(MD, "w").write("\n".join(L) + "\n")
227+
228+ print(f"{status} (time: {total_time or 'n/a'}); {len(failed)} failing task(s), {tests['failed']} test failure(s)")
229+ print(f"Wrote {MD} and {JS}")
230+ PYEOF
0 commit comments