From bfb759d137dfab11ff46f2edccedaac789519e0f Mon Sep 17 00:00:00 2001 From: Annie Gesellchen <10727170+cagesellchen@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:40:53 -0700 Subject: [PATCH 1/2] feat(pull-requests): expose review resolution reason Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8bf69b12-5131-4382-8c81-8ed1c88fb211 --- README.md | 1 + docs/feature-flags.md | 1 + .../pull_request_review_write.snap | 4 ++ .../__toolsnaps__/resolve_review_thread.snap | 4 ++ pkg/github/granular_tools_test.go | 8 ++-- pkg/github/pullrequests.go | 37 +++++++++++------ pkg/github/pullrequests_granular.go | 16 +++++++- pkg/github/pullrequests_test.go | 41 ++++++++++++++++++- 8 files changed, 92 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 9527478a0f..cc4608efa4 100644 --- a/README.md +++ b/README.md @@ -1236,6 +1236,7 @@ The following sets of tools are available: - `owner`: Repository owner (string, required) - `pullNumber`: Pull request number (number, required) - `repo`: Repository name (string, required) + - `resolutionReason`: Optional reason for resolving a Copilot code review thread: addressed, wont-fix, or invalid. (string, optional) - `threadId`: The node ID of the review thread (e.g., PRRT_kwDOxxx). Required for resolve_thread and unresolve_thread methods. Get thread IDs from pull_request_read with method get_review_comments. (string, optional) - **search_pull_requests** - Search pull requests diff --git a/docs/feature-flags.md b/docs/feature-flags.md index 0de5bdd722..af05055685 100644 --- a/docs/feature-flags.md +++ b/docs/feature-flags.md @@ -251,6 +251,7 @@ runtime behavior (such as output formatting) won't appear here. - **resolve_review_thread** - Resolve Review Thread - **Required OAuth Scopes**: `repo` + - `resolutionReason`: Optional reason for resolving a Copilot code review thread: addressed, wont-fix, or invalid. (string, optional) - `threadID`: The node ID of the review thread to resolve (e.g., PRRT_kwDOxxx) (string, required) - **submit_pending_pull_request_review** - Submit Pending Pull Request Review diff --git a/pkg/github/__toolsnaps__/pull_request_review_write.snap b/pkg/github/__toolsnaps__/pull_request_review_write.snap index 74ef808559..43b97bc313 100644 --- a/pkg/github/__toolsnaps__/pull_request_review_write.snap +++ b/pkg/github/__toolsnaps__/pull_request_review_write.snap @@ -47,6 +47,10 @@ "description": "Repository name", "type": "string" }, + "resolutionReason": { + "description": "Optional reason for resolving a Copilot code review thread: addressed, wont-fix, or invalid.", + "type": "string" + }, "threadId": { "description": "The node ID of the review thread (e.g., PRRT_kwDOxxx). Required for resolve_thread and unresolve_thread methods. Get thread IDs from pull_request_read with method get_review_comments.", "type": "string" diff --git a/pkg/github/__toolsnaps__/resolve_review_thread.snap b/pkg/github/__toolsnaps__/resolve_review_thread.snap index 5f2b21b36f..b9d4758c24 100644 --- a/pkg/github/__toolsnaps__/resolve_review_thread.snap +++ b/pkg/github/__toolsnaps__/resolve_review_thread.snap @@ -9,6 +9,10 @@ "description": "Resolve a review thread on a pull request. Resolving an already-resolved thread is a no-op.", "inputSchema": { "properties": { + "resolutionReason": { + "description": "Optional reason for resolving a Copilot code review thread: addressed, wont-fix, or invalid.", + "type": "string" + }, "threadID": { "description": "The node ID of the review thread to resolve (e.g., PRRT_kwDOxxx)", "type": "string" diff --git a/pkg/github/granular_tools_test.go b/pkg/github/granular_tools_test.go index d70dd568dc..f427d774ef 100644 --- a/pkg/github/granular_tools_test.go +++ b/pkg/github/granular_tools_test.go @@ -1738,8 +1738,9 @@ func TestGranularResolveReviewThread(t *testing.T) { } } `graphql:"resolveReviewThread(input: $input)"` }{}, - githubv4.ResolveReviewThreadInput{ - ThreadID: githubv4.ID("PRRT_123"), + resolveReviewThreadInput{ + ThreadID: githubv4.ID("PRRT_123"), + ResolutionReason: newGQLStringlike[githubv4.String]("addressed"), }, nil, githubv4mock.DataResponse(map[string]any{ @@ -1755,7 +1756,8 @@ func TestGranularResolveReviewThread(t *testing.T) { handler := serverTool.Handler(deps) request := createMCPRequest(map[string]any{ - "threadID": "PRRT_123", + "threadID": "PRRT_123", + "resolutionReason": "addressed", }) result, err := handler(ContextWithDeps(context.Background(), deps), &request) require.NoError(t, err) diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index 8801ec2894..bd530651d6 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -1760,14 +1760,15 @@ func UpdatePullRequestBranch(t translations.TranslationHelperFunc) inventory.Ser } type PullRequestReviewWriteParams struct { - Method string - Owner string - Repo string - PullNumber int32 - Body string - Event string - CommitID *string - ThreadID string + Method string + Owner string + Repo string + PullNumber int32 + Body string + Event string + CommitID *string + ThreadID string + ResolutionReason *string } func PullRequestReviewWrite(t translations.TranslationHelperFunc) inventory.ServerTool { @@ -1811,6 +1812,10 @@ func PullRequestReviewWrite(t translations.TranslationHelperFunc) inventory.Serv Type: "string", Description: "The node ID of the review thread (e.g., PRRT_kwDOxxx). Required for resolve_thread and unresolve_thread methods. Get thread IDs from pull_request_read with method get_review_comments.", }, + "resolutionReason": { + Type: "string", + Description: "Optional reason for resolving a Copilot code review thread: addressed, wont-fix, or invalid.", + }, }, Required: []string{"method", "owner", "repo", "pullNumber"}, } @@ -1858,10 +1863,10 @@ Available methods: result, err := DeletePendingPullRequestReview(ctx, client, params) return result, nil, err case "resolve_thread": - result, err := ResolveReviewThread(ctx, client, params.ThreadID, true) + result, err := ResolveReviewThread(ctx, client, params.ThreadID, params.ResolutionReason, true) return result, nil, err case "unresolve_thread": - result, err := ResolveReviewThread(ctx, client, params.ThreadID, false) + result, err := ResolveReviewThread(ctx, client, params.ThreadID, nil, false) return result, nil, err default: return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", params.Method)), nil, nil @@ -2094,8 +2099,13 @@ func DeletePendingPullRequestReview(ctx context.Context, client *githubv4.Client return utils.NewToolResultText("pending pull request review successfully deleted"), nil } +type resolveReviewThreadInput struct { + ThreadID githubv4.ID `json:"threadId"` + ResolutionReason *githubv4.String `json:"resolutionReason,omitempty"` +} + // ResolveReviewThread resolves or unresolves a PR review thread using GraphQL mutations. -func ResolveReviewThread(ctx context.Context, client *githubv4.Client, threadID string, resolve bool) (*mcp.CallToolResult, error) { +func ResolveReviewThread(ctx context.Context, client *githubv4.Client, threadID string, resolutionReason *string, resolve bool) (*mcp.CallToolResult, error) { if threadID == "" { return utils.NewToolResultError("threadId is required for resolve_thread and unresolve_thread methods"), nil } @@ -2110,8 +2120,9 @@ func ResolveReviewThread(ctx context.Context, client *githubv4.Client, threadID } `graphql:"resolveReviewThread(input: $input)"` } - input := githubv4.ResolveReviewThreadInput{ - ThreadID: githubv4.ID(threadID), + input := resolveReviewThreadInput{ + ThreadID: githubv4.ID(threadID), + ResolutionReason: newGQLStringlikePtr[githubv4.String](resolutionReason), } if err := client.Mutate(ctx, &mutation, input, nil); err != nil { diff --git a/pkg/github/pullrequests_granular.go b/pkg/github/pullrequests_granular.go index c727beb6e5..3d3cff764a 100644 --- a/pkg/github/pullrequests_granular.go +++ b/pkg/github/pullrequests_granular.go @@ -690,6 +690,10 @@ func GranularResolveReviewThread(t translations.TranslationHelperFunc) inventory Type: "string", Description: "The node ID of the review thread to resolve (e.g., PRRT_kwDOxxx)", }, + "resolutionReason": { + Type: "string", + Description: "Optional reason for resolving a Copilot code review thread: addressed, wont-fix, or invalid.", + }, }, Required: []string{"threadID"}, }, @@ -700,13 +704,21 @@ func GranularResolveReviewThread(t translations.TranslationHelperFunc) inventory if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } + resolutionReason, hasResolutionReason, err := OptionalParamOK[string](args, "resolutionReason") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + var resolutionReasonPtr *string + if hasResolutionReason { + resolutionReasonPtr = &resolutionReason + } gqlClient, err := deps.GetGQLClient(ctx) if err != nil { return utils.NewToolResultErrorFromErr("failed to get GitHub GraphQL client", err), nil, nil } - result, err := ResolveReviewThread(ctx, gqlClient, threadID, true) + result, err := ResolveReviewThread(ctx, gqlClient, threadID, resolutionReasonPtr, true) return result, nil, err }, ) @@ -750,7 +762,7 @@ func GranularUnresolveReviewThread(t translations.TranslationHelperFunc) invento return utils.NewToolResultErrorFromErr("failed to get GitHub GraphQL client", err), nil, nil } - result, err := ResolveReviewThread(ctx, gqlClient, threadID, false) + result, err := ResolveReviewThread(ctx, gqlClient, threadID, nil, false) return result, nil, err }, ) diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go index 03ec851cf4..9e9843cf99 100644 --- a/pkg/github/pullrequests_test.go +++ b/pkg/github/pullrequests_test.go @@ -4572,7 +4572,7 @@ func TestResolveReviewThread(t *testing.T) { } } `graphql:"resolveReviewThread(input: $input)"` }{}, - githubv4.ResolveReviewThreadInput{ + resolveReviewThreadInput{ ThreadID: githubv4.ID("PRRT_kwDOTest123"), }, nil, @@ -4588,6 +4588,43 @@ func TestResolveReviewThread(t *testing.T) { ), expectedResult: "review thread resolved successfully", }, + { + name: "successful resolve thread with resolution reason", + requestArgs: map[string]any{ + "method": "resolve_thread", + "owner": "owner", + "repo": "repo", + "pullNumber": float64(42), + "threadId": "PRRT_kwDOTest123", + "resolutionReason": "wont-fix", + }, + mockedClient: githubv4mock.NewMockedHTTPClient( + githubv4mock.NewMutationMatcher( + struct { + ResolveReviewThread struct { + Thread struct { + ID githubv4.ID + IsResolved githubv4.Boolean + } + } `graphql:"resolveReviewThread(input: $input)"` + }{}, + resolveReviewThreadInput{ + ThreadID: githubv4.ID("PRRT_kwDOTest123"), + ResolutionReason: newGQLStringlike[githubv4.String]("wont-fix"), + }, + nil, + githubv4mock.DataResponse(map[string]any{ + "resolveReviewThread": map[string]any{ + "thread": map[string]any{ + "id": "PRRT_kwDOTest123", + "isResolved": true, + }, + }, + }), + ), + ), + expectedResult: "review thread resolved successfully", + }, { name: "successful unresolve thread", requestArgs: map[string]any{ @@ -4692,7 +4729,7 @@ func TestResolveReviewThread(t *testing.T) { } } `graphql:"resolveReviewThread(input: $input)"` }{}, - githubv4.ResolveReviewThreadInput{ + resolveReviewThreadInput{ ThreadID: githubv4.ID("PRRT_invalid"), }, nil, From 96d913ab25fdc5ad69086a306cfbe1f0dfcba88a Mon Sep 17 00:00:00 2001 From: Annie Gesellchen <10727170+cagesellchen@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:28:58 -0700 Subject: [PATCH 2/2] fix(pull-requests): preserve resolver API Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8bf69b12-5131-4382-8c81-8ed1c88fb211 --- pkg/github/granular_tools_test.go | 79 +++++++++++++++++------------ pkg/github/pullrequests.go | 11 ++-- pkg/github/pullrequests_granular.go | 4 +- 3 files changed, 56 insertions(+), 38 deletions(-) diff --git a/pkg/github/granular_tools_test.go b/pkg/github/granular_tools_test.go index f427d774ef..e802203c98 100644 --- a/pkg/github/granular_tools_test.go +++ b/pkg/github/granular_tools_test.go @@ -1728,40 +1728,53 @@ func TestGranularAddPullRequestReviewComment(t *testing.T) { } func TestGranularResolveReviewThread(t *testing.T) { - mockedClient := githubv4mock.NewMockedHTTPClient( - githubv4mock.NewMutationMatcher( - struct { - ResolveReviewThread struct { - Thread struct { - ID githubv4.ID - IsResolved githubv4.Boolean - } - } `graphql:"resolveReviewThread(input: $input)"` - }{}, - resolveReviewThreadInput{ - ThreadID: githubv4.ID("PRRT_123"), - ResolutionReason: newGQLStringlike[githubv4.String]("addressed"), - }, - nil, - githubv4mock.DataResponse(map[string]any{ - "resolveReviewThread": map[string]any{ - "thread": map[string]any{"id": "PRRT_123", "isResolved": true}, - }, - }), - ), - ) - gqlClient := githubv4.NewClient(mockedClient) - deps := BaseDeps{GQLClient: gqlClient} - serverTool := GranularResolveReviewThread(translations.NullTranslationHelper) - handler := serverTool.Handler(deps) + tests := []struct { + name string + resolutionReason *string + }{ + {name: "with resolution reason", resolutionReason: gogithub.Ptr("addressed")}, + {name: "without resolution reason"}, + } - request := createMCPRequest(map[string]any{ - "threadID": "PRRT_123", - "resolutionReason": "addressed", - }) - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - require.NoError(t, err) - assert.False(t, result.IsError) + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockedClient := githubv4mock.NewMockedHTTPClient( + githubv4mock.NewMutationMatcher( + struct { + ResolveReviewThread struct { + Thread struct { + ID githubv4.ID + IsResolved githubv4.Boolean + } + } `graphql:"resolveReviewThread(input: $input)"` + }{}, + resolveReviewThreadInput{ + ThreadID: githubv4.ID("PRRT_123"), + ResolutionReason: newGQLStringlikePtr[githubv4.String](tc.resolutionReason), + }, + nil, + githubv4mock.DataResponse(map[string]any{ + "resolveReviewThread": map[string]any{ + "thread": map[string]any{"id": "PRRT_123", "isResolved": true}, + }, + }), + ), + ) + gqlClient := githubv4.NewClient(mockedClient) + deps := BaseDeps{GQLClient: gqlClient} + serverTool := GranularResolveReviewThread(translations.NullTranslationHelper) + handler := serverTool.Handler(deps) + + args := map[string]any{"threadID": "PRRT_123"} + if tc.resolutionReason != nil { + args["resolutionReason"] = *tc.resolutionReason + } + request := createMCPRequest(args) + result, err := handler(ContextWithDeps(context.Background(), deps), &request) + require.NoError(t, err) + assert.False(t, result.IsError) + }) + } } func TestGranularUnresolveReviewThread(t *testing.T) { diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index bd530651d6..eea382d1e3 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -1863,10 +1863,10 @@ Available methods: result, err := DeletePendingPullRequestReview(ctx, client, params) return result, nil, err case "resolve_thread": - result, err := ResolveReviewThread(ctx, client, params.ThreadID, params.ResolutionReason, true) + result, err := ResolveReviewThreadWithReason(ctx, client, params.ThreadID, params.ResolutionReason, true) return result, nil, err case "unresolve_thread": - result, err := ResolveReviewThread(ctx, client, params.ThreadID, nil, false) + result, err := ResolveReviewThread(ctx, client, params.ThreadID, false) return result, nil, err default: return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", params.Method)), nil, nil @@ -2105,7 +2105,12 @@ type resolveReviewThreadInput struct { } // ResolveReviewThread resolves or unresolves a PR review thread using GraphQL mutations. -func ResolveReviewThread(ctx context.Context, client *githubv4.Client, threadID string, resolutionReason *string, resolve bool) (*mcp.CallToolResult, error) { +func ResolveReviewThread(ctx context.Context, client *githubv4.Client, threadID string, resolve bool) (*mcp.CallToolResult, error) { + return ResolveReviewThreadWithReason(ctx, client, threadID, nil, resolve) +} + +// ResolveReviewThreadWithReason resolves or unresolves a PR review thread with an optional resolution reason. +func ResolveReviewThreadWithReason(ctx context.Context, client *githubv4.Client, threadID string, resolutionReason *string, resolve bool) (*mcp.CallToolResult, error) { if threadID == "" { return utils.NewToolResultError("threadId is required for resolve_thread and unresolve_thread methods"), nil } diff --git a/pkg/github/pullrequests_granular.go b/pkg/github/pullrequests_granular.go index 3d3cff764a..f5007c68df 100644 --- a/pkg/github/pullrequests_granular.go +++ b/pkg/github/pullrequests_granular.go @@ -718,7 +718,7 @@ func GranularResolveReviewThread(t translations.TranslationHelperFunc) inventory return utils.NewToolResultErrorFromErr("failed to get GitHub GraphQL client", err), nil, nil } - result, err := ResolveReviewThread(ctx, gqlClient, threadID, resolutionReasonPtr, true) + result, err := ResolveReviewThreadWithReason(ctx, gqlClient, threadID, resolutionReasonPtr, true) return result, nil, err }, ) @@ -762,7 +762,7 @@ func GranularUnresolveReviewThread(t translations.TranslationHelperFunc) invento return utils.NewToolResultErrorFromErr("failed to get GitHub GraphQL client", err), nil, nil } - result, err := ResolveReviewThread(ctx, gqlClient, threadID, nil, false) + result, err := ResolveReviewThread(ctx, gqlClient, threadID, false) return result, nil, err }, )