Skip to content

Commit 3234755

Browse files
committed
feat: add reviewers parameter to UpdatePullRequest and update tests
1 parent 614f226 commit 3234755

File tree

3 files changed

+190
-12
lines changed

3 files changed

+190
-12
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
327327
- `state`: New state ('open' or 'closed') (string, optional)
328328
- `base`: New base branch name (string, optional)
329329
- `maintainer_can_modify`: Allow maintainer edits (boolean, optional)
330+
- `reviewers`: GitHub usernames to request reviews from (string[], optional)
330331

331332
### Repositories
332333

pkg/github/pullrequests.go

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ func UpdatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu
103103
mcp.WithBoolean("maintainer_can_modify",
104104
mcp.Description("Allow maintainer edits"),
105105
),
106+
mcp.WithArray("reviewers",
107+
mcp.Description("GitHub usernames to request reviews from"),
108+
mcp.Items(map[string]interface{}{
109+
"type": "string",
110+
}),
111+
),
106112
),
107113
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
108114
owner, err := requiredParam[string](request, "owner")
@@ -157,26 +163,101 @@ func UpdatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu
157163
updateNeeded = true
158164
}
159165

160-
if !updateNeeded {
161-
return mcp.NewToolResultError("No update parameters provided."), nil
166+
// Handle reviewers separately
167+
var reviewers []string
168+
if reviewersArr, ok := request.Params.Arguments["reviewers"].([]interface{}); ok && len(reviewersArr) > 0 {
169+
for _, reviewer := range reviewersArr {
170+
if reviewerStr, ok := reviewer.(string); ok {
171+
reviewers = append(reviewers, reviewerStr)
172+
}
173+
}
162174
}
163175

176+
// Create the GitHub client
164177
client, err := getClient(ctx)
165178
if err != nil {
166179
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
167180
}
168-
pr, resp, err := client.PullRequests.Edit(ctx, owner, repo, pullNumber, update)
169-
if err != nil {
170-
return nil, fmt.Errorf("failed to update pull request: %w", err)
181+
182+
var pr *github.PullRequest
183+
var resp *http.Response
184+
185+
// First, update the PR if needed
186+
if updateNeeded {
187+
var ghResp *github.Response
188+
pr, ghResp, err = client.PullRequests.Edit(ctx, owner, repo, pullNumber, update)
189+
if err != nil {
190+
return nil, fmt.Errorf("failed to update pull request: %w", err)
191+
}
192+
resp = ghResp.Response
193+
defer func() {
194+
if resp != nil && resp.Body != nil {
195+
_ = resp.Body.Close()
196+
}
197+
}()
198+
199+
if resp.StatusCode != http.StatusOK {
200+
body, err := io.ReadAll(resp.Body)
201+
if err != nil {
202+
return nil, fmt.Errorf("failed to read response body: %w", err)
203+
}
204+
return mcp.NewToolResultError(fmt.Sprintf("failed to update pull request: %s", string(body))), nil
205+
}
206+
} else {
207+
// If no update needed, just get the current PR
208+
var ghResp *github.Response
209+
pr, ghResp, err = client.PullRequests.Get(ctx, owner, repo, pullNumber)
210+
if err != nil {
211+
return nil, fmt.Errorf("failed to get pull request: %w", err)
212+
}
213+
resp = ghResp.Response
214+
defer func() {
215+
if resp != nil && resp.Body != nil {
216+
_ = resp.Body.Close()
217+
}
218+
}()
219+
220+
if resp.StatusCode != http.StatusOK {
221+
body, err := io.ReadAll(resp.Body)
222+
if err != nil {
223+
return nil, fmt.Errorf("failed to read response body: %w", err)
224+
}
225+
return mcp.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil
226+
}
171227
}
172-
defer func() { _ = resp.Body.Close() }()
173228

174-
if resp.StatusCode != http.StatusOK {
175-
body, err := io.ReadAll(resp.Body)
229+
// Add reviewers if specified
230+
if len(reviewers) > 0 {
231+
reviewersRequest := github.ReviewersRequest{
232+
Reviewers: reviewers,
233+
}
234+
235+
// Use the direct result of RequestReviewers which includes the requested reviewers
236+
updatedPR, resp, err := client.PullRequests.RequestReviewers(ctx, owner, repo, pullNumber, reviewersRequest)
176237
if err != nil {
177-
return nil, fmt.Errorf("failed to read response body: %w", err)
238+
return nil, fmt.Errorf("failed to request reviewers: %w", err)
178239
}
179-
return mcp.NewToolResultError(fmt.Sprintf("failed to update pull request: %s", string(body))), nil
240+
defer func() {
241+
if resp != nil && resp.Body != nil {
242+
_ = resp.Body.Close()
243+
}
244+
}()
245+
246+
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
247+
body, err := io.ReadAll(resp.Body)
248+
if err != nil {
249+
return nil, fmt.Errorf("failed to read response body: %w", err)
250+
}
251+
return mcp.NewToolResultError(fmt.Sprintf("failed to request reviewers: %s", string(body))), nil
252+
}
253+
254+
// Use the updated PR with reviewers
255+
pr = updatedPR
256+
}
257+
258+
// If no updates and no reviewers, return error
259+
if !updateNeeded && len(reviewers) == 0 {
260+
return mcp.NewToolResultError("No update parameters provided"), nil
180261
}
181262

182263
r, err := json.Marshal(pr)

pkg/github/pullrequests_test.go

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ func Test_UpdatePullRequest(t *testing.T) {
141141
assert.Contains(t, tool.InputSchema.Properties, "state")
142142
assert.Contains(t, tool.InputSchema.Properties, "base")
143143
assert.Contains(t, tool.InputSchema.Properties, "maintainer_can_modify")
144+
assert.Contains(t, tool.InputSchema.Properties, "reviewers")
144145
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber"})
145146

146147
// Setup mock PR for success case
@@ -162,6 +163,23 @@ func Test_UpdatePullRequest(t *testing.T) {
162163
State: github.Ptr("closed"), // State updated
163164
}
164165

166+
// Mock PR for when there are no updates but we still need a response
167+
mockNoUpdatePR := &github.PullRequest{
168+
Number: github.Ptr(42),
169+
Title: github.Ptr("Test PR"),
170+
State: github.Ptr("open"),
171+
}
172+
173+
mockPRWithReviewers := &github.PullRequest{
174+
Number: github.Ptr(42),
175+
Title: github.Ptr("Test PR"),
176+
State: github.Ptr("open"),
177+
RequestedReviewers: []*github.User{
178+
{Login: github.Ptr("reviewer1")},
179+
{Login: github.Ptr("reviewer2")},
180+
},
181+
}
182+
165183
tests := []struct {
166184
name string
167185
mockedClient *http.Client
@@ -220,8 +238,40 @@ func Test_UpdatePullRequest(t *testing.T) {
220238
expectedPR: mockClosedPR,
221239
},
222240
{
223-
name: "no update parameters provided",
224-
mockedClient: mock.NewMockedHTTPClient(), // No API call expected
241+
name: "successful PR update with reviewers",
242+
mockedClient: mock.NewMockedHTTPClient(
243+
mock.WithRequestMatch(
244+
mock.GetReposPullsByOwnerByRepoByPullNumber,
245+
&github.PullRequest{
246+
Number: github.Ptr(42),
247+
Title: github.Ptr("Test PR"),
248+
State: github.Ptr("open"),
249+
},
250+
),
251+
// Mock for RequestReviewers call, returning the PR with reviewers
252+
mock.WithRequestMatch(
253+
mock.PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber,
254+
mockPRWithReviewers,
255+
),
256+
),
257+
requestArgs: map[string]interface{}{
258+
"owner": "owner",
259+
"repo": "repo",
260+
"pullNumber": float64(42),
261+
"reviewers": []interface{}{"reviewer1", "reviewer2"},
262+
},
263+
expectError: false,
264+
expectedPR: mockPRWithReviewers,
265+
},
266+
{
267+
name: "no update parameters provided",
268+
mockedClient: mock.NewMockedHTTPClient(
269+
// Mock a response for the GET PR request in case of no updates
270+
mock.WithRequestMatch(
271+
mock.GetReposPullsByOwnerByRepoByPullNumber,
272+
mockNoUpdatePR,
273+
),
274+
),
225275
requestArgs: map[string]interface{}{
226276
"owner": "owner",
227277
"repo": "repo",
@@ -251,6 +301,32 @@ func Test_UpdatePullRequest(t *testing.T) {
251301
expectError: true,
252302
expectedErrMsg: "failed to update pull request",
253303
},
304+
{
305+
name: "request reviewers fails",
306+
mockedClient: mock.NewMockedHTTPClient(
307+
// First it gets the PR (no fields to update)
308+
mock.WithRequestMatch(
309+
mock.GetReposPullsByOwnerByRepoByPullNumber,
310+
mockNoUpdatePR,
311+
),
312+
// Then reviewer request fails
313+
mock.WithRequestMatchHandler(
314+
mock.PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber,
315+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
316+
w.WriteHeader(http.StatusUnprocessableEntity)
317+
_, _ = w.Write([]byte(`{"message": "Invalid reviewers"}`))
318+
}),
319+
),
320+
),
321+
requestArgs: map[string]interface{}{
322+
"owner": "owner",
323+
"repo": "repo",
324+
"pullNumber": float64(42),
325+
"reviewers": []interface{}{"invalid-user"},
326+
},
327+
expectError: true,
328+
expectedErrMsg: "failed to request reviewers",
329+
},
254330
}
255331

256332
for _, tc := range tests {
@@ -304,6 +380,26 @@ func Test_UpdatePullRequest(t *testing.T) {
304380
if tc.expectedPR.MaintainerCanModify != nil {
305381
assert.Equal(t, *tc.expectedPR.MaintainerCanModify, *returnedPR.MaintainerCanModify)
306382
}
383+
384+
// Check reviewers if they exist in the expected PR
385+
if tc.expectedPR.RequestedReviewers != nil && len(tc.expectedPR.RequestedReviewers) > 0 {
386+
assert.NotNil(t, returnedPR.RequestedReviewers)
387+
assert.Equal(t, len(tc.expectedPR.RequestedReviewers), len(returnedPR.RequestedReviewers))
388+
389+
// Create maps of reviewer logins for easy comparison
390+
expectedReviewers := make(map[string]bool)
391+
for _, reviewer := range tc.expectedPR.RequestedReviewers {
392+
expectedReviewers[*reviewer.Login] = true
393+
}
394+
395+
actualReviewers := make(map[string]bool)
396+
for _, reviewer := range returnedPR.RequestedReviewers {
397+
actualReviewers[*reviewer.Login] = true
398+
}
399+
400+
// Compare the maps
401+
assert.Equal(t, expectedReviewers, actualReviewers)
402+
}
307403
})
308404
}
309405
}

0 commit comments

Comments
 (0)