Skip to content

feat: add reviewers parameter to UpdatePullRequest #285

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Jul 31, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3234755
feat: add reviewers parameter to UpdatePullRequest and update tests
MayorFaj Apr 15, 2025
9db748b
Merge branch 'main' into feat/259/assign-reviewers
MayorFaj Apr 17, 2025
a84ede9
Merge branch 'main' into feat/259/assign-reviewers
MayorFaj Apr 17, 2025
276ac8d
Merge branch 'main' into feat/259/assign-reviewers
MayorFaj Apr 18, 2025
53d0833
Merge branch 'main' into feat/259/assign-reviewers
MayorFaj Apr 22, 2025
c4de80b
Merge branch 'main' into feat/259/assign-reviewers
MayorFaj Apr 22, 2025
0e51a09
Merge branch 'main' into feat/259/assign-reviewers
MayorFaj Apr 23, 2025
b9d2e28
Merge branch 'main' into feat/259/assign-reviewers
MayorFaj Apr 30, 2025
9ac7250
Merge branch 'main' into feat/259/assign-reviewers
MayorFaj May 5, 2025
71dcedf
Merge branch 'main' into feat/259/assign-reviewers
MayorFaj Jun 25, 2025
5c85a09
Update pullrequests.go
MayorFaj Jun 25, 2025
b09f589
feat: enhance update pull request functionality with reviewers support
MayorFaj Jun 25, 2025
53e2708
Merge branch 'main' into feat/259/assign-reviewers
MayorFaj Jun 26, 2025
7676eae
Merge branch 'main' into feat/259/assign-reviewers
MayorFaj Jun 28, 2025
9209f5c
Merge branch 'main' into feat/259/assign-reviewers
MayorFaj Jun 30, 2025
6f21c3f
update README to clarify optional reviewers parameter in API document…
MayorFaj Jun 30, 2025
8a6cabf
Merge branch 'main' into feat/259/assign-reviewers
MayorFaj Jul 1, 2025
4e28ee7
Merge branch 'main' into feat/259/assign-reviewers
MayorFaj Jul 2, 2025
432907e
Merge branch 'main' into feat/259/assign-reviewers
MayorFaj Jul 3, 2025
0474365
Merge branch 'main' into feat/259/assign-reviewers
MayorFaj Jul 9, 2025
20bfead
Merge branch 'main' into feat/259/assign-reviewers
MayorFaj Jul 16, 2025
5650e1d
Merge branch 'main' into feat/259/assign-reviewers
MayorFaj Jul 19, 2025
0674183
Merge branch 'main' into feat/259/assign-reviewers
MayorFaj Jul 25, 2025
046f994
feat: enhance UpdatePullRequest to return early if no updates or revi…
MayorFaj Jul 25, 2025
90eb11a
Add updating draft state to `update_pull_request` tool (#774)
mattdholloway Jul 29, 2025
033f613
Add support for org-level discussions in list_discussions tool (#775)
tommaso-moro Jul 29, 2025
94cef70
refactor: streamline UpdatePullRequest logic and enhance test cases f…
MayorFaj Jul 29, 2025
d89e522
Merge branch 'main' into feat/259/assign-reviewers
MayorFaj Jul 29, 2025
5ea322b
refactor: remove redundant draft update tests and streamline UpdatePu…
MayorFaj Jul 29, 2025
be359ea
test: add unit tests for updating pull request draft state
MayorFaj Jul 29, 2025
1934e2a
Merge branch 'main' into feat/259/assign-reviewers
MayorFaj Jul 30, 2025
c1b9a46
refactor: simplify UpdatePullRequest tests by removing unused mock data
MayorFaj Jul 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
- `state`: New state ('open' or 'closed') (string, optional)
- `base`: New base branch name (string, optional)
- `maintainer_can_modify`: Allow maintainer edits (boolean, optional)
- `reviewers`: GitHub usernames to request reviews from (string[], optional)

### Repositories

Expand Down
101 changes: 91 additions & 10 deletions pkg/github/pullrequests.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ func UpdatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu
mcp.WithBoolean("maintainer_can_modify",
mcp.Description("Allow maintainer edits"),
),
mcp.WithArray("reviewers",
mcp.Description("GitHub usernames to request reviews from"),
mcp.Items(map[string]interface{}{
"type": "string",
}),
),
),
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
owner, err := requiredParam[string](request, "owner")
Expand Down Expand Up @@ -157,26 +163,101 @@ func UpdatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu
updateNeeded = true
}

if !updateNeeded {
return mcp.NewToolResultError("No update parameters provided."), nil
// Handle reviewers separately
var reviewers []string
if reviewersArr, ok := request.Params.Arguments["reviewers"].([]interface{}); ok && len(reviewersArr) > 0 {
for _, reviewer := range reviewersArr {
if reviewerStr, ok := reviewer.(string); ok {
reviewers = append(reviewers, reviewerStr)
}
}
}

// Create the GitHub client
client, err := getClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
}
pr, resp, err := client.PullRequests.Edit(ctx, owner, repo, pullNumber, update)
if err != nil {
return nil, fmt.Errorf("failed to update pull request: %w", err)

var pr *github.PullRequest
var resp *http.Response

// First, update the PR if needed
if updateNeeded {
var ghResp *github.Response
pr, ghResp, err = client.PullRequests.Edit(ctx, owner, repo, pullNumber, update)
if err != nil {
return nil, fmt.Errorf("failed to update pull request: %w", err)
}
resp = ghResp.Response
defer func() {
if resp != nil && resp.Body != nil {
_ = resp.Body.Close()
}
}()

if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
return mcp.NewToolResultError(fmt.Sprintf("failed to update pull request: %s", string(body))), nil
}
} else {
// If no update needed, just get the current PR
var ghResp *github.Response
pr, ghResp, err = client.PullRequests.Get(ctx, owner, repo, pullNumber)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We we need this else? If no updates are needed and no new reviewers were requested we will just return an error (please see comment below).

if err != nil {
return nil, fmt.Errorf("failed to get pull request: %w", err)
}
resp = ghResp.Response
defer func() {
if resp != nil && resp.Body != nil {
_ = resp.Body.Close()
}
}()

if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
return mcp.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil
}
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
// Add reviewers if specified
if len(reviewers) > 0 {
reviewersRequest := github.ReviewersRequest{
Reviewers: reviewers,
}

// Use the direct result of RequestReviewers which includes the requested reviewers
updatedPR, resp, err := client.PullRequests.RequestReviewers(ctx, owner, repo, pullNumber, reviewersRequest)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
return nil, fmt.Errorf("failed to request reviewers: %w", err)
}
return mcp.NewToolResultError(fmt.Sprintf("failed to update pull request: %s", string(body))), nil
defer func() {
if resp != nil && resp.Body != nil {
_ = resp.Body.Close()
}
}()

if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
return mcp.NewToolResultError(fmt.Sprintf("failed to request reviewers: %s", string(body))), nil
}

// Use the updated PR with reviewers
pr = updatedPR
}

// If no updates and no reviewers, return error
if !updateNeeded && len(reviewers) == 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this check before the line 316?

return mcp.NewToolResultError("No update parameters provided"), nil
}

r, err := json.Marshal(pr)
Expand Down
100 changes: 98 additions & 2 deletions pkg/github/pullrequests_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ func Test_UpdatePullRequest(t *testing.T) {
assert.Contains(t, tool.InputSchema.Properties, "state")
assert.Contains(t, tool.InputSchema.Properties, "base")
assert.Contains(t, tool.InputSchema.Properties, "maintainer_can_modify")
assert.Contains(t, tool.InputSchema.Properties, "reviewers")
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber"})

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

// Mock PR for when there are no updates but we still need a response
mockNoUpdatePR := &github.PullRequest{
Number: github.Ptr(42),
Title: github.Ptr("Test PR"),
State: github.Ptr("open"),
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be removed as well due to the referencing test not being required


mockPRWithReviewers := &github.PullRequest{
Number: github.Ptr(42),
Title: github.Ptr("Test PR"),
State: github.Ptr("open"),
RequestedReviewers: []*github.User{
{Login: github.Ptr("reviewer1")},
{Login: github.Ptr("reviewer2")},
},
}

tests := []struct {
name string
mockedClient *http.Client
Expand Down Expand Up @@ -220,8 +238,40 @@ func Test_UpdatePullRequest(t *testing.T) {
expectedPR: mockClosedPR,
},
{
name: "no update parameters provided",
mockedClient: mock.NewMockedHTTPClient(), // No API call expected
name: "successful PR update with reviewers",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatch(
mock.GetReposPullsByOwnerByRepoByPullNumber,
&github.PullRequest{
Number: github.Ptr(42),
Title: github.Ptr("Test PR"),
State: github.Ptr("open"),
},
),
// Mock for RequestReviewers call, returning the PR with reviewers
mock.WithRequestMatch(
mock.PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber,
mockPRWithReviewers,
),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"pullNumber": float64(42),
"reviewers": []interface{}{"reviewer1", "reviewer2"},
},
expectError: false,
expectedPR: mockPRWithReviewers,
},
{
name: "no update parameters provided",
mockedClient: mock.NewMockedHTTPClient(
// Mock a response for the GET PR request in case of no updates
mock.WithRequestMatch(
mock.GetReposPullsByOwnerByRepoByPullNumber,
mockNoUpdatePR,
),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
Expand Down Expand Up @@ -251,6 +301,32 @@ func Test_UpdatePullRequest(t *testing.T) {
expectError: true,
expectedErrMsg: "failed to update pull request",
},
{
name: "request reviewers fails",
mockedClient: mock.NewMockedHTTPClient(
// First it gets the PR (no fields to update)
mock.WithRequestMatch(
mock.GetReposPullsByOwnerByRepoByPullNumber,
mockNoUpdatePR,
),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting the PR here is not needed

// Then reviewer request fails
mock.WithRequestMatchHandler(
mock.PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnprocessableEntity)
_, _ = w.Write([]byte(`{"message": "Invalid reviewers"}`))
}),
),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"pullNumber": float64(42),
"reviewers": []interface{}{"invalid-user"},
},
expectError: true,
expectedErrMsg: "failed to request reviewers",
},
}

for _, tc := range tests {
Expand Down Expand Up @@ -304,6 +380,26 @@ func Test_UpdatePullRequest(t *testing.T) {
if tc.expectedPR.MaintainerCanModify != nil {
assert.Equal(t, *tc.expectedPR.MaintainerCanModify, *returnedPR.MaintainerCanModify)
}

// Check reviewers if they exist in the expected PR
if tc.expectedPR.RequestedReviewers != nil && len(tc.expectedPR.RequestedReviewers) > 0 {
assert.NotNil(t, returnedPR.RequestedReviewers)
assert.Equal(t, len(tc.expectedPR.RequestedReviewers), len(returnedPR.RequestedReviewers))

// Create maps of reviewer logins for easy comparison
expectedReviewers := make(map[string]bool)
for _, reviewer := range tc.expectedPR.RequestedReviewers {
expectedReviewers[*reviewer.Login] = true
}

actualReviewers := make(map[string]bool)
for _, reviewer := range returnedPR.RequestedReviewers {
actualReviewers[*reviewer.Login] = true
}

// Compare the maps
assert.Equal(t, expectedReviewers, actualReviewers)
}
})
}
}
Expand Down
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