fix(robomp): validate PR review comment anchors against the diff before submitting
A single comment on a line outside a diff hunk 422s the whole review on
GitHub, which the 422/500 fallback (ported here from the Forgejo work,
including payload shaping and commit_id) then degraded into plain issue
comments. Now we fetch /pulls/{n}/files, compute per-hunk anchorable line
sets, drop non-anchorable comments, fold them into the review summary, and
only fall back to issue comments for genuine 422/500s that survive
filtering. Files-fetch failure fails open (submit unfiltered).
This commit is contained in:
@@ -77,6 +77,7 @@ class PullRequestFileInfo:
|
||||
status: str
|
||||
additions: int
|
||||
deletions: int
|
||||
patch: str = ""
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
@@ -179,7 +180,13 @@ def _parse_retry_after(resp: httpx.Response) -> float | None:
|
||||
class GitHubClient:
|
||||
"""Async + sync facades over a small slice of the GitHub REST API."""
|
||||
|
||||
def __init__(self, token: str, *, transport: httpx.BaseTransport | None = None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
token: str,
|
||||
*,
|
||||
transport: httpx.BaseTransport | None = None,
|
||||
platform: str = "github",
|
||||
) -> None:
|
||||
self._token = token
|
||||
self._headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
@@ -188,6 +195,7 @@ class GitHubClient:
|
||||
"User-Agent": "robomp/0.1",
|
||||
}
|
||||
self._transport = transport
|
||||
self._platform = platform
|
||||
|
||||
def _client(self) -> httpx.Client:
|
||||
return httpx.Client(
|
||||
@@ -581,6 +589,26 @@ class GitHubClient:
|
||||
f"/repos/{repo}/issues/{number}/labels/{encoded}",
|
||||
)
|
||||
|
||||
def _review_comments_payload(self, comments: list[Mapping[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""Adapt canonical host-tool comment shape to the wire schema for this platform.
|
||||
|
||||
GitHub keeps line/side/start_line/start_side; Forgejo/Gitea only reads
|
||||
path/body/new_position (+old_position), so github-only keys are dropped
|
||||
and `line` is mapped to `new_position` for RIGHT-side comments or
|
||||
`old_position` for LEFT-side (removed-line) comments.
|
||||
"""
|
||||
if self._platform != "forgejo":
|
||||
return [dict(c) for c in comments]
|
||||
payload: list[dict[str, Any]] = []
|
||||
for c in comments:
|
||||
entry: dict[str, Any] = {"path": c["path"], "body": c["body"]}
|
||||
if str(c.get("side", "RIGHT")).upper() == "LEFT":
|
||||
entry["old_position"] = c["line"]
|
||||
else:
|
||||
entry["new_position"] = c["line"]
|
||||
payload.append(entry)
|
||||
return payload
|
||||
|
||||
async def submit_pr_review(
|
||||
self,
|
||||
*,
|
||||
@@ -589,12 +617,12 @@ class GitHubClient:
|
||||
body: str,
|
||||
event: str,
|
||||
comments: list[Mapping[str, Any]],
|
||||
commit_id: str | None = None,
|
||||
) -> PullRequestReviewInfo:
|
||||
data = await self.request(
|
||||
"POST",
|
||||
f"/repos/{repo}/pulls/{pr_number}/reviews",
|
||||
json={"body": body, "event": event, "comments": comments},
|
||||
)
|
||||
payload: dict[str, Any] = {"body": body, "event": event, "comments": self._review_comments_payload(comments)}
|
||||
if commit_id:
|
||||
payload["commit_id"] = commit_id
|
||||
data = await self.request("POST", f"/repos/{repo}/pulls/{pr_number}/reviews", json=payload)
|
||||
return _pr_review_from_payload(data)
|
||||
|
||||
async def add_assignees(self, repo: str, number: int, assignees: list[str]) -> None:
|
||||
@@ -750,6 +778,7 @@ def _pr_file_from_payload(data: Mapping[str, Any]) -> PullRequestFileInfo:
|
||||
status=str(data.get("status") or ""),
|
||||
additions=int(data.get("additions") or 0),
|
||||
deletions=int(data.get("deletions") or 0),
|
||||
patch=str(data.get("patch") or ""),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user