feat: resolve old inline comments when superseding review
CI / test (pull_request) Successful in 14s
CI / review (gpt-4.1, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 21s
CI / review (gpt-5, security, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 42s
CI / review (gpt-5, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 1m1s

Closes #27

After superseding an old review, resolves all its inline comments via
POST /pulls/comments/{id}/resolve. This clears unresolved conversation
markers from the PR timeline/diff view.

New API methods:
- ListReviewComments: GET /repos/.../pulls/{n}/reviews/{id}/comments
- ResolveComment: POST /repos/.../pulls/comments/{id}/resolve

Failures are non-fatal (debug log) — the review still posts and
supersedes even if resolution fails.
This commit is contained in:
Rodin
2026-05-02 12:07:11 -07:00
parent cd601bdcf4
commit 17027c1fa3
2 changed files with 68 additions and 0 deletions
+21
View File
@@ -362,6 +362,27 @@ func main() {
} else {
slog.Info("marked old review as superseded", "old_state", existingReview.State, "new_review_id", posted.ID, "pr", prNumber)
}
// Resolve old review's inline comments (clears unresolved conversations in PR timeline)
oldComments, err := giteaClient.ListReviewComments(ctx, owner, repoName, prNumber, existingReview.ID)
if err != nil {
slog.Warn("could not list old review comments for resolution", "review_id", existingReview.ID, "error", err)
} else {
resolved := 0
for _, c := range oldComments {
if c.ID == 0 {
continue
}
if err := giteaClient.ResolveComment(ctx, owner, repoName, c.ID); err != nil {
slog.Debug("could not resolve inline comment", "comment_id", c.ID, "error", err)
} else {
resolved++
}
}
if resolved > 0 {
slog.Info("resolved old inline comments", "count", resolved, "pr", prNumber)
}
}
}
}
+47
View File
@@ -82,6 +82,7 @@ type ChangedFile struct {
// ReviewComment represents an inline comment to attach to a review.
type ReviewComment struct {
ID int64 `json:"id,omitempty"`
Path string `json:"path"`
NewPosition int64 `json:"new_position"`
Body string `json:"body"`
@@ -460,3 +461,49 @@ func (c *Client) EditComment(ctx context.Context, owner, repo string, commentID
}
return nil
}
// ListReviewComments returns the inline comments attached to a specific review.
func (c *Client) ListReviewComments(ctx context.Context, owner, repo string, prNumber int, reviewID int64) ([]ReviewComment, error) {
reqURL := fmt.Sprintf("%s/api/v1/repos/%s/%s/pulls/%d/reviews/%d/comments",
c.baseURL,
url.PathEscape(owner),
url.PathEscape(repo),
prNumber,
reviewID)
body, err := c.doGet(ctx, reqURL)
if err != nil {
return nil, fmt.Errorf("list review comments: %w", err)
}
var comments []ReviewComment
if err := json.Unmarshal(body, &comments); err != nil {
return nil, fmt.Errorf("parse review comments: %w", err)
}
return comments, nil
}
// ResolveComment marks an inline review comment as resolved.
func (c *Client) ResolveComment(ctx context.Context, owner, repo string, commentID int64) error {
reqURL := fmt.Sprintf("%s/api/v1/repos/%s/%s/pulls/comments/%d/resolve",
c.baseURL,
url.PathEscape(owner),
url.PathEscape(repo),
commentID)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, nil)
if err != nil {
return fmt.Errorf("create resolve request: %w", err)
}
req.Header.Set("Authorization", "token "+c.token)
resp, err := c.http.Do(req)
if err != nil {
return fmt.Errorf("resolve comment: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("resolve comment failed (status %d): %s", resp.StatusCode, body)
}
return nil
}