Skip to content

Commit 45e90ae

Browse files
MayorFajmattdhollowaytommaso-moro
authored
feat: add reviewers parameter to UpdatePullRequest (github#285)
* feat: add reviewers parameter to UpdatePullRequest and update tests * Update pullrequests.go * feat: enhance update pull request functionality with reviewers support * update README to clarify optional reviewers parameter in API documentation- go run ./cmd/github-mcp-server generate-docs * feat: enhance UpdatePullRequest to return early if no updates or reviewers are provided * Add updating draft state to `update_pull_request` tool (github#774) * initial impl of pull request draft state update * appease linter * update README * add nosec * fixed err return type for json marshalling * add gql test * Add support for org-level discussions in list_discussions tool (github#775) * make repo optional, and default to .github when not provided. improve tool description * autogen * update tests * small copy paste error fixes * refactor: streamline UpdatePullRequest logic and enhance test cases for reviewer updates * refactor: remove redundant draft update tests and streamline UpdatePullRequest logic * test: add unit tests for updating pull request draft state * refactor: simplify UpdatePullRequest tests by removing unused mock data --------- Co-authored-by: Matt Holloway <mattdholloway@github.com> Co-authored-by: Tommaso Moro <37270480+tommaso-moro@users.noreply.github.com>
1 parent 1c6171b commit 45e90ae

File tree

5 files changed

+138
-7
lines changed

5 files changed

+138
-7
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,7 @@ The following sets of tools are available (all are on by default):
766766
- `owner`: Repository owner (string, required)
767767
- `pullNumber`: Pull request number to update (number, required)
768768
- `repo`: Repository name (string, required)
769+
- `reviewers`: GitHub usernames to request reviews from (string[], optional)
769770
- `state`: New state (string, optional)
770771
- `title`: New title (string, optional)
771772

pkg/github/__toolsnaps__/update_pull_request.snap

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@
3434
"description": "Repository name",
3535
"type": "string"
3636
},
37+
"reviewers": {
38+
"description": "GitHub usernames to request reviews from",
39+
"items": {
40+
"type": "string"
41+
},
42+
"type": "array"
43+
},
3744
"state": {
3845
"description": "New state",
3946
"enum": [

pkg/github/discussions_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ var (
8787
"url": "https://github.com/owner/.github/discussions/4",
8888
"category": map[string]any{"name": "General"},
8989
},
90-
9190
}
9291

9392
// Ordered mock responses
@@ -190,7 +189,7 @@ var (
190189
"startCursor": "",
191190
"endCursor": "",
192191
},
193-
"totalCount": 4,
192+
"totalCount": 4,
194193
},
195194
},
196195
})

pkg/github/pullrequests.go

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,12 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra
241241
mcp.WithBoolean("maintainer_can_modify",
242242
mcp.Description("Allow maintainer edits"),
243243
),
244+
mcp.WithArray("reviewers",
245+
mcp.Description("GitHub usernames to request reviews from"),
246+
mcp.Items(map[string]interface{}{
247+
"type": "string",
248+
}),
249+
),
244250
),
245251
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
246252
owner, err := RequiredParam[string](request, "owner")
@@ -256,15 +262,17 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra
256262
return mcp.NewToolResultError(err.Error()), nil
257263
}
258264

265+
// Check if draft parameter is provided
259266
draftProvided := request.GetArguments()["draft"] != nil
260267
var draftValue bool
261268
if draftProvided {
262269
draftValue, err = OptionalParam[bool](request, "draft")
263270
if err != nil {
264-
return nil, err
271+
return mcp.NewToolResultError(err.Error()), nil
265272
}
266273
}
267274

275+
// Build the update struct only with provided fields
268276
update := &github.PullRequest{}
269277
restUpdateNeeded := false
270278

@@ -303,10 +311,18 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra
303311
restUpdateNeeded = true
304312
}
305313

306-
if !restUpdateNeeded && !draftProvided {
314+
// Handle reviewers separately
315+
reviewers, err := OptionalStringArrayParam(request, "reviewers")
316+
if err != nil {
317+
return mcp.NewToolResultError(err.Error()), nil
318+
}
319+
320+
// If no updates, no draft change, and no reviewers, return error early
321+
if !restUpdateNeeded && !draftProvided && len(reviewers) == 0 {
307322
return mcp.NewToolResultError("No update parameters provided."), nil
308323
}
309324

325+
// Handle REST API updates (title, body, state, base, maintainer_can_modify)
310326
if restUpdateNeeded {
311327
client, err := getClient(ctx)
312328
if err != nil {
@@ -332,6 +348,7 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra
332348
}
333349
}
334350

351+
// Handle draft status changes using GraphQL
335352
if draftProvided {
336353
gqlClient, err := getGQLClient(ctx)
337354
if err != nil {
@@ -397,6 +414,41 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra
397414
}
398415
}
399416

417+
// Handle reviewer requests
418+
if len(reviewers) > 0 {
419+
client, err := getClient(ctx)
420+
if err != nil {
421+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
422+
}
423+
424+
reviewersRequest := github.ReviewersRequest{
425+
Reviewers: reviewers,
426+
}
427+
428+
_, resp, err := client.PullRequests.RequestReviewers(ctx, owner, repo, pullNumber, reviewersRequest)
429+
if err != nil {
430+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
431+
"failed to request reviewers",
432+
resp,
433+
err,
434+
), nil
435+
}
436+
defer func() {
437+
if resp != nil && resp.Body != nil {
438+
_ = resp.Body.Close()
439+
}
440+
}()
441+
442+
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
443+
body, err := io.ReadAll(resp.Body)
444+
if err != nil {
445+
return nil, fmt.Errorf("failed to read response body: %w", err)
446+
}
447+
return mcp.NewToolResultError(fmt.Sprintf("failed to request reviewers: %s", string(body))), nil
448+
}
449+
}
450+
451+
// Get the final state of the PR to return
400452
client, err := getClient(ctx)
401453
if err != nil {
402454
return nil, err

pkg/github/pullrequests_test.go

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ func Test_UpdatePullRequest(t *testing.T) {
151151
assert.Contains(t, tool.InputSchema.Properties, "state")
152152
assert.Contains(t, tool.InputSchema.Properties, "base")
153153
assert.Contains(t, tool.InputSchema.Properties, "maintainer_can_modify")
154+
assert.Contains(t, tool.InputSchema.Properties, "reviewers")
154155
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber"})
155156

156157
// Setup mock PR for success case
@@ -173,6 +174,17 @@ func Test_UpdatePullRequest(t *testing.T) {
173174
State: github.Ptr("closed"), // State updated
174175
}
175176

177+
// Mock PR for when there are no updates but we still need a response
178+
mockPRWithReviewers := &github.PullRequest{
179+
Number: github.Ptr(42),
180+
Title: github.Ptr("Test PR"),
181+
State: github.Ptr("open"),
182+
RequestedReviewers: []*github.User{
183+
{Login: github.Ptr("reviewer1")},
184+
{Login: github.Ptr("reviewer2")},
185+
},
186+
}
187+
176188
tests := []struct {
177189
name string
178190
mockedClient *http.Client
@@ -238,6 +250,28 @@ func Test_UpdatePullRequest(t *testing.T) {
238250
expectError: false,
239251
expectedPR: mockClosedPR,
240252
},
253+
{
254+
name: "successful PR update with reviewers",
255+
mockedClient: mock.NewMockedHTTPClient(
256+
// Mock for RequestReviewers call, returning the PR with reviewers
257+
mock.WithRequestMatch(
258+
mock.PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber,
259+
mockPRWithReviewers,
260+
),
261+
mock.WithRequestMatch(
262+
mock.GetReposPullsByOwnerByRepoByPullNumber,
263+
mockPRWithReviewers,
264+
),
265+
),
266+
requestArgs: map[string]interface{}{
267+
"owner": "owner",
268+
"repo": "repo",
269+
"pullNumber": float64(42),
270+
"reviewers": []interface{}{"reviewer1", "reviewer2"},
271+
},
272+
expectError: false,
273+
expectedPR: mockPRWithReviewers,
274+
},
241275
{
242276
name: "successful PR update (title only)",
243277
mockedClient: mock.NewMockedHTTPClient(
@@ -295,6 +329,27 @@ func Test_UpdatePullRequest(t *testing.T) {
295329
expectError: true,
296330
expectedErrMsg: "failed to update pull request",
297331
},
332+
{
333+
name: "request reviewers fails",
334+
mockedClient: mock.NewMockedHTTPClient(
335+
// Then reviewer request fails
336+
mock.WithRequestMatchHandler(
337+
mock.PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber,
338+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
339+
w.WriteHeader(http.StatusUnprocessableEntity)
340+
_, _ = w.Write([]byte(`{"message": "Invalid reviewers"}`))
341+
}),
342+
),
343+
),
344+
requestArgs: map[string]interface{}{
345+
"owner": "owner",
346+
"repo": "repo",
347+
"pullNumber": float64(42),
348+
"reviewers": []interface{}{"invalid-user"},
349+
},
350+
expectError: true,
351+
expectedErrMsg: "failed to request reviewers",
352+
},
298353
}
299354

300355
for _, tc := range tests {
@@ -347,6 +402,26 @@ func Test_UpdatePullRequest(t *testing.T) {
347402
if tc.expectedPR.MaintainerCanModify != nil {
348403
assert.Equal(t, *tc.expectedPR.MaintainerCanModify, *returnedPR.MaintainerCanModify)
349404
}
405+
406+
// Check reviewers if they exist in the expected PR
407+
if len(tc.expectedPR.RequestedReviewers) > 0 {
408+
assert.NotNil(t, returnedPR.RequestedReviewers)
409+
assert.Equal(t, len(tc.expectedPR.RequestedReviewers), len(returnedPR.RequestedReviewers))
410+
411+
// Create maps of reviewer logins for easy comparison
412+
expectedReviewers := make(map[string]bool)
413+
for _, reviewer := range tc.expectedPR.RequestedReviewers {
414+
expectedReviewers[*reviewer.Login] = true
415+
}
416+
417+
actualReviewers := make(map[string]bool)
418+
for _, reviewer := range returnedPR.RequestedReviewers {
419+
actualReviewers[*reviewer.Login] = true
420+
}
421+
422+
// Compare the maps
423+
assert.Equal(t, expectedReviewers, actualReviewers)
424+
}
350425
})
351426
}
352427
}
@@ -529,9 +604,6 @@ func Test_UpdatePullRequest_Draft(t *testing.T) {
529604
err = json.Unmarshal([]byte(textContent.Text), &returnedPR)
530605
require.NoError(t, err)
531606
assert.Equal(t, *tc.expectedPR.Number, *returnedPR.Number)
532-
if tc.expectedPR.Draft != nil {
533-
assert.Equal(t, *tc.expectedPR.Draft, *returnedPR.Draft)
534-
}
535607
})
536608
}
537609
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy