Skip to content

Commit 82999be

Browse files
committed
Run review without checking out PR
1 parent 6a07814 commit 82999be

4 files changed

Lines changed: 284 additions & 85 deletions

File tree

.github/scripts/pr-triage/common.py

Lines changed: 96 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class Summary:
3939
restored_branch: str | None = None
4040
restoration_note: str | None = None
4141
outcome: str = ""
42+
changed_files_label: str = "Files changed"
4243
changed_files: list[str] = field(default_factory=list)
4344
commits: list[str] = field(default_factory=list)
4445
push_result: str | None = None
@@ -63,7 +64,7 @@ def print_text(self) -> None:
6364
for failure in self.failures:
6465
print(f"- {failure}")
6566
if self.changed_files:
66-
print("Files changed:")
67+
print(f"{self.changed_files_label}:")
6768
for path in self.changed_files:
6869
print(f"- {path}")
6970
if self.commits:
@@ -177,7 +178,16 @@ def restore_original_branch(summary: Summary) -> None:
177178
try:
178179
branch = current_branch()
179180
except WorkflowError as e:
180-
summary.restoration_note = str(e)
181+
if merge_in_progress():
182+
summary.restoration_note = "not restored because a merge is in progress"
183+
return
184+
if status_porcelain().strip():
185+
summary.restoration_note = "not restored because the working tree has uncommitted changes"
186+
return
187+
progress(f"Restoring original branch from detached HEAD: {summary.original_branch}")
188+
git(["checkout", summary.original_branch], summary)
189+
summary.restored_branch = summary.original_branch
190+
summary.restoration_note = "restored from detached HEAD"
181191
return
182192
if branch == summary.original_branch:
183193
summary.restored_branch = branch
@@ -240,14 +250,22 @@ def checkout_pr_no_push_check(pr: int, summary: Summary) -> None:
240250
remember_pr_url(pr_view(pr, summary), summary)
241251

242252

243-
def run_pr_workflow(pr: int, body: Callable[[Summary], int], *, push_required: bool = True) -> int:
253+
def run_pr_workflow(
254+
pr: int,
255+
body: Callable[[Summary], int],
256+
*,
257+
push_required: bool = True,
258+
checkout_required: bool = True,
259+
) -> int:
244260
summary = Summary(pr=pr)
245261
try:
246262
require_clean_worktree(summary)
247263
summary.original_branch = current_branch(summary)
264+
if push_required and not checkout_required:
265+
raise WorkflowError("push workflows require checking out the PR branch")
248266
if push_required:
249267
checkout_pr(pr, summary)
250-
else:
268+
elif checkout_required:
251269
checkout_pr_no_push_check(pr, summary)
252270
return body(summary)
253271
except Exception as e:
@@ -328,8 +346,22 @@ def extract_job_id(check: dict[str, Any]) -> int | None:
328346
return int(value) if isinstance(value, int) else None
329347

330348

331-
def invoke_copilot(prompt: str, summary: Summary) -> str:
332-
cmd = ["copilot", "-p", prompt, "--allow-all-tools", "--model", COPILOT_MODEL]
349+
def invoke_copilot(
350+
prompt: str,
351+
summary: Summary,
352+
event_log_path: Path | None = None,
353+
tool_usage_path: Path | None = None,
354+
*,
355+
allow_all_tools: bool = True,
356+
extra_args: list[str] | None = None,
357+
) -> str:
358+
cmd = ["copilot", "-p", prompt, "--model", COPILOT_MODEL]
359+
if allow_all_tools:
360+
cmd.append("--allow-all-tools")
361+
if extra_args is not None:
362+
cmd.extend(extra_args)
363+
if event_log_path is not None:
364+
cmd.extend(["--output-format", "json"])
333365
progress(f"Handing off to Copilot CLI using {COPILOT_MODEL}; streaming output below")
334366
proc = subprocess.Popen(
335367
cmd,
@@ -342,19 +374,71 @@ def invoke_copilot(prompt: str, summary: Summary) -> str:
342374
bufsize=1,
343375
)
344376
output_parts: list[str] = []
377+
message_parts: list[str] = []
378+
tool_uses: list[dict[str, Any]] = []
379+
event_log = event_log_path.open("w", encoding="utf-8") if event_log_path is not None else None
345380
if proc.stdout is not None:
346-
for line in proc.stdout:
347-
print(line, end="", flush=True)
348-
output_parts.append(line)
381+
try:
382+
for line in proc.stdout:
383+
if event_log is not None:
384+
event_log.write(line)
385+
event_log.flush()
386+
output_parts.append(line)
387+
try:
388+
event = json.loads(line)
389+
except json.JSONDecodeError:
390+
continue
391+
event_type = event.get("type")
392+
data = event.get("data") or {}
393+
if event_type == "assistant.message":
394+
content = data.get("content")
395+
if isinstance(content, str) and content:
396+
print(content, flush=True)
397+
message_parts.append(content)
398+
elif event_type == "tool.execution_start":
399+
tool_name = data.get("toolName")
400+
if isinstance(tool_name, str):
401+
tool_uses.append({"tool": tool_name, "arguments": data.get("arguments")})
402+
continue
403+
print(line, end="", flush=True)
404+
output_parts.append(line)
405+
finally:
406+
if event_log is not None:
407+
event_log.close()
349408
returncode = proc.wait()
350-
output = "".join(output_parts)
409+
output = "\n".join(message_parts) if event_log_path is not None else "".join(output_parts)
351410
if returncode != 0:
352411
raise subprocess.CalledProcessError(
353412
returncode,
354-
["copilot", "-p", "<generated prompt>", "--allow-all-tools", "--model", COPILOT_MODEL],
355-
output,
413+
["copilot", "-p", "<generated prompt>", "--model", COPILOT_MODEL],
414+
"".join(output_parts),
356415
"",
357416
)
417+
if tool_usage_path is not None:
418+
counts: dict[str, int] = {}
419+
for tool_use in tool_uses:
420+
tool = tool_use["tool"]
421+
counts[tool] = counts.get(tool, 0) + 1
422+
tool_usage_path.write_text(
423+
json.dumps(
424+
{
425+
"tools": [{"tool": tool, "count": count} for tool, count in sorted(counts.items())],
426+
"tool_uses": tool_uses,
427+
},
428+
indent=2,
429+
sort_keys=True,
430+
)
431+
+ "\n",
432+
encoding="utf-8",
433+
)
434+
summary.notes.append(f"Copilot tool usage summary: {display_path(str(tool_usage_path))}")
435+
if counts:
436+
tools = ", ".join(f"{tool} ({count})" for tool, count in sorted(counts.items()))
437+
summary.notes.append(f"Copilot tools used: {tools}")
438+
else:
439+
summary.notes.append("Copilot tools used: none")
440+
if event_log_path is not None:
441+
summary.notes.append(f"Copilot event log: {display_path(str(event_log_path))}")
358442
progress("Copilot CLI handoff completed")
359443
return output.strip()
360444

.github/scripts/pr-triage/fix_ci.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ def parse_args() -> argparse.Namespace:
3737
parser.add_argument("pr", type=int, help="pull request number")
3838
parser.add_argument("--no-push", action="store_true", help="commit locally but do not push to the PR")
3939
parser.add_argument("--keep-temp", action="store_true", help="reuse and retain the temp bundle directory")
40+
parser.add_argument(
41+
"--capture-tool-usage",
42+
action="store_true",
43+
help="capture Copilot JSONL events and tool usage summary in the work bundle",
44+
)
4045
return parser.parse_args()
4146

4247

@@ -182,7 +187,14 @@ def body(summary: Summary) -> int:
182187

183188
commit_message_path = bundle_dir / "commit-message.txt"
184189
prompt_improvement_path = bundle_dir / "prompt-improvement.md"
185-
response = invoke_copilot(copilot_prompt(args.pr, bundle_checks, commit_message_path, prompt_improvement_path), summary)
190+
event_log_path = bundle_dir / "copilot-events.jsonl" if args.capture_tool_usage else None
191+
tool_usage_path = bundle_dir / "copilot-tools-used.json" if args.capture_tool_usage else None
192+
response = invoke_copilot(
193+
copilot_prompt(args.pr, bundle_checks, commit_message_path, prompt_improvement_path),
194+
summary,
195+
event_log_path=event_log_path,
196+
tool_usage_path=tool_usage_path,
197+
)
186198
(bundle_dir / "copilot-response.txt").write_text(response + "\n", encoding="utf-8")
187199
read_prompt_improvement(prompt_improvement_path, summary)
188200

.github/scripts/pr-triage/resolve_conflicts.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ def parse_args() -> argparse.Namespace:
2727
parser.add_argument("--upstream", default="upstream", help="upstream remote name")
2828
parser.add_argument("--no-push", action="store_true", help="commit locally but do not push to the PR")
2929
parser.add_argument("--keep-temp", action="store_true", help="reuse and retain the temp bundle directory")
30+
parser.add_argument(
31+
"--capture-tool-usage",
32+
action="store_true",
33+
help="capture Copilot JSONL events and tool usage summary in the work bundle",
34+
)
3035
return parser.parse_args()
3136

3237

@@ -100,7 +105,14 @@ def workflow(summary: Summary) -> int:
100105
bundle_dir = make_temp_dir("otel-conflicts", args.pr, args.keep_temp)
101106
plan_path = write_conflict_bundle(bundle_dir, summary)
102107

103-
response = invoke_copilot(copilot_prompt(plan_path), summary)
108+
event_log_path = bundle_dir / "copilot-events.jsonl" if args.capture_tool_usage else None
109+
tool_usage_path = bundle_dir / "copilot-tools-used.json" if args.capture_tool_usage else None
110+
response = invoke_copilot(
111+
copilot_prompt(plan_path),
112+
summary,
113+
event_log_path=event_log_path,
114+
tool_usage_path=tool_usage_path,
115+
)
104116
(bundle_dir / "copilot-response.txt").write_text(response + "\n", encoding="utf-8")
105117

106118
remaining = unmerged_paths(summary)

0 commit comments

Comments
 (0)