feat(vcs): add CommitID to ReviewRequest (#115)
CI / test (pull_request) Successful in 20s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 27s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 49s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m15s
CI / test (pull_request) Successful in 20s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 27s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 49s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m15s
Add CommitID string field to vcs.ReviewRequest so both platform adapters can thread the commit anchor through the abstraction layer. Changes: - vcs/types.go: Add CommitID field with json tag commit_id,omitempty - gitea/client.go: Re-add commitID parameter to PostReview (was removed during PR #112 refactoring) - gitea/adapter.go: Forward req.CommitID to underlying client - github/review.go: Use req.CommitID as primary anchor, fall back to comment-derived CommitID when empty, reject on conflict - cmd/review-bot/main.go: Set ReviewRequest.CommitID = evaluatedSHA Fixes #115
This commit is contained in:
+9
-6
@@ -82,21 +82,24 @@ func translateGitHubReviewState(state string) string {
|
||||
// (via the omitempty tag on postReviewRequest.Comments).
|
||||
//
|
||||
// The GitHub API accepts a single commit_id per review submission. PostReview
|
||||
// extracts it from the first comment with a non-empty CommitID. If any subsequent
|
||||
// comment specifies a different CommitID, PostReview returns ErrConflictingCommitIDs.
|
||||
// uses req.CommitID as the primary anchor when set. If req.CommitID is empty,
|
||||
// it falls back to extracting commit_id from the first comment with a non-empty
|
||||
// CommitID. If any comment specifies a CommitID that conflicts with the
|
||||
// resolved value, PostReview returns ErrConflictingCommitIDs.
|
||||
// Comments with an empty CommitID are allowed and inherit the review-level value.
|
||||
func (c *Client) PostReview(ctx context.Context, owner, repo string, number int, req vcs.ReviewRequest) (*vcs.Review, error) {
|
||||
reqURL := fmt.Sprintf("%s/repos/%s/%s/pulls/%d/reviews",
|
||||
c.baseURL, url.PathEscape(owner), url.PathEscape(repo), number)
|
||||
|
||||
payload := postReviewRequest{
|
||||
Body: req.Body,
|
||||
Event: string(req.Event),
|
||||
CommitID: req.CommitID,
|
||||
Body: req.Body,
|
||||
Event: string(req.Event),
|
||||
}
|
||||
|
||||
// Build the payload in one pass. The GitHub API accepts a single commit_id
|
||||
// per review; we extract it from the first comment that supplies one and
|
||||
// reject the request if any other comment disagrees.
|
||||
// per review. We use req.CommitID as the primary anchor; if any comment
|
||||
// specifies a different non-empty CommitID, we reject the request.
|
||||
for _, comment := range req.Comments {
|
||||
if comment.CommitID != "" {
|
||||
if payload.CommitID == "" {
|
||||
|
||||
@@ -389,3 +389,125 @@ func TestPostReview_ConflictingCommitIDs(t *testing.T) {
|
||||
t.Errorf("expected ErrConflictingCommitIDs, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostReview_CommitIDFromRequest(t *testing.T) {
|
||||
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
var req postReviewRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
t.Fatalf("unmarshal request: %v", err)
|
||||
}
|
||||
if req.CommitID != "req-level-sha" {
|
||||
t.Errorf("expected commit_id %q, got %q", "req-level-sha", req.CommitID)
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"id": 200,
|
||||
"body": "ok",
|
||||
"state": "APPROVED",
|
||||
"commit_id": "req-level-sha",
|
||||
"user": map[string]string{"login": "bot"},
|
||||
})
|
||||
})
|
||||
|
||||
// CommitID set at request level, no comments
|
||||
review, err := c.PostReview(context.Background(), "owner", "repo", 5, vcs.ReviewRequest{
|
||||
Body: "ok",
|
||||
Event: vcs.ReviewEventApprove,
|
||||
CommitID: "req-level-sha",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if review.CommitID != "req-level-sha" {
|
||||
t.Errorf("expected commit_id %q, got %q", "req-level-sha", review.CommitID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostReview_CommitIDFallbackToComment(t *testing.T) {
|
||||
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
var req postReviewRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
t.Fatalf("unmarshal request: %v", err)
|
||||
}
|
||||
// When req.CommitID is empty, should fall back to comment's CommitID
|
||||
if req.CommitID != "comment-sha" {
|
||||
t.Errorf("expected commit_id %q (from comment fallback), got %q", "comment-sha", req.CommitID)
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"id": 201,
|
||||
"body": "ok",
|
||||
"state": "APPROVED",
|
||||
"commit_id": "comment-sha",
|
||||
"user": map[string]string{"login": "bot"},
|
||||
})
|
||||
})
|
||||
|
||||
review, err := c.PostReview(context.Background(), "owner", "repo", 5, vcs.ReviewRequest{
|
||||
Body: "ok",
|
||||
Event: vcs.ReviewEventApprove,
|
||||
// CommitID intentionally empty — should fall back to comment
|
||||
Comments: []vcs.ReviewComment{
|
||||
{Path: "main.go", Position: 1, CommitID: "comment-sha", Body: "nit"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if review.CommitID != "comment-sha" {
|
||||
t.Errorf("expected commit_id %q, got %q", "comment-sha", review.CommitID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostReview_CommitIDConflictBetweenRequestAndComment(t *testing.T) {
|
||||
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("request should not have been sent")
|
||||
})
|
||||
|
||||
_, err := c.PostReview(context.Background(), "owner", "repo", 5, vcs.ReviewRequest{
|
||||
Body: "ok",
|
||||
Event: vcs.ReviewEventApprove,
|
||||
CommitID: "req-sha",
|
||||
Comments: []vcs.ReviewComment{
|
||||
{Path: "main.go", Position: 1, CommitID: "different-sha", Body: "nit"},
|
||||
},
|
||||
})
|
||||
if !errors.Is(err, ErrConflictingCommitIDs) {
|
||||
t.Errorf("expected ErrConflictingCommitIDs, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostReview_CommitIDMatchBetweenRequestAndComment(t *testing.T) {
|
||||
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
var req postReviewRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
t.Fatalf("unmarshal request: %v", err)
|
||||
}
|
||||
if req.CommitID != "same-sha" {
|
||||
t.Errorf("expected commit_id %q, got %q", "same-sha", req.CommitID)
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"id": 202,
|
||||
"body": "ok",
|
||||
"state": "APPROVED",
|
||||
"commit_id": "same-sha",
|
||||
"user": map[string]string{"login": "bot"},
|
||||
})
|
||||
})
|
||||
|
||||
_, err := c.PostReview(context.Background(), "owner", "repo", 5, vcs.ReviewRequest{
|
||||
Body: "ok",
|
||||
Event: vcs.ReviewEventApprove,
|
||||
CommitID: "same-sha",
|
||||
Comments: []vcs.ReviewComment{
|
||||
{Path: "main.go", Position: 1, CommitID: "same-sha", Body: "nit"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user