|
20 | 20 |
|
21 | 21 | _UNSET = object() |
22 | 22 | _ASSIGNMENT_GUIDANCE_AUTHORS = {"github-actions", "github-actions[bot]", "guidelines-bot"} |
| 23 | +_FAIL_CLOSED_REVIEWER_ACTIVITY_GAP_REASONS = frozenset( |
| 24 | + { |
| 25 | + "observer_failed", |
| 26 | + "observer_cancelled", |
| 27 | + "observer_run_missing", |
| 28 | + "observer_state_unknown", |
| 29 | + "artifact_missing", |
| 30 | + "artifact_invalid", |
| 31 | + "artifact_expired", |
| 32 | + "reconcile_failed_closed", |
| 33 | + } |
| 34 | +) |
| 35 | +_REVIEWER_ACTIVITY_GAP_KINDS = frozenset( |
| 36 | + { |
| 37 | + "issue_comment:created", |
| 38 | + "pull_request_review:submitted", |
| 39 | + "pull_request_review_comment:created", |
| 40 | + } |
| 41 | +) |
23 | 42 |
|
24 | 43 |
|
25 | 44 | def _record_timestamp(record: dict | None, *, parse_timestamp) -> datetime | None: |
@@ -139,6 +158,102 @@ def _build_current_scope_key( |
139 | 158 | ) |
140 | 159 |
|
141 | 160 |
|
| 161 | +def _gap_source_kind(gap: dict) -> str | None: |
| 162 | + source_event_kind = gap.get("source_event_kind") |
| 163 | + if isinstance(source_event_kind, str) and source_event_kind.strip(): |
| 164 | + return source_event_kind |
| 165 | + source_event_key = gap.get("source_event_key") |
| 166 | + if not isinstance(source_event_key, str): |
| 167 | + return None |
| 168 | + if source_event_key.startswith("issue_comment:"): |
| 169 | + return "issue_comment:created" |
| 170 | + if source_event_key.startswith("pull_request_review_comment:"): |
| 171 | + return "pull_request_review_comment:created" |
| 172 | + if source_event_key.startswith("pull_request_review:"): |
| 173 | + return "pull_request_review:submitted" |
| 174 | + return None |
| 175 | + |
| 176 | + |
| 177 | +def _gap_event_timestamp(gap: dict): |
| 178 | + return live_review_support.parse_github_timestamp(gap.get("source_event_created_at")) |
| 179 | + |
| 180 | + |
| 181 | +def _visible_review_commit_id(gap: dict) -> str | None: |
| 182 | + diagnostic = gap.get("visible_review_diagnostic") |
| 183 | + payload = diagnostic.get("payload") if isinstance(diagnostic, dict) else None |
| 184 | + commit_id = payload.get("commit_id") if isinstance(payload, dict) else None |
| 185 | + if isinstance(commit_id, str) and commit_id.strip(): |
| 186 | + return commit_id |
| 187 | + source_commit_id = gap.get("source_commit_id") |
| 188 | + if isinstance(source_commit_id, str) and source_commit_id.strip(): |
| 189 | + return source_commit_id |
| 190 | + comment = gap.get("comment") |
| 191 | + comment_commit_id = None |
| 192 | + if isinstance(comment, dict): |
| 193 | + comment_commit_id = comment.get("commit_id") or comment.get("original_commit_id") |
| 194 | + return comment_commit_id if isinstance(comment_commit_id, str) and comment_commit_id.strip() else None |
| 195 | + |
| 196 | + |
| 197 | +def _visible_activity_author(gap: dict) -> str | None: |
| 198 | + diagnostic = gap.get("visible_review_diagnostic") |
| 199 | + payload = diagnostic.get("payload") if isinstance(diagnostic, dict) else None |
| 200 | + author = payload.get("author") if isinstance(payload, dict) else None |
| 201 | + if isinstance(author, str) and author.strip(): |
| 202 | + return author |
| 203 | + source_actor = gap.get("source_actor_login") |
| 204 | + if isinstance(source_actor, str) and source_actor.strip(): |
| 205 | + return source_actor |
| 206 | + actor = gap.get("actor") |
| 207 | + if isinstance(actor, str) and actor.strip(): |
| 208 | + return actor |
| 209 | + comment = gap.get("comment") |
| 210 | + user = comment.get("user") if isinstance(comment, dict) else None |
| 211 | + login = user.get("login") if isinstance(user, dict) else None |
| 212 | + return login if isinstance(login, str) and login.strip() else None |
| 213 | + |
| 214 | + |
| 215 | +def has_fail_closed_reviewer_activity_for_current_scope( |
| 216 | + review_data: dict, |
| 217 | + fail_closed_gaps, |
| 218 | + *, |
| 219 | + anchor_timestamp: str | None = None, |
| 220 | + current_head_sha: str | None = None, |
| 221 | +) -> bool: |
| 222 | + current_reviewer = review_data.get("current_reviewer") |
| 223 | + if not isinstance(current_reviewer, str) or not current_reviewer.strip(): |
| 224 | + return False |
| 225 | + floor = live_review_support.parse_github_timestamp(anchor_timestamp) |
| 226 | + if floor is None: |
| 227 | + _, cycle_boundary = _initial_cycle_boundary(review_data) |
| 228 | + floor = live_review_support.parse_github_timestamp(cycle_boundary) |
| 229 | + for gap in fail_closed_gaps: |
| 230 | + if not isinstance(gap, dict): |
| 231 | + continue |
| 232 | + if gap.get("operator_action_required") is not True: |
| 233 | + continue |
| 234 | + if gap.get("reason") not in _FAIL_CLOSED_REVIEWER_ACTIVITY_GAP_REASONS: |
| 235 | + continue |
| 236 | + source_kind = _gap_source_kind(gap) |
| 237 | + if source_kind not in _REVIEWER_ACTIVITY_GAP_KINDS: |
| 238 | + continue |
| 239 | + event_timestamp = _gap_event_timestamp(gap) |
| 240 | + if event_timestamp is None: |
| 241 | + continue |
| 242 | + if floor is not None and event_timestamp < floor: |
| 243 | + continue |
| 244 | + author = _visible_activity_author(gap) |
| 245 | + if not isinstance(author, str) or author.lower() != current_reviewer.lower(): |
| 246 | + continue |
| 247 | + if source_kind in {"pull_request_review:submitted", "pull_request_review_comment:created"}: |
| 248 | + if not isinstance(current_head_sha, str) or not current_head_sha.strip(): |
| 249 | + continue |
| 250 | + commit_id = _visible_review_commit_id(gap) |
| 251 | + if commit_id != current_head_sha: |
| 252 | + continue |
| 253 | + return True |
| 254 | + return False |
| 255 | + |
| 256 | + |
142 | 257 | def _current_scope_fields( |
143 | 258 | review_data: dict, |
144 | 259 | current_reviewer: str | None, |
@@ -478,6 +593,7 @@ def derive_reviewer_response_state( |
478 | 593 | reason="no_reviewer_activity", |
479 | 594 | scope_fields=_current_scope_fields(review_data, current_reviewer, current_head, None), |
480 | 595 | anchor_timestamp=_initial_reviewer_anchor(review_data), |
| 596 | + current_head_sha=current_head, |
481 | 597 | reviewer_comment=reviewer_comment, |
482 | 598 | reviewer_review=reviewer_review, |
483 | 599 | current_cycle_reviewer_handoff=reviewer_handoff, |
|
0 commit comments