feat(robomp): added incoming PR review feature with classify and submit
- Added `review_pr` task that checks out PR head in a detached worktree, classifies rank/type/area, and posts a batched GitHub review as `event=COMMENT`. - Added four new host tools: `fetch_pr`, `classify_pr`, `pr_review_comment`, and `submit_pr_review`; review tools self-gate on `review_mode`, push/open-PR tools refuse when `review_mode` is set. - Added sqlite staging table `pr_review_comments` with `stage_review_comment`, `list_staged_review_comments`, and `clear_staged_review_comments` DAOs. - Routed `pull_request.opened/reopened/ready_for_review` to `review_pr` and extended `pull_request.closed` cleanup to any tracked PR regardless of author.
This commit is contained in:
@@ -134,6 +134,7 @@ def route(
|
||||
maintainers: frozenset[str] = frozenset(),
|
||||
reviewer_bots: frozenset[str] = frozenset(),
|
||||
resolve_issue_from_pr: PrIssueResolver = None,
|
||||
pr_review_enabled: bool = True,
|
||||
) -> RouteDecision:
|
||||
"""Decide whether and how to handle a webhook event.
|
||||
|
||||
@@ -225,23 +226,26 @@ def route(
|
||||
if not isinstance(number, int):
|
||||
return RouteDecision("skip", None, repo, None, "comment missing issue number")
|
||||
if "pull_request" in issue:
|
||||
# Conversation comment on a PR. The PR number lives at issue.number
|
||||
# on this payload type. Prefer the originating issue key when the
|
||||
# DB has it, but do not drop bot-authored follow-ups just because
|
||||
# the PR mapping was lost; the worker can recover from the PR
|
||||
# branch or handle the PR directly.
|
||||
# Conversation comments on incoming contributor PRs are intentionally
|
||||
# ignored for now: the one-shot review runs on open, and re-review
|
||||
# directives are not wired yet. Only bot-authored PRs resume a live
|
||||
# amend-and-push workflow.
|
||||
key = _resolve_pr_key(number)
|
||||
login, assoc = _submitter_info(comment)
|
||||
return RouteDecision(
|
||||
"queue",
|
||||
"handle_pr_conversation",
|
||||
repo,
|
||||
key,
|
||||
f"issue_comment.created on PR #{number}",
|
||||
submitter=login,
|
||||
association=assoc,
|
||||
**_directive_kwargs(comment, login, assoc),
|
||||
)
|
||||
issue_user_raw = issue.get("user")
|
||||
issue_user = issue_user_raw if isinstance(issue_user_raw, Mapping) else {}
|
||||
if str(issue_user.get("login") or "") == bot_login:
|
||||
return RouteDecision(
|
||||
"queue",
|
||||
"handle_pr_conversation",
|
||||
repo,
|
||||
key,
|
||||
f"issue_comment.created on PR #{number}",
|
||||
submitter=login,
|
||||
association=assoc,
|
||||
**_directive_kwargs(comment, login, assoc),
|
||||
)
|
||||
return RouteDecision("skip", None, repo, issue_key(repo, number), "incoming PR comments ignored")
|
||||
key = issue_key(repo, number)
|
||||
login, assoc = _submitter_info(comment)
|
||||
return RouteDecision(
|
||||
@@ -255,6 +259,29 @@ def route(
|
||||
**_directive_kwargs(comment, login, assoc),
|
||||
)
|
||||
|
||||
if event_type == "pull_request" and action in ("opened", "reopened", "ready_for_review"):
|
||||
if not pr_review_enabled:
|
||||
return RouteDecision("skip", None, repo, None, "PR review disabled")
|
||||
pr = payload.get("pull_request") or {}
|
||||
if bool(pr.get("draft")):
|
||||
return RouteDecision("skip", None, repo, None, "draft PR")
|
||||
pr_user = pr.get("user") or {}
|
||||
if _is_bot_account(pr_user, bot_login):
|
||||
return RouteDecision("skip", None, repo, None, "bot-authored PR")
|
||||
number = pr.get("number")
|
||||
if not isinstance(number, int):
|
||||
return RouteDecision("skip", None, repo, None, "PR missing number")
|
||||
login, assoc = _submitter_info(pr)
|
||||
return RouteDecision(
|
||||
"queue",
|
||||
"review_pr",
|
||||
repo,
|
||||
issue_key(repo, number),
|
||||
f"pull_request.{action}",
|
||||
submitter=login,
|
||||
association=assoc,
|
||||
)
|
||||
|
||||
if event_type == "pull_request_review_comment" and action == "created":
|
||||
comment = payload.get("comment") or {}
|
||||
rb_login = _reviewer_bot_login(comment.get("user"))
|
||||
@@ -282,15 +309,11 @@ def route(
|
||||
|
||||
if event_type == "pull_request" and action == "closed":
|
||||
pr = payload.get("pull_request") or {}
|
||||
pr_user = pr.get("user") or {}
|
||||
if str(pr_user.get("login") or "") != bot_login:
|
||||
return RouteDecision("skip", None, repo, None, "PR not bot-authored")
|
||||
if not bool(pr.get("merged")):
|
||||
return RouteDecision("skip", None, repo, None, "PR closed without merge")
|
||||
number = pr.get("number")
|
||||
if not isinstance(number, int):
|
||||
return RouteDecision("skip", None, repo, None, "PR missing number")
|
||||
return RouteDecision("queue", "cleanup_workspace", repo, _resolve_pr_key(number), "pull_request.merged")
|
||||
reason = "pull_request.merged" if bool(pr.get("merged")) else "pull_request.closed"
|
||||
return RouteDecision("queue", "cleanup_workspace", repo, _resolve_pr_key(number), reason)
|
||||
|
||||
return RouteDecision("skip", None, repo, None, f"{event_type}.{action} not handled")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user