Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
50 changes: 8 additions & 42 deletions pkg/github/granular_tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1653,49 +1653,15 @@ func TestGranularUpdatePullRequestDraftState(t *testing.T) {

func TestGranularAddPullRequestReviewComment(t *testing.T) {
mockedClient := githubv4mock.NewMockedHTTPClient(
githubv4mock.NewQueryMatcher(
struct {
Viewer struct {
Login githubv4.String
}
}{},
nil,
githubv4mock.DataResponse(map[string]any{
"viewer": map[string]any{"login": "testuser"},
}),
),
githubv4mock.NewQueryMatcher(
struct {
Repository struct {
PullRequest struct {
Reviews struct {
Nodes []struct {
ID githubv4.ID
State githubv4.PullRequestReviewState
URL githubv4.URI
}
} `graphql:"reviews(first: 1, author: $author)"`
} `graphql:"pullRequest(number: $prNum)"`
} `graphql:"repository(owner: $owner, name: $name)"`
}{},
map[string]any{
"author": githubv4.String("testuser"),
"owner": githubv4.String("owner"),
"name": githubv4.String("repo"),
"prNum": githubv4.Int(1),
viewerIDQuery("U_testuser"),
getPendingReviewsQuery(getPendingReviewsQueryParams{
owner: "owner",
repo: "repo",
prNum: 1,
reviews: []pendingReviewQueryReview{
{id: "PRR_123", authorID: "U_testuser"},
},
githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"pullRequest": map[string]any{
"reviews": map[string]any{
"nodes": []map[string]any{
{"id": "PRR_123", "state": "PENDING", "url": "https://github.com/owner/repo/pull/1#pullrequestreview-123"},
},
},
},
},
}),
),
}),
githubv4mock.NewMutationMatcher(
struct {
AddPullRequestReviewThread struct {
Expand Down
262 changes: 106 additions & 156 deletions pkg/github/pullrequests.go
Original file line number Diff line number Diff line change
Expand Up @@ -1989,57 +1989,9 @@ func CreatePullRequestReview(ctx context.Context, client *githubv4.Client, param
}

func SubmitPendingPullRequestReview(ctx context.Context, client *githubv4.Client, params PullRequestReviewWriteParams) (*mcp.CallToolResult, error) {
// First we'll get the current user
var getViewerQuery struct {
Viewer struct {
Login githubv4.String
}
}

if err := client.Query(ctx, &getViewerQuery, nil); err != nil {
return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
"failed to get current user",
err,
), nil
}

var getLatestReviewForViewerQuery struct {
Repository struct {
PullRequest struct {
Reviews struct {
Nodes []struct {
ID githubv4.ID
State githubv4.PullRequestReviewState
URL githubv4.URI
}
} `graphql:"reviews(first: 1, author: $author)"`
} `graphql:"pullRequest(number: $prNum)"`
} `graphql:"repository(owner: $owner, name: $name)"`
}

vars := map[string]any{
"author": githubv4.String(getViewerQuery.Viewer.Login),
"owner": githubv4.String(params.Owner),
"name": githubv4.String(params.Repo),
"prNum": githubv4.Int(params.PullNumber),
}

if err := client.Query(ctx, &getLatestReviewForViewerQuery, vars); err != nil {
return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
"failed to get latest review for current user",
err,
), nil
}

// Validate there is one review and the state is pending
if len(getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes) == 0 {
return utils.NewToolResultError("No pending review found for the viewer"), nil
}

review := getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes[0]
if review.State != githubv4.PullRequestReviewStatePending {
errText := fmt.Sprintf("The latest review, found at %s is not pending", review.URL)
return utils.NewToolResultError(errText), nil
review, result := getPendingPullRequestReviewForViewer(ctx, client, params.Owner, params.Repo, params.PullNumber)
if result != nil {
return result, nil
}

// Prepare the mutation
Expand All @@ -2055,7 +2007,7 @@ func SubmitPendingPullRequestReview(ctx context.Context, client *githubv4.Client
ctx,
&submitPullRequestReviewMutation,
githubv4.SubmitPullRequestReviewInput{
PullRequestReviewID: &review.ID,
PullRequestReviewID: review,
Event: githubv4.PullRequestReviewEvent(params.Event),
Body: newGQLStringlikePtr[githubv4.String](&params.Body),
},
Expand All @@ -2074,57 +2026,9 @@ func SubmitPendingPullRequestReview(ctx context.Context, client *githubv4.Client
}

func DeletePendingPullRequestReview(ctx context.Context, client *githubv4.Client, params PullRequestReviewWriteParams) (*mcp.CallToolResult, error) {
// First we'll get the current user
var getViewerQuery struct {
Viewer struct {
Login githubv4.String
}
}

if err := client.Query(ctx, &getViewerQuery, nil); err != nil {
return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
"failed to get current user",
err,
), nil
}

var getLatestReviewForViewerQuery struct {
Repository struct {
PullRequest struct {
Reviews struct {
Nodes []struct {
ID githubv4.ID
State githubv4.PullRequestReviewState
URL githubv4.URI
}
} `graphql:"reviews(first: 1, author: $author)"`
} `graphql:"pullRequest(number: $prNum)"`
} `graphql:"repository(owner: $owner, name: $name)"`
}

vars := map[string]any{
"author": githubv4.String(getViewerQuery.Viewer.Login),
"owner": githubv4.String(params.Owner),
"name": githubv4.String(params.Repo),
"prNum": githubv4.Int(params.PullNumber),
}

if err := client.Query(ctx, &getLatestReviewForViewerQuery, vars); err != nil {
return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
"failed to get latest review for current user",
err,
), nil
}

// Validate there is one review and the state is pending
if len(getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes) == 0 {
return utils.NewToolResultError("No pending review found for the viewer"), nil
}

review := getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes[0]
if review.State != githubv4.PullRequestReviewStatePending {
errText := fmt.Sprintf("The latest review, found at %s is not pending", review.URL)
return utils.NewToolResultError(errText), nil
review, result := getPendingPullRequestReviewForViewer(ctx, client, params.Owner, params.Repo, params.PullNumber)
if result != nil {
return result, nil
}

// Prepare the mutation
Expand All @@ -2140,7 +2044,7 @@ func DeletePendingPullRequestReview(ctx context.Context, client *githubv4.Client
ctx,
&deletePullRequestReviewMutation,
githubv4.DeletePullRequestReviewInput{
PullRequestReviewID: &review.ID,
PullRequestReviewID: review,
},
nil,
); err != nil {
Expand Down Expand Up @@ -2235,57 +2139,9 @@ type AddCommentToPendingReviewParams struct {

// AddCommentToPendingReviewCall adds a review comment to the viewer's pending pull request review.
func AddCommentToPendingReviewCall(ctx context.Context, client *githubv4.Client, params AddCommentToPendingReviewParams) (*mcp.CallToolResult, error) {
// Get the current user
var getViewerQuery struct {
Viewer struct {
Login githubv4.String
}
}

if err := client.Query(ctx, &getViewerQuery, nil); err != nil {
return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
"failed to get current user",
err,
), nil
}

var getLatestReviewForViewerQuery struct {
Repository struct {
PullRequest struct {
Reviews struct {
Nodes []struct {
ID githubv4.ID
State githubv4.PullRequestReviewState
URL githubv4.URI
}
} `graphql:"reviews(first: 1, author: $author)"`
} `graphql:"pullRequest(number: $prNum)"`
} `graphql:"repository(owner: $owner, name: $name)"`
}

vars := map[string]any{
"author": githubv4.String(getViewerQuery.Viewer.Login),
"owner": githubv4.String(params.Owner),
"name": githubv4.String(params.Repo),
"prNum": githubv4.Int(params.PullNumber),
}

if err := client.Query(ctx, &getLatestReviewForViewerQuery, vars); err != nil {
return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
"failed to get latest review for current user",
err,
), nil
}

// Validate there is one review and the state is pending
if len(getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes) == 0 {
return utils.NewToolResultError("No pending review found for the viewer"), nil
}

review := getLatestReviewForViewerQuery.Repository.PullRequest.Reviews.Nodes[0]
if review.State != githubv4.PullRequestReviewStatePending {
errText := fmt.Sprintf("The latest review, found at %s is not pending", review.URL)
return utils.NewToolResultError(errText), nil
review, result := getPendingPullRequestReviewForViewer(ctx, client, params.Owner, params.Repo, params.PullNumber)
if result != nil {
return result, nil
}

// Create a new review thread comment on the review.
Expand All @@ -2308,7 +2164,7 @@ func AddCommentToPendingReviewCall(ctx context.Context, client *githubv4.Client,
Side: newGQLStringlikePtr[githubv4.DiffSide](params.Side),
StartLine: newGQLIntPtr(params.StartLine),
StartSide: newGQLStringlikePtr[githubv4.DiffSide](params.StartSide),
PullRequestReviewID: &review.ID,
PullRequestReviewID: review,
},
nil,
); err != nil {
Expand All @@ -2326,6 +2182,100 @@ func AddCommentToPendingReviewCall(ctx context.Context, client *githubv4.Client,
return utils.NewToolResultText("pull request review comment successfully added to pending review"), nil
}

type pendingReviewAuthor struct {
Bot struct {
ID githubv4.ID `graphql:"botId: id"`
} `graphql:"... on Bot"`
EnterpriseUserAccount struct {
ID githubv4.ID `graphql:"enterpriseUserAccountId: id"`
} `graphql:"... on EnterpriseUserAccount"`
Mannequin struct {
ID githubv4.ID `graphql:"mannequinId: id"`
} `graphql:"... on Mannequin"`
Organization struct {
ID githubv4.ID `graphql:"organizationId: id"`
} `graphql:"... on Organization"`
User struct {
ID githubv4.ID `graphql:"userId: id"`
} `graphql:"... on User"`
}

func (a pendingReviewAuthor) id() githubv4.ID {
for _, id := range []githubv4.ID{
a.Bot.ID,
a.EnterpriseUserAccount.ID,
a.Mannequin.ID,
a.Organization.ID,
a.User.ID,
} {
if id != nil {
return id
}
}
return nil
}

func getPendingPullRequestReviewForViewer(ctx context.Context, client *githubv4.Client, owner, repo string, pullNumber int32) (*githubv4.ID, *mcp.CallToolResult) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Live MCP validation: concrete Actor fragments expose the stable node ID even when viewer and review author types differ.

var getViewerQuery struct {
Viewer struct {
ID githubv4.ID
}
}

if err := client.Query(ctx, &getViewerQuery, nil); err != nil {
return nil, ghErrors.NewGitHubGraphQLErrorResponse(ctx,
"failed to get current user",
err,
)
}

vars := map[string]any{
"after": (*githubv4.String)(nil),
"owner": githubv4.String(owner),
"name": githubv4.String(repo),
"prNum": githubv4.Int(pullNumber),
"states": []githubv4.PullRequestReviewState{githubv4.PullRequestReviewStatePending},
}

for {
var getPendingReviewsQuery struct {
Repository struct {
PullRequest struct {
Reviews struct {
Nodes []struct {
ID githubv4.ID
Author pendingReviewAuthor
}
PageInfo struct {
HasNextPage githubv4.Boolean
EndCursor githubv4.String
}
} `graphql:"reviews(first: 100, after: $after, states: $states)"`
} `graphql:"pullRequest(number: $prNum)"`
} `graphql:"repository(owner: $owner, name: $name)"`
}

if err := client.Query(ctx, &getPendingReviewsQuery, vars); err != nil {
return nil, ghErrors.NewGitHubGraphQLErrorResponse(ctx,
"failed to get pending pull request reviews",
err,
)
}

for _, review := range getPendingReviewsQuery.Repository.PullRequest.Reviews.Nodes {
if review.Author.id() == getViewerQuery.Viewer.ID {
reviewID := review.ID
return &reviewID, nil
}
}

if !getPendingReviewsQuery.Repository.PullRequest.Reviews.PageInfo.HasNextPage {
return nil, utils.NewToolResultError("No pending review found for the viewer")
}
vars["after"] = githubv4.NewString(getPendingReviewsQuery.Repository.PullRequest.Reviews.PageInfo.EndCursor)
Comment thread
Copilot marked this conversation as resolved.
}
}

// AddCommentToPendingReview creates a tool to add a comment to a pull request review.
func AddCommentToPendingReview(t translations.TranslationHelperFunc) inventory.ServerTool {
schema := &jsonschema.Schema{
Expand Down
Loading
Loading