Skip to content

Commit 0fe6999

Browse files
committed
Merge branch 'develop' into ji/remove-cast-compute
2 parents 2c7610d + 3cc1b6a commit 0fe6999

146 files changed

Lines changed: 4228 additions & 1185 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/scripts/fuzz_report/cli.py

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import argparse
55
import json
66
import os
7+
import re
78
import subprocess
89
import sys
910
from pathlib import Path
@@ -14,6 +15,12 @@
1415

1516
TEMPLATES_DIR = Path(__file__).parent / "templates"
1617

18+
# Marker used to find/update the single recurrence-tracking comment.
19+
_RECURRENCE_MARKER = "<!-- fuzzer-recurrence-tracker -->"
20+
_RECURRENCE_COUNT_RE = r"<!-- fuzzer-recurrence-tracker count:(\d+) -->"
21+
# Variables that must be set (non-empty) before creating or commenting on an issue.
22+
REQUIRED_REPORT_VARIABLES = ["FUZZ_TARGET", "CRASH_FILE", "ARTIFACT_URL"]
23+
1724

1825
def parse_var_arg(var_str: str) -> tuple[str, str]:
1926
"""Parse a -v KEY=VALUE argument into (key, value)."""
@@ -80,7 +87,14 @@ def _build_template_variables(
8087
def _determine_action(
8188
dedup_path: str | Path | None,
8289
) -> tuple[str, dict | None]:
83-
"""Determine action from dedup result. Returns (action, dedup_dict)."""
90+
"""Determine action from dedup result. Returns (action, dedup_dict).
91+
92+
Actions:
93+
create – new issue
94+
skip – exact duplicate, do nothing
95+
update_count – high-confidence duplicate, bump recurrence counter
96+
comment – medium-confidence duplicate, post full comment
97+
"""
8498
if not dedup_path or not Path(dedup_path).exists():
8599
return "create", None
86100

@@ -91,9 +105,90 @@ def _determine_action(
91105
if dedup.get("confidence") == "exact":
92106
return "skip", dedup
93107

108+
if dedup.get("confidence") == "high":
109+
return "update_count", dedup
110+
94111
return "comment", dedup
95112

96113

114+
def _render_recurrence_body(count: int) -> str:
115+
"""Render the minimal recurrence-tracking comment body."""
116+
return (
117+
f"Seen **{count}** time{'s' if count != 1 else ''}\n\n"
118+
f"<!-- fuzzer-recurrence-tracker count:{count} -->"
119+
)
120+
121+
122+
def _update_recurrence_count(repo: str, issue_number: int | str) -> int:
123+
"""Find-or-create the recurrence comment, incrementing its count.
124+
125+
Uses a compare-and-swap pattern: reads the current count from the
126+
existing comment (if any), increments it, and writes back.
127+
128+
Returns the new count.
129+
"""
130+
# List all comments on the issue
131+
result = subprocess.run(
132+
[
133+
"gh",
134+
"api",
135+
f"repos/{repo}/issues/{issue_number}/comments",
136+
"--paginate",
137+
"--jq",
138+
f'.[] | select(.body | contains("{_RECURRENCE_MARKER}")) | {{id: .id, body: .body}}',
139+
],
140+
capture_output=True,
141+
text=True,
142+
check=True,
143+
)
144+
145+
existing_id = None
146+
current_count = 0
147+
148+
for line in result.stdout.strip().splitlines():
149+
if not line:
150+
continue
151+
comment = json.loads(line)
152+
existing_id = comment["id"]
153+
m = re.search(_RECURRENCE_COUNT_RE, comment["body"])
154+
if m:
155+
current_count = int(m.group(1))
156+
break
157+
158+
new_count = current_count + 1
159+
body = _render_recurrence_body(new_count)
160+
161+
if existing_id:
162+
# Update existing comment (not atomic — race is acceptable since
163+
# fuzz CI jobs are serialized)
164+
subprocess.run(
165+
[
166+
"gh",
167+
"api",
168+
f"repos/{repo}/issues/comments/{existing_id}",
169+
"-X",
170+
"PATCH",
171+
"-f",
172+
f"body={body}",
173+
],
174+
check=True,
175+
)
176+
else:
177+
# Create new recurrence comment
178+
subprocess.run(
179+
[
180+
"gh",
181+
"api",
182+
f"repos/{repo}/issues/{issue_number}/comments",
183+
"-f",
184+
f"body={body}",
185+
],
186+
check=True,
187+
)
188+
189+
return new_count
190+
191+
97192
def cmd_extract(args: argparse.Namespace) -> int:
98193
"""Extract crash info from log file."""
99194
if not Path(args.log_file).exists():
@@ -142,6 +237,16 @@ def cmd_check_duplicate(args: argparse.Namespace) -> int:
142237
return 0
143238

144239

240+
def _validate_required_variables(variables: dict[str, str]) -> list[str]:
241+
"""Return the names of any required variables that are missing or empty."""
242+
missing = []
243+
for name in REQUIRED_REPORT_VARIABLES:
244+
val = variables.get(name, "")
245+
if not val or val == "(not set)":
246+
missing.append(name)
247+
return missing
248+
249+
145250
def cmd_report(args: argparse.Namespace) -> int:
146251
"""Create or comment on a GitHub issue based on crash + dedup results."""
147252
if not Path(args.crash_info).exists():
@@ -163,11 +268,32 @@ def cmd_report(args: argparse.Namespace) -> int:
163268
variables = _build_template_variables(crash_info, args.var, claude_analysis)
164269
existing_issue = dedup.get("issue_number") if dedup else None
165270

271+
# Validate required variables before creating/commenting (skip is fine without them)
272+
if action != "skip":
273+
missing = _validate_required_variables(variables)
274+
if missing:
275+
print(
276+
f"Error: Required variables not set: {', '.join(missing)}",
277+
file=sys.stderr,
278+
)
279+
_write_github_output("validation_failed", "true")
280+
_write_github_output("missing_variables", ", ".join(missing))
281+
return 1
282+
166283
if action == "skip":
167284
print(f"Exact duplicate of #{existing_issue}, skipping.", file=sys.stderr)
168285
_write_github_output("issue_number", str(existing_issue))
169286
return 0
170287

288+
if action == "update_count":
289+
new_count = _update_recurrence_count(args.repo, existing_issue)
290+
print(
291+
f"Updated recurrence count on #{existing_issue} to {new_count}",
292+
file=sys.stderr,
293+
)
294+
_write_github_output("issue_number", str(existing_issue))
295+
return 0
296+
171297
if action == "comment":
172298
variables.setdefault("DEDUP_REASON", dedup.get("reason", ""))
173299
variables.setdefault("DEDUP_CONFIDENCE", dedup.get("confidence", ""))
@@ -245,6 +371,7 @@ def cmd_dry_run(args: argparse.Namespace) -> int:
245371
print(f" panic_message: {crash_info.panic_message}", file=sys.stderr)
246372
print(f" crash_type: {crash_info.crash_type}", file=sys.stderr)
247373
print(f" seed_hash: {crash_info.seed_hash}", file=sys.stderr)
374+
print(f" stack_frames: {crash_info.stack_frames[:5]}", file=sys.stderr)
248375
print(file=sys.stderr)
249376

250377
# Step 2: Dedup (if issues file provided)
@@ -261,6 +388,8 @@ def cmd_dry_run(args: argparse.Namespace) -> int:
261388
print(f" confidence: {dedup_result.confidence}", file=sys.stderr)
262389
print(f" issue: #{dedup_result.issue_number}", file=sys.stderr)
263390
print(f" reason: {dedup_result.reason}", file=sys.stderr)
391+
if dedup_result.debug:
392+
print(f" debug: {json.dumps(dedup_result.debug, indent=4)}", file=sys.stderr)
264393
print(file=sys.stderr)
265394

266395
# Write dedup to temp file so _determine_action can read it
@@ -279,6 +408,15 @@ def cmd_dry_run(args: argparse.Namespace) -> int:
279408
variables = _build_template_variables(crash_info, args.var, claude_analysis)
280409
existing_issue = dedup.get("issue_number") if dedup else None
281410

411+
# Validate required variables (same check as real report)
412+
if action != "skip":
413+
missing = _validate_required_variables(variables)
414+
if missing:
415+
print(
416+
f"Warning: Required variables not set: {', '.join(missing)}",
417+
file=sys.stderr,
418+
)
419+
282420
print(f"=== Action: {action.upper()} ===", file=sys.stderr)
283421

284422
if action == "skip":
@@ -288,6 +426,15 @@ def cmd_dry_run(args: argparse.Namespace) -> int:
288426
)
289427
return 0
290428

429+
if action == "update_count":
430+
print(
431+
f"(would update recurrence count on #{existing_issue})",
432+
file=sys.stderr,
433+
)
434+
print(file=sys.stderr)
435+
print(_render_recurrence_body(1))
436+
return 0
437+
291438
if action == "comment":
292439
template_path = TEMPLATES_DIR / "related_comment.md"
293440
variables.setdefault("DEDUP_REASON", dedup.get("reason", ""))

0 commit comments

Comments
 (0)