feat: show pull reviewer decisions (Closes #1334)
CI / lint (pull_request) Successful in 3m51s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Failing after 5m47s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-24 04:09:05 +00:00
1 parent f8ab6977ed
commit bd96c3caa3
7 files changed
+275 -10

No files matched your search

+63 -2
View File
@@ -2771,6 +2771,55 @@ async def pull_completion_detail(repository: str, number: int) -> dict:
}
def _normalize_reviewer_statuses(pull: dict, reviews: object, head_sha: str) -> list[dict]:
"""Return one bounded, current decision per reviewer without exposing raw review data."""
latest: dict[str, dict] = {}
for review in (reviews if isinstance(reviews, list) else [])[:100]:
if not isinstance(review, dict):
continue
user = review.get("user") if isinstance(review.get("user"), dict) else {}
login = user.get("login")
state = review.get("state")
commit_id = review.get("commit_id")
if (
not isinstance(login, str)
or not login
or state not in {"APPROVED", "REQUEST_CHANGES", "COMMENT"}
or not isinstance(commit_id, str)
or not commit_id
):
continue
previous = latest.get(login.casefold())
review_id = review.get("id") if isinstance(review.get("id"), int) else 0
if previous is None or review_id >= previous["id"]:
latest[login.casefold()] = {
"id": review_id, "login": login, "state": state, "commit_id": commit_id,
}
statuses: dict[str, dict] = {}
state_names = {
"APPROVED": "approved", "REQUEST_CHANGES": "changes_requested", "COMMENT": "commented",
}
for key, review in latest.items():
current = review["commit_id"] == head_sha
status = state_names[review["state"]] if current else "outdated"
statuses[key] = {
"login": review["login"],
"status": status,
"head_sha": review["commit_id"],
"blocking": status in {"changes_requested", "outdated"},
}
requested = pull.get("requested_reviewers") if isinstance(pull, dict) else []
for reviewer in (requested if isinstance(requested, list) else [])[:25]:
login = reviewer.get("login") if isinstance(reviewer, dict) else None
if isinstance(login, str) and login:
statuses[login.casefold()] = {
"login": login, "status": "waiting", "head_sha": head_sha, "blocking": True,
}
return sorted(statuses.values(), key=lambda item: item["login"].casefold())
async def pull_completion_review(repository: str, number: int) -> dict:
base = f"repos/{repository}/pulls/{number}"
pull = await fetch(base)
@@ -2780,12 +2829,13 @@ async def pull_completion_review(repository: str, number: int) -> dict:
head: dict = head_value if isinstance(head_value, dict) else {}
sha_value = head.get("sha")
sha = sha_value if isinstance(sha_value, str) else ""
files, status, diff_result = await asyncio.gather(
files, status, diff_result, reviews = await asyncio.gather(
fetch(f"{base}/files"),
fetch(f"repos/{repository}/commits/{sha}/status"),
fetch_text(
f"repos/{repository}/pulls/{number}.diff", REVIEW_DIFF_MAX_BYTES
),
fetch(f"{base}/reviews?limit=100"),
)
diff, diff_truncated = diff_result
previews = _diff_previews(diff, diff_truncated)
@@ -2799,6 +2849,7 @@ async def pull_completion_review(repository: str, number: int) -> dict:
"merged": pull.get("merged") is True,
"ci_state": status.get("state", "unknown") if isinstance(status, dict) else "unknown",
"checks": _normalize_commit_checks(status),
"reviewers": _normalize_reviewer_statuses(pull, reviews, sha),
"files": [
{
"filename": item.get("filename", ""),
@@ -2873,7 +2924,10 @@ async def pull_check_status(repository: str, number: int) -> dict:
head: dict = head_value if isinstance(head_value, dict) else {}
sha_value = head.get("sha")
sha = sha_value if isinstance(sha_value, str) else ""
status = await fetch(f"repos/{repository}/commits/{sha}/status")
status, reviews = await asyncio.gather(
fetch(f"repos/{repository}/commits/{sha}/status"),
fetch(f"{base}/reviews?limit=100"),
)
return {
"repository": repository,
"number": number,
@@ -2884,6 +2938,7 @@ async def pull_check_status(repository: str, number: int) -> dict:
"merged": pull.get("merged") is True,
"ci_state": status.get("state", "unknown") if isinstance(status, dict) else "unknown",
"checks": _normalize_commit_checks(status),
"reviewers": _normalize_reviewer_statuses(pull, reviews, sha),
}
@@ -2924,6 +2979,12 @@ async def merge_assigned_pull(
or ci_state != "success"
):
raise PullNotMergeableError("Pull request is not currently safe to merge")
reviews = await fetch(f"{base}/reviews?limit=100")
if any(
reviewer["blocking"]
for reviewer in _normalize_reviewer_statuses(pull, reviews, current_sha)
):
raise PullNotMergeableError("Pull request review is not currently resolved")
response = await _get_client().post(
f"/api/v1/{base}/merge",
headers=_auth(),