Skip to content

Commit cea0936

Browse files
authored
fix(reviewer-bot): recover claim scope boundary from guidance (#560)
1 parent 932599e commit cea0936

3 files changed

Lines changed: 71 additions & 8 deletions

File tree

scripts/reviewer_bot_core/reviewer_response_policy.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from . import live_review_support, reviewer_review_helpers
2020

2121
_UNSET = object()
22+
_ASSIGNMENT_GUIDANCE_AUTHORS = {"github-actions", "github-actions[bot]", "guidelines-bot"}
2223

2324

2425
def _record_timestamp(record: dict | None, *, parse_timestamp) -> datetime | None:
@@ -63,7 +64,43 @@ def _initial_cycle_boundary(review_data: dict) -> tuple[str | None, str | None]:
6364
return None, None
6465

6566

66-
def _alternate_current_head_cycle_boundary(review_data: dict, issue_snapshot: dict | None) -> str | None:
67+
def _claim_assignment_guidance_timestamp(bot, issue_number: int, current_reviewer: str | None) -> str | None:
68+
if not isinstance(current_reviewer, str) or not current_reviewer.strip():
69+
return None
70+
expected_first_line = f"👋 Hey @{current_reviewer}! You've been assigned to review this coding guideline PR."
71+
first_match: tuple[datetime, str] | None = None
72+
page = 1
73+
while True:
74+
response = bot.github.list_issue_comments_result(issue_number, page=page)
75+
if not response.ok or not isinstance(response.payload, list):
76+
return None
77+
for comment in response.payload:
78+
if not isinstance(comment, dict):
79+
continue
80+
user = comment.get("user")
81+
login = user.get("login") if isinstance(user, dict) else None
82+
created_at = comment.get("created_at")
83+
body = comment.get("body")
84+
if not isinstance(login, str) or not isinstance(created_at, str) or not isinstance(body, str):
85+
continue
86+
if login.lower() not in _ASSIGNMENT_GUIDANCE_AUTHORS:
87+
continue
88+
lines = body.splitlines()
89+
first_line = lines[0].strip() if lines else ""
90+
if first_line != expected_first_line:
91+
continue
92+
created_dt = live_review_support.parse_github_timestamp(created_at)
93+
if created_dt is None:
94+
continue
95+
if first_match is None or created_dt < first_match[0]:
96+
first_match = (created_dt, created_at)
97+
if len(response.payload) < 100:
98+
break
99+
page += 1
100+
return first_match[1] if first_match is not None else None
101+
102+
103+
def _alternate_current_head_cycle_boundary(bot, issue_number: int, review_data: dict, issue_snapshot: dict | None) -> str | None:
67104
if not isinstance(issue_snapshot, dict) or not isinstance(issue_snapshot.get("pull_request"), dict):
68105
return None
69106
if review_data.get("assignment_method") != "claim":
@@ -72,8 +109,7 @@ def _alternate_current_head_cycle_boundary(review_data: dict, issue_snapshot: di
72109
value = review_data.get(field)
73110
if isinstance(value, str) and value:
74111
return None
75-
created_at = issue_snapshot.get("created_at")
76-
return created_at if isinstance(created_at, str) and created_at else None
112+
return _claim_assignment_guidance_timestamp(bot, issue_number, review_data.get("current_reviewer"))
77113

78114

79115
def _scope_basis_and_anchor(review_data: dict, contributor_handoff: dict | None) -> tuple[str | None, str | None]:
@@ -591,7 +627,12 @@ def compute_reviewer_response_state(
591627
reviews,
592628
parse_timestamp=bot.parse_iso8601_timestamp,
593629
)
594-
alternate_current_head_cycle_boundary = _alternate_current_head_cycle_boundary(review_data, issue_snapshot)
630+
alternate_current_head_cycle_boundary = _alternate_current_head_cycle_boundary(
631+
bot,
632+
issue_number,
633+
review_data,
634+
issue_snapshot,
635+
)
595636

596637
return derive_reviewer_response_state(
597638
review_data,

tests/integration/reviewer_bot/test_app_preview_overdue.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def test_execute_run_preview_check_overdue_uses_frozen_pr264_operational_project
9797
}
9898

9999

100-
def test_execute_run_preview_check_overdue_backfills_claim_cycle_from_pr_creation(monkeypatch, capsys):
100+
def test_execute_run_preview_check_overdue_backfills_claim_cycle_from_assignment_guidance(monkeypatch, capsys):
101101
harness = AppHarness(monkeypatch)
102102
harness.set_event(
103103
EVENT_NAME="workflow_dispatch",
@@ -133,7 +133,7 @@ def test_execute_run_preview_check_overdue_backfills_claim_cycle_from_pr_creatio
133133
"state": "open",
134134
"pull_request": {},
135135
"labels": [],
136-
"created_at": "2026-02-10T17:20:07Z",
136+
"created_at": "2025-12-08T04:16:34Z",
137137
},
138138
).add_request(
139139
"GET",
@@ -147,6 +147,17 @@ def test_execute_run_preview_check_overdue_backfills_claim_cycle_from_pr_creatio
147147
).add_pull_request_reviews(
148148
264,
149149
[review_payload(501, state="APPROVED", submitted_at="2026-03-18T12:10:42Z", commit_id="head-live", author="plaindocs")],
150+
).add_request(
151+
"GET",
152+
"issues/264/comments?per_page=100&page=1",
153+
status_code=200,
154+
payload=[
155+
{
156+
"user": {"login": "github-actions"},
157+
"created_at": "2026-02-10T17:20:07Z",
158+
"body": "👋 Hey @iglesias! You've been assigned to review this coding guideline PR.\n\n## Your Role as Reviewer",
159+
}
160+
],
150161
)
151162
harness.runtime.github.stub(routes)
152163
harness.stub_load_state(lambda *, fail_on_unavailable=False: state)

tests/unit/reviewer_bot/test_reviews_live_fetch.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ def test_compute_reviewer_response_state_reports_awaiting_write_approval_after_c
241241
assert response_state["reason"] == "current_head_alternate_approval_present"
242242

243243

244-
def test_compute_reviewer_response_state_uses_issue_created_at_for_claim_alternate_approval_scope(monkeypatch):
244+
def test_compute_reviewer_response_state_uses_assignment_guidance_for_claim_alternate_approval_scope(monkeypatch):
245245
state = make_state()
246246
review = make_tracked_review_state(
247247
state,
@@ -261,11 +261,22 @@ def test_compute_reviewer_response_state_uses_issue_created_at_for_claim_alterna
261261
routes = RouteGitHubApi().add_pull_request_snapshot(42, pull_request_payload(42, head_sha="head-live")).add_pull_request_reviews(
262262
42,
263263
[review_payload(10, state="APPROVED", submitted_at="2026-03-18T12:10:42Z", commit_id="head-live", author="plaindocs")],
264+
).add_request(
265+
"GET",
266+
"issues/42/comments?per_page=100&page=1",
267+
status_code=200,
268+
payload=[
269+
{
270+
"user": {"login": "github-actions"},
271+
"created_at": "2026-02-10T17:20:07Z",
272+
"body": "👋 Hey @iglesias! You've been assigned to review this coding guideline PR.\n\n## Your Role as Reviewer",
273+
}
274+
],
264275
)
265276
runtime = _runtime(monkeypatch, routes)
266277
runtime.github.get_issue_or_pr_snapshot = lambda issue_number: {
267278
**issue_snapshot(issue_number, state="open", is_pull_request=True),
268-
"created_at": "2026-02-10T17:20:07Z",
279+
"created_at": "2025-12-08T04:16:34Z",
269280
}
270281

271282
response_state = reviews.compute_reviewer_response_state(runtime, 42, review)

0 commit comments

Comments
 (0)