-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathreconcile.py
More file actions
539 lines (487 loc) · 27 KB
/
reconcile.py
File metadata and controls
539 lines (487 loc) · 27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
"""Trusted deferred reconcile helpers for reviewer-bot workflow_run processing."""
from __future__ import annotations
import json
import os
from .comment_routing import (
_digest_body,
_handle_command,
_record_conversation_freshness,
classify_comment_payload,
)
from .reviews import (
accept_reviewer_review_from_live_review,
find_triage_approval_after,
get_latest_valid_current_reviewer_review_for_cycle,
)
def _now_iso(bot) -> str:
return bot.datetime.now(bot.timezone.utc).isoformat()
def _ensure_source_event_key(review_data: dict, source_event_key: str, payload: dict | None = None) -> None:
review_data.setdefault("deferred_gaps", {})
if payload is None:
payload = {}
payload["source_event_key"] = source_event_key
review_data["deferred_gaps"][source_event_key] = payload
def _clear_source_event_key(review_data: dict, source_event_key: str) -> None:
deferred_gaps = review_data.get("deferred_gaps")
if isinstance(deferred_gaps, dict):
deferred_gaps.pop(source_event_key, None)
def _mark_reconciled_source_event(review_data: dict, source_event_key: str) -> None:
reconciled = review_data.setdefault("reconciled_source_events", [])
if source_event_key not in reconciled:
reconciled.append(source_event_key)
def _was_reconciled_source_event(review_data: dict, source_event_key: str) -> bool:
reconciled = review_data.get("reconciled_source_events")
return isinstance(reconciled, list) and source_event_key in reconciled
def _record_review_rebuild(bot, state: dict, issue_number: int, review_data: dict) -> bool:
pull_request = bot.github_api("GET", f"pulls/{issue_number}")
if not isinstance(pull_request, dict):
raise RuntimeError(f"Failed to fetch pull request #{issue_number}")
reviews = bot.get_pull_request_reviews(issue_number)
if reviews is None:
raise RuntimeError(f"Failed to fetch live reviews for PR #{issue_number}")
completion, _ = bot.reviews_module.rebuild_pr_approval_state(bot, issue_number, review_data, pull_request=pull_request, reviews=reviews)
if completion is None:
raise RuntimeError(f"Unable to rebuild approval state for PR #{issue_number}")
latest = get_latest_valid_current_reviewer_review_for_cycle(bot, issue_number, review_data, reviews=reviews)
if latest is not None:
submitted_at = latest.get("submitted_at")
if accept_reviewer_review_from_live_review(review_data, latest, actor=review_data.get("current_reviewer")) and isinstance(submitted_at, str):
bot.reviews_module.record_reviewer_activity(review_data, submitted_at)
return bool(completion.get("completed"))
def reconcile_active_review_entry(
bot,
state: dict,
issue_number: int,
*,
require_pull_request_context: bool = True,
completion_source: str = "rectify:reconcile-pr-review",
) -> tuple[str, bool, bool]:
review_data = bot.ensure_review_entry(state, issue_number)
if review_data is None:
return f"ℹ️ No active review entry exists for #{issue_number}; nothing to rectify.", True, False
assigned_reviewer = review_data.get("current_reviewer")
if not assigned_reviewer:
return f"ℹ️ #{issue_number} has no tracked assigned reviewer; nothing to rectify.", True, False
if require_pull_request_context and os.environ.get("IS_PULL_REQUEST", "false").lower() != "true":
return f"ℹ️ #{issue_number} is not a pull request in this event context; `/rectify` only reconciles PR reviews.", True, False
if str(state.get("freshness_runtime_epoch", "")).strip() != "freshness_v15" and os.environ.get("IS_PULL_REQUEST", "false").lower() == "true":
return "ℹ️ PR review freshness rectify is epoch-gated and currently inactive.", True, False
state_changed = bot.maybe_record_head_observation_repair(issue_number, review_data)
reviews = bot.get_pull_request_reviews(issue_number)
if reviews is None:
return f"❌ Failed to fetch reviews for PR #{issue_number}; cannot run `/rectify`.", False, False
latest_review = get_latest_valid_current_reviewer_review_for_cycle(bot, issue_number, review_data, reviews=reviews)
messages: list[str] = []
if latest_review is not None:
latest_state = str(latest_review.get("state", "")).upper()
submitted_at = latest_review.get("submitted_at")
if accept_reviewer_review_from_live_review(review_data, latest_review, actor=assigned_reviewer) and isinstance(submitted_at, str):
state_changed = True
bot.reviews_module.record_reviewer_activity(review_data, submitted_at)
messages.append(f"latest review by @{assigned_reviewer} is `{latest_state}`")
if _record_review_rebuild(bot, state, issue_number, review_data):
state_changed = True
review_data["review_completion_source"] = completion_source
if review_data.get("mandatory_approver_required"):
escalation_opened_at = bot.parse_iso8601_timestamp(review_data.get("mandatory_approver_pinged_at")) or bot.parse_iso8601_timestamp(review_data.get("mandatory_approver_label_applied_at"))
triage_approval = find_triage_approval_after(bot, reviews, escalation_opened_at)
if triage_approval is not None:
approver, _ = triage_approval
if bot.satisfy_mandatory_approver_requirement(state, issue_number, approver):
state_changed = True
messages.append(f"mandatory triage approval satisfied by @{approver}")
if state_changed:
return f"✅ Rectified PR #{issue_number}: {'; '.join(messages) or 'reconciled live review state'}.", True, True
return f"ℹ️ Rectify checked PR #{issue_number}: {'; '.join(messages) or 'no reconciliation transitions applied'}.", True, False
def _validate_deferred_comment_artifact(payload: dict) -> None:
required = {
"schema_version",
"source_workflow_name",
"source_workflow_file",
"source_run_id",
"source_run_attempt",
"source_event_name",
"source_event_action",
"source_event_key",
"pr_number",
"comment_id",
"comment_class",
"has_non_command_text",
"source_body_digest",
"source_created_at",
}
missing = sorted(required - set(payload))
if missing:
raise RuntimeError("Deferred comment artifact missing required fields: " + ", ".join(missing))
if payload.get("schema_version") != 2:
raise RuntimeError("Deferred comment artifact schema_version is not accepted by V18 reconcile")
if not isinstance(payload.get("comment_id"), int) or not isinstance(payload.get("pr_number"), int):
raise RuntimeError("Deferred comment artifact comment_id and pr_number must be integers")
if not isinstance(payload.get("comment_class"), str) or not isinstance(payload.get("has_non_command_text"), bool):
raise RuntimeError("Deferred comment artifact parse fields are malformed")
if not isinstance(payload.get("source_body_digest"), str) or not isinstance(payload.get("source_created_at"), str):
raise RuntimeError("Deferred comment artifact source digest or timestamp is malformed")
def _validate_deferred_review_artifact(payload: dict) -> None:
required = {
"schema_version",
"source_workflow_name",
"source_workflow_file",
"source_run_id",
"source_run_attempt",
"source_event_name",
"source_event_action",
"source_event_key",
"pr_number",
"review_id",
}
missing = sorted(required - set(payload))
if missing:
raise RuntimeError("Deferred review artifact missing required fields: " + ", ".join(missing))
if payload.get("schema_version") != 2:
raise RuntimeError("Deferred review artifact schema_version is not accepted by V18 reconcile")
if not isinstance(payload.get("review_id"), int) or not isinstance(payload.get("pr_number"), int):
raise RuntimeError("Deferred review artifact review_id and pr_number must be integers")
def _load_deferred_context() -> dict:
path = os.environ.get("DEFERRED_CONTEXT_PATH", "").strip()
if not path:
raise RuntimeError("Missing DEFERRED_CONTEXT_PATH for workflow_run reconcile")
with open(path, encoding="utf-8") as handle:
payload = json.load(handle)
if not isinstance(payload, dict):
raise RuntimeError("Deferred context payload must be a JSON object")
return payload
def _set_env_if_present(name: str, value) -> None:
if value is None:
return
os.environ[name] = str(value)
def _hydrate_reconcile_pr_context(bot, pr_number: int) -> dict:
pull_request = bot.github_api("GET", f"pulls/{pr_number}")
if not isinstance(pull_request, dict):
raise RuntimeError(f"Failed to fetch live PR #{pr_number} for reconcile context")
author = pull_request.get("user")
if not isinstance(author, dict):
raise RuntimeError(f"Live PR #{pr_number} is missing author metadata")
author_login = author.get("login")
if not isinstance(author_login, str) or not author_login.strip():
raise RuntimeError(f"Live PR #{pr_number} is missing a valid author login")
labels = pull_request.get("labels")
if labels is None:
labels = []
if not isinstance(labels, list):
raise RuntimeError(f"Live PR #{pr_number} labels are malformed")
label_names: list[str] = []
for label in labels:
if not isinstance(label, dict):
raise RuntimeError(f"Live PR #{pr_number} contains malformed label metadata")
name = label.get("name")
if not isinstance(name, str):
raise RuntimeError(f"Live PR #{pr_number} contains a label without a valid name")
label_names.append(name)
os.environ["IS_PULL_REQUEST"] = "true"
os.environ["ISSUE_AUTHOR"] = author_login
os.environ["ISSUE_LABELS"] = json.dumps(label_names)
return pull_request
def _hydrate_reconcile_comment_context(live_comment: dict, payload: dict) -> None:
user = live_comment.get("user")
if not isinstance(user, dict):
raise RuntimeError("Live deferred comment user metadata is unavailable")
comment_author = user.get("login") or payload.get("actor_login") or ""
if not isinstance(comment_author, str) or not comment_author.strip():
raise RuntimeError("Live deferred comment author login is unavailable")
comment_user_type = user.get("type")
if not isinstance(comment_user_type, str) or not comment_user_type.strip():
raise RuntimeError("Live deferred comment user type is unavailable")
author_association = live_comment.get("author_association")
if not isinstance(author_association, str) or not author_association.strip():
raise RuntimeError("Live deferred comment author association is unavailable")
_set_env_if_present("COMMENT_AUTHOR", comment_author)
_set_env_if_present("COMMENT_ID", payload.get("comment_id"))
_set_env_if_present("COMMENT_CREATED_AT", payload.get("source_created_at"))
_set_env_if_present("COMMENT_USER_TYPE", comment_user_type)
_set_env_if_present("COMMENT_AUTHOR_ASSOCIATION", author_association)
_set_env_if_present("COMMENT_SENDER_TYPE", comment_user_type)
os.environ["COMMENT_INSTALLATION_ID"] = ""
os.environ["COMMENT_PERFORMED_VIA_GITHUB_APP"] = "true" if live_comment.get("performed_via_github_app") else "false"
def _validate_observer_noop_payload(payload: dict) -> None:
required = {
"schema_version",
"kind",
"reason",
"source_workflow_name",
"source_workflow_file",
"source_run_id",
"source_run_attempt",
"source_event_name",
"source_event_action",
"source_event_key",
"pr_number",
}
missing = sorted(required - set(payload))
if missing:
raise RuntimeError("Observer no-op payload missing required fields: " + ", ".join(missing))
if payload.get("schema_version") != 1:
raise RuntimeError("Observer no-op payload schema_version is not accepted")
if payload.get("kind") != "observer_noop":
raise RuntimeError("Observer no-op payload kind mismatch")
if not isinstance(payload.get("reason"), str) or not payload.get("reason"):
raise RuntimeError("Observer no-op payload reason must be a non-empty string")
if not isinstance(payload.get("pr_number"), int):
raise RuntimeError("Observer no-op payload pr_number must be an integer")
def _expected_observer_identity(payload: dict) -> tuple[str, str]:
event_name = payload.get("source_event_name")
event_action = payload.get("source_event_action")
if event_name == "issue_comment" and event_action == "created":
return (
"Reviewer Bot PR Comment Observer",
".github/workflows/reviewer-bot-pr-comment-observer.yml",
)
if event_name == "pull_request_review" and event_action == "submitted":
return (
"Reviewer Bot PR Review Submitted Observer",
".github/workflows/reviewer-bot-pr-review-submitted-observer.yml",
)
if event_name == "pull_request_review" and event_action == "dismissed":
return (
"Reviewer Bot PR Review Dismissed Observer",
".github/workflows/reviewer-bot-pr-review-dismissed-observer.yml",
)
raise RuntimeError("Unsupported deferred workflow identity")
def _validate_workflow_run_artifact_identity(payload: dict) -> None:
expected_name, expected_file = _expected_observer_identity(payload)
if payload.get("source_workflow_name") != expected_name:
raise RuntimeError("Deferred artifact workflow name mismatch")
if payload.get("source_workflow_file") != expected_file:
raise RuntimeError("Deferred artifact workflow file mismatch")
triggering_name = os.environ.get("WORKFLOW_RUN_TRIGGERING_NAME", "").strip()
if triggering_name and triggering_name != expected_name:
raise RuntimeError("Triggering workflow name mismatch")
triggering_id = os.environ.get("WORKFLOW_RUN_TRIGGERING_ID", "").strip()
if triggering_id and str(payload.get("source_run_id")) != triggering_id:
raise RuntimeError("Deferred artifact run_id mismatch")
triggering_attempt = os.environ.get("WORKFLOW_RUN_TRIGGERING_ATTEMPT", "").strip()
if triggering_attempt and str(payload.get("source_run_attempt")) != triggering_attempt:
raise RuntimeError("Deferred artifact run_attempt mismatch")
if os.environ.get("WORKFLOW_RUN_TRIGGERING_CONCLUSION", "").strip() != "success":
raise RuntimeError("Triggering observer workflow did not conclude successfully")
def _artifact_expected_name(payload: dict) -> str:
event_name = payload.get("source_event_name")
event_action = payload.get("source_event_action")
run_id = payload.get("source_run_id")
run_attempt = payload.get("source_run_attempt")
if event_name == "issue_comment" and event_action == "created":
return f"reviewer-bot-comment-context-{run_id}-attempt-{run_attempt}"
if event_name == "pull_request_review" and event_action == "submitted":
return f"reviewer-bot-review-submitted-context-{run_id}-attempt-{run_attempt}"
if event_name == "pull_request_review" and event_action == "dismissed":
return f"reviewer-bot-review-dismissed-context-{run_id}-attempt-{run_attempt}"
raise RuntimeError("Unsupported deferred artifact naming")
def _artifact_expected_payload_name(payload: dict) -> str:
event_name = payload.get("source_event_name")
event_action = payload.get("source_event_action")
if event_name == "issue_comment" and event_action == "created":
return "deferred-comment.json"
if event_name == "pull_request_review" and event_action == "submitted":
return "deferred-review-submitted.json"
if event_name == "pull_request_review" and event_action == "dismissed":
return "deferred-review-dismissed.json"
raise RuntimeError("Unsupported deferred payload path")
def _update_deferred_gap(bot, review_data: dict, payload: dict, reason: str, diagnostic_summary: str) -> None:
source_event_key = str(payload.get("source_event_key", ""))
if not source_event_key:
return
review_data.setdefault("deferred_gaps", {})
existing = review_data["deferred_gaps"].get(source_event_key, {})
if not isinstance(existing, dict):
existing = {}
existing.update(
{
"source_event_key": source_event_key,
"source_event_kind": f"{payload.get('source_event_name')}:{payload.get('source_event_action')}",
"pr_number": payload.get("pr_number"),
"reason": reason,
"source_event_created_at": payload.get("source_created_at") or payload.get("source_submitted_at"),
"source_run_id": payload.get("source_run_id"),
"source_run_attempt": payload.get("source_run_attempt"),
"source_workflow_file": payload.get("source_workflow_file"),
"source_artifact_name": payload.get("source_artifact_name"),
"first_noted_at": existing.get("first_noted_at") or _now_iso(bot),
"last_checked_at": _now_iso(bot),
"operator_action_required": True,
"diagnostic_summary": diagnostic_summary,
}
)
review_data["deferred_gaps"][source_event_key] = existing
def _validate_live_comment_replay_contract(bot, review_data: dict, payload: dict, live_body: str) -> dict | None:
source_comment_class = str(payload.get("comment_class", ""))
live_classified = classify_comment_payload(bot, live_body)
live_comment_class = str(live_classified.get("comment_class", ""))
source_has_non_command_text = bool(payload.get("has_non_command_text"))
live_has_non_command_text = bool(live_classified.get("has_non_command_text"))
if live_comment_class != source_comment_class:
_update_deferred_gap(
bot,
review_data,
payload,
"reconcile_failed_closed",
(
f"Deferred comment {payload['comment_id']} classification changed from "
f"{source_comment_class} to {live_comment_class}; replay suppressed. "
f"See {bot.REVIEW_FRESHNESS_RUNBOOK_PATH}."
),
)
return None
if live_has_non_command_text != source_has_non_command_text:
_update_deferred_gap(
bot,
review_data,
payload,
"reconcile_failed_closed",
(
f"Deferred comment {payload['comment_id']} non-command text classification drifted; "
f"replay suppressed. See {bot.REVIEW_FRESHNESS_RUNBOOK_PATH}."
),
)
return None
if source_comment_class in {"command_only", "command_plus_text"} and int(live_classified.get("command_count", 0)) != 1:
_update_deferred_gap(
bot,
review_data,
payload,
"reconcile_failed_closed",
(
f"Deferred comment {payload['comment_id']} no longer resolves to exactly one command; "
f"replay suppressed. See {bot.REVIEW_FRESHNESS_RUNBOOK_PATH}."
),
)
return None
return live_classified
def handle_workflow_run_event(bot, state: dict) -> bool:
bot.assert_lock_held("handle_workflow_run_event")
if str(state.get("freshness_runtime_epoch", "")).strip() != "freshness_v15":
print("V18 workflow_run reconcile safe-noop before epoch flip")
return False
payload = _load_deferred_context()
pr_number = int(payload.get("pr_number", 0) or 0)
if pr_number <= 0:
raise RuntimeError("Deferred context is missing a valid PR number")
bot.collect_touched_item(pr_number)
review_data = bot.ensure_review_entry(state, pr_number, create=True)
if review_data is None:
raise RuntimeError(f"No review entry available for PR #{pr_number}")
event_name = payload.get("source_event_name")
event_action = payload.get("source_event_action")
source_event_key = str(payload.get("source_event_key", ""))
try:
if payload.get("kind") == "observer_noop":
_validate_observer_noop_payload(payload)
_validate_workflow_run_artifact_identity(payload)
print(
"Observer workflow produced explicit no-op payload for "
f"{source_event_key}: {payload.get('reason')}"
)
return False
if event_name == "issue_comment":
_validate_deferred_comment_artifact(payload)
_validate_workflow_run_artifact_identity(payload)
_hydrate_reconcile_pr_context(bot, pr_number)
if source_event_key != f"issue_comment:{payload['comment_id']}":
raise RuntimeError("Deferred comment artifact source_event_key mismatch")
comment_author = str(payload.get("actor_login", ""))
comment_created_at = str(payload.get("source_created_at"))
comment_id_value = payload.get("comment_id")
if not isinstance(comment_id_value, int):
raise RuntimeError("Deferred comment artifact comment_id must be an integer")
comment_id = comment_id_value
classified = payload.get("comment_class")
source_freshness_eligible = classified in {"plain_text", "command_plus_text"} and bool(payload.get("has_non_command_text"))
live_comment = bot.github_api("GET", f"issues/comments/{payload['comment_id']}")
if not isinstance(live_comment, dict):
changed = False
if source_freshness_eligible:
changed = _record_conversation_freshness(bot, state, pr_number, comment_author, comment_id, comment_created_at)
_update_deferred_gap(bot, review_data, payload, "reconcile_failed_closed", f"Deferred comment {payload['comment_id']} is no longer visible; source-time freshness only may be preserved. See {bot.REVIEW_FRESHNESS_RUNBOOK_PATH}.")
return changed
_hydrate_reconcile_comment_context(live_comment, payload)
live_body = live_comment.get("body")
if not isinstance(live_body, str):
raise RuntimeError("Live deferred comment body is unavailable")
if _digest_body(live_body) != payload.get("source_body_digest"):
changed = False
if source_freshness_eligible:
changed = _record_conversation_freshness(bot, state, pr_number, comment_author, comment_id, comment_created_at)
_update_deferred_gap(bot, review_data, payload, "reconcile_failed_closed", f"Deferred comment {payload['comment_id']} body digest changed; command execution suppressed. See {bot.REVIEW_FRESHNESS_RUNBOOK_PATH}.")
return changed
changed = False
if source_freshness_eligible:
changed = _record_conversation_freshness(bot, state, pr_number, comment_author, comment_id, comment_created_at) or changed
live_classified = _validate_live_comment_replay_contract(bot, review_data, payload, live_body)
if live_classified is None:
return changed
if classified in {"command_only", "command_plus_text"}:
changed = _handle_command(bot, state, pr_number, comment_author, live_classified) or changed
_mark_reconciled_source_event(review_data, source_event_key)
_clear_source_event_key(review_data, source_event_key)
return changed
if event_name == "pull_request_review" and event_action == "submitted":
_validate_deferred_review_artifact(payload)
_validate_workflow_run_artifact_identity(payload)
review_id_value = payload.get("review_id")
if not isinstance(review_id_value, int):
raise RuntimeError("Deferred review artifact review_id must be an integer")
review_id = review_id_value
if source_event_key != f"pull_request_review:{review_id}":
raise RuntimeError("Deferred review-submitted artifact source_event_key mismatch")
live_review = bot.github_api("GET", f"pulls/{pr_number}/reviews/{review_id}")
live_pr = bot.github_api("GET", f"pulls/{pr_number}")
if not isinstance(live_pr, dict):
raise RuntimeError(f"Failed to fetch live PR #{pr_number}")
live_commit_id = None
live_submitted_at = payload.get("source_submitted_at")
live_state = payload.get("source_review_state")
if isinstance(live_review, dict):
live_commit_id = live_review.get("commit_id")
live_submitted_at = live_review.get("submitted_at") or live_submitted_at
live_state = live_review.get("state") or live_state
else:
live_commit_id = payload.get("source_commit_id")
actor = str(payload.get("actor_login", ""))
changed = bot.maybe_record_head_observation_repair(pr_number, review_data)
if isinstance(review_data.get("current_reviewer"), str) and review_data.get("current_reviewer", "").lower() == actor.lower() and isinstance(live_commit_id, str) and isinstance(live_submitted_at, str):
bot.reviews_module.accept_channel_event(
review_data,
"reviewer_review",
semantic_key=source_event_key,
timestamp=live_submitted_at,
actor=actor,
reviewed_head_sha=live_commit_id,
source_precedence=1,
)
bot.reviews_module.record_reviewer_activity(review_data, live_submitted_at)
_record_review_rebuild(bot, state, pr_number, review_data)
_mark_reconciled_source_event(review_data, source_event_key)
_clear_source_event_key(review_data, source_event_key)
return changed or True
if event_name == "pull_request_review" and event_action == "dismissed":
_validate_deferred_review_artifact(payload)
_validate_workflow_run_artifact_identity(payload)
review_id_value = payload.get("review_id")
if not isinstance(review_id_value, int):
raise RuntimeError("Deferred review artifact review_id must be an integer")
if source_event_key != f"pull_request_review_dismissed:{review_id_value}":
raise RuntimeError("Deferred review-dismissed artifact source_event_key mismatch")
bot.reviews_module.accept_channel_event(
review_data,
"review_dismissal",
semantic_key=source_event_key,
timestamp=_now_iso(bot),
dismissal_only=True,
)
bot.maybe_record_head_observation_repair(pr_number, review_data)
_record_review_rebuild(bot, state, pr_number, review_data)
_mark_reconciled_source_event(review_data, source_event_key)
_clear_source_event_key(review_data, source_event_key)
return True
except RuntimeError as exc:
_update_deferred_gap(bot, review_data, payload, "reconcile_failed_closed", f"{exc} See {bot.REVIEW_FRESHNESS_RUNBOOK_PATH}.")
raise
raise RuntimeError("Unsupported deferred workflow_run payload")