From 3b3376435981483185227cb8ee81f3948b984eee Mon Sep 17 00:00:00 2001 From: Iulia B Date: Mon, 11 May 2026 13:56:38 +0000 Subject: [PATCH 1/7] upgrade to v 0.86 --- e2e/e2e_test.go | 2 +- go.mod | 2 +- go.sum | 4 ++-- internal/ghmcp/server.go | 2 +- pkg/errors/error.go | 2 +- pkg/errors/error_test.go | 2 +- pkg/github/actions.go | 6 ++--- pkg/github/actions_test.go | 2 +- pkg/github/code_scanning.go | 2 +- pkg/github/code_scanning_test.go | 2 +- pkg/github/context_tools_test.go | 2 +- pkg/github/copilot.go | 2 +- pkg/github/copilot_test.go | 2 +- pkg/github/dependabot.go | 2 +- pkg/github/dependabot_test.go | 2 +- pkg/github/dependencies.go | 2 +- pkg/github/discussions.go | 2 +- pkg/github/discussions_test.go | 2 +- pkg/github/gists.go | 2 +- pkg/github/gists_test.go | 2 +- pkg/github/git.go | 2 +- pkg/github/git_test.go | 2 +- pkg/github/granular_tools_test.go | 2 +- pkg/github/issues.go | 10 ++++---- pkg/github/issues_granular.go | 2 +- pkg/github/issues_test.go | 2 +- pkg/github/minimal_types.go | 2 +- pkg/github/notifications.go | 10 ++------ pkg/github/notifications_test.go | 17 +------------- pkg/github/params.go | 2 +- pkg/github/params_test.go | 2 +- pkg/github/projects.go | 23 +++++-------------- pkg/github/projects_test.go | 2 +- pkg/github/pullrequests.go | 2 +- pkg/github/pullrequests_granular.go | 2 +- pkg/github/pullrequests_test.go | 2 +- pkg/github/repositories.go | 2 +- pkg/github/repositories_helper.go | 2 +- pkg/github/repositories_test.go | 2 +- pkg/github/repository_resource.go | 2 +- pkg/github/repository_resource_completions.go | 2 +- .../repository_resource_completions_test.go | 2 +- pkg/github/repository_resource_test.go | 2 +- pkg/github/search.go | 2 +- pkg/github/search_test.go | 2 +- pkg/github/search_utils.go | 2 +- pkg/github/secret_scanning.go | 2 +- pkg/github/secret_scanning_test.go | 2 +- pkg/github/security_advisories.go | 2 +- pkg/github/security_advisories_test.go | 2 +- pkg/github/server_test.go | 2 +- pkg/github/tools.go | 2 +- pkg/lockdown/lockdown.go | 2 +- pkg/lockdown/lockdown_test.go | 2 +- pkg/raw/raw.go | 5 ++-- pkg/raw/raw_test.go | 2 +- 56 files changed, 69 insertions(+), 104 deletions(-) diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index ad40ecad02..b43ef33bba 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -18,7 +18,7 @@ import ( "github.com/github/github-mcp-server/internal/ghmcp" "github.com/github/github-mcp-server/pkg/github" "github.com/github/github-mcp-server/pkg/translations" - gogithub "github.com/google/go-github/v82/github" + gogithub "github.com/google/go-github/v86/github" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/require" ) diff --git a/go.mod b/go.mod index 89cafc377d..20713586de 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.25.0 require ( github.com/go-chi/chi/v5 v5.2.5 github.com/go-viper/mapstructure/v2 v2.5.0 - github.com/google/go-github/v82 v82.0.0 + github.com/google/go-github/v86 v86.0.0 github.com/google/jsonschema-go v0.4.2 github.com/josephburnett/jd/v2 v2.5.0 github.com/lithammer/fuzzysearch v1.1.8 diff --git a/go.sum b/go.sum index 615b4e9c0c..d18cd07d5e 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +16,8 @@ github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArs github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-github/v82 v82.0.0 h1:OH09ESON2QwKCUVMYmMcVu1IFKFoaZHwqYaUtr/MVfk= -github.com/google/go-github/v82 v82.0.0/go.mod h1:hQ6Xo0VKfL8RZ7z1hSfB4fvISg0QqHOqe9BP0qo+WvM= +github.com/google/go-github/v86 v86.0.0 h1:S/6aANJhwRm8EQmGKVML3j41yq0h2BsTP8FnDkO7kcA= +github.com/google/go-github/v86 v86.0.0/go.mod h1:zKv1l4SwDXNFMGByi2FWkq71KwSXqj/eQRZuqtmcot8= github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= diff --git a/internal/ghmcp/server.go b/internal/ghmcp/server.go index b1925bffd3..e9b2529fe2 100644 --- a/internal/ghmcp/server.go +++ b/internal/ghmcp/server.go @@ -24,7 +24,7 @@ import ( "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" - gogithub "github.com/google/go-github/v82/github" + gogithub "github.com/google/go-github/v86/github" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/shurcooL/githubv4" ) diff --git a/pkg/errors/error.go b/pkg/errors/error.go index d757651592..1c93be1487 100644 --- a/pkg/errors/error.go +++ b/pkg/errors/error.go @@ -6,7 +6,7 @@ import ( "net/http" "github.com/github/github-mcp-server/pkg/utils" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/errors/error_test.go b/pkg/errors/error_test.go index e33d5bd39e..0f0a0b9432 100644 --- a/pkg/errors/error_test.go +++ b/pkg/errors/error_test.go @@ -6,7 +6,7 @@ import ( "net/http" "testing" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/github/actions.go b/pkg/github/actions.go index c3b5bb8c71..849aca8f1d 100644 --- a/pkg/github/actions.go +++ b/pkg/github/actions.go @@ -16,7 +16,7 @@ import ( "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -990,10 +990,10 @@ func runWorkflow(ctx context.Context, client *github.Client, owner, repo, workfl var workflowType string if workflowIDInt, parseErr := strconv.ParseInt(workflowID, 10, 64); parseErr == nil { - resp, err = client.Actions.CreateWorkflowDispatchEventByID(ctx, owner, repo, workflowIDInt, event) + _, resp, err = client.Actions.CreateWorkflowDispatchEventByID(ctx, owner, repo, workflowIDInt, event) workflowType = "workflow_id" } else { - resp, err = client.Actions.CreateWorkflowDispatchEventByFileName(ctx, owner, repo, workflowID, event) + _, resp, err = client.Actions.CreateWorkflowDispatchEventByFileName(ctx, owner, repo, workflowID, event) workflowType = "workflow_file" } diff --git a/pkg/github/actions_test.go b/pkg/github/actions_test.go index fe960ed924..f4a9da5e3c 100644 --- a/pkg/github/actions_test.go +++ b/pkg/github/actions_test.go @@ -8,7 +8,7 @@ import ( "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/github/code_scanning.go b/pkg/github/code_scanning.go index 34249b2129..0172a6a507 100644 --- a/pkg/github/code_scanning.go +++ b/pkg/github/code_scanning.go @@ -11,7 +11,7 @@ import ( "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/github/code_scanning_test.go b/pkg/github/code_scanning_test.go index 7a3c16fd15..ee6ae5b155 100644 --- a/pkg/github/code_scanning_test.go +++ b/pkg/github/code_scanning_test.go @@ -8,7 +8,7 @@ import ( "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/github/context_tools_test.go b/pkg/github/context_tools_test.go index 365a019ab6..01912a6d56 100644 --- a/pkg/github/context_tools_test.go +++ b/pkg/github/context_tools_test.go @@ -10,7 +10,7 @@ import ( "github.com/github/github-mcp-server/internal/githubv4mock" "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/shurcooL/githubv4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/github/copilot.go b/pkg/github/copilot.go index d95357e738..cdf6f6d234 100644 --- a/pkg/github/copilot.go +++ b/pkg/github/copilot.go @@ -17,7 +17,7 @@ import ( "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" "github.com/go-viper/mapstructure/v2" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/shurcooL/githubv4" diff --git a/pkg/github/copilot_test.go b/pkg/github/copilot_test.go index 0a1d5ef3b6..b5c8e5ec01 100644 --- a/pkg/github/copilot_test.go +++ b/pkg/github/copilot_test.go @@ -10,7 +10,7 @@ import ( "github.com/github/github-mcp-server/internal/githubv4mock" "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/shurcooL/githubv4" "github.com/stretchr/testify/assert" diff --git a/pkg/github/dependabot.go b/pkg/github/dependabot.go index 541cc5c1e7..a55f173886 100644 --- a/pkg/github/dependabot.go +++ b/pkg/github/dependabot.go @@ -12,7 +12,7 @@ import ( "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/github/dependabot_test.go b/pkg/github/dependabot_test.go index 6c9b95ca36..28cded92f2 100644 --- a/pkg/github/dependabot_test.go +++ b/pkg/github/dependabot_test.go @@ -8,7 +8,7 @@ import ( "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/github/dependencies.go b/pkg/github/dependencies.go index aad213e4e5..15c7f83fa7 100644 --- a/pkg/github/dependencies.go +++ b/pkg/github/dependencies.go @@ -18,7 +18,7 @@ import ( "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" - gogithub "github.com/google/go-github/v82/github" + gogithub "github.com/google/go-github/v86/github" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/shurcooL/githubv4" ) diff --git a/pkg/github/discussions.go b/pkg/github/discussions.go index 700560b475..b1a1069d84 100644 --- a/pkg/github/discussions.go +++ b/pkg/github/discussions.go @@ -10,7 +10,7 @@ import ( "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" "github.com/go-viper/mapstructure/v2" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/shurcooL/githubv4" diff --git a/pkg/github/discussions_test.go b/pkg/github/discussions_test.go index 692ef2ec83..3e2008dd90 100644 --- a/pkg/github/discussions_test.go +++ b/pkg/github/discussions_test.go @@ -9,7 +9,7 @@ import ( "github.com/github/github-mcp-server/internal/githubv4mock" "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/shurcooL/githubv4" "github.com/stretchr/testify/assert" diff --git a/pkg/github/gists.go b/pkg/github/gists.go index a0bc1b0855..dff3a59f61 100644 --- a/pkg/github/gists.go +++ b/pkg/github/gists.go @@ -12,7 +12,7 @@ import ( "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/github/gists_test.go b/pkg/github/gists_test.go index 74cd45d276..d535c4486d 100644 --- a/pkg/github/gists_test.go +++ b/pkg/github/gists_test.go @@ -9,7 +9,7 @@ import ( "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/github/git.go b/pkg/github/git.go index 33a1f94efa..deb6b1e6a9 100644 --- a/pkg/github/git.go +++ b/pkg/github/git.go @@ -11,7 +11,7 @@ import ( "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/github/git_test.go b/pkg/github/git_test.go index cef65c9ef4..141cfb2199 100644 --- a/pkg/github/git_test.go +++ b/pkg/github/git_test.go @@ -9,7 +9,7 @@ import ( "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/github/granular_tools_test.go b/pkg/github/granular_tools_test.go index 6623894e43..2421aea2bd 100644 --- a/pkg/github/granular_tools_test.go +++ b/pkg/github/granular_tools_test.go @@ -9,7 +9,7 @@ import ( "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/inventory" "github.com/github/github-mcp-server/pkg/translations" - gogithub "github.com/google/go-github/v82/github" + gogithub "github.com/google/go-github/v86/github" "github.com/shurcooL/githubv4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 81161626bb..9415b71066 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -15,7 +15,7 @@ import ( "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/shurcooL/githubv4" @@ -418,11 +418,9 @@ func GetSubIssues(ctx context.Context, client *github.Client, deps ToolDependenc } featureFlags := deps.GetFlags(ctx) - opts := &github.IssueListOptions{ - ListOptions: github.ListOptions{ - Page: pagination.Page, - PerPage: pagination.PerPage, - }, + opts := &github.ListOptions{ + Page: pagination.Page, + PerPage: pagination.PerPage, } subIssues, resp, err := client.SubIssue.ListByIssue(ctx, owner, repo, int64(issueNumber), opts) diff --git a/pkg/github/issues_granular.go b/pkg/github/issues_granular.go index fe3b4bcc9b..1e1dba0cd1 100644 --- a/pkg/github/issues_granular.go +++ b/pkg/github/issues_granular.go @@ -12,7 +12,7 @@ import ( "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/shurcooL/githubv4" diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index 9c20824746..ca45c5130b 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -14,7 +14,7 @@ import ( "github.com/github/github-mcp-server/internal/githubv4mock" "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/shurcooL/githubv4" "github.com/stretchr/testify/assert" diff --git a/pkg/github/minimal_types.go b/pkg/github/minimal_types.go index a8757c51c3..a458243cd7 100644 --- a/pkg/github/minimal_types.go +++ b/pkg/github/minimal_types.go @@ -3,7 +3,7 @@ package github import ( "time" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/github/github-mcp-server/pkg/sanitize" ) diff --git a/pkg/github/notifications.go b/pkg/github/notifications.go index ddd3023932..e7b732f2f5 100644 --- a/pkg/github/notifications.go +++ b/pkg/github/notifications.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "net/http" - "strconv" "time" ghErrors "github.com/github/github-mcp-server/pkg/errors" @@ -14,7 +13,7 @@ import ( "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -210,12 +209,7 @@ func DismissNotification(t translations.TranslationHelperFunc) inventory.ServerT switch state { case "done": // for some inexplicable reason, the API seems to have threadID as int64 and string depending on the endpoint - var threadIDInt int64 - threadIDInt, err = strconv.ParseInt(threadID, 10, 64) - if err != nil { - return utils.NewToolResultError(fmt.Sprintf("invalid threadID format: %v", err)), nil, nil - } - resp, err = client.Activity.MarkThreadDone(ctx, threadIDInt) + resp, err = client.Activity.MarkThreadDone(ctx, threadID) case "read": resp, err = client.Activity.MarkThreadRead(ctx, threadID) default: diff --git a/pkg/github/notifications_test.go b/pkg/github/notifications_test.go index 030367d067..036aa0de3b 100644 --- a/pkg/github/notifications_test.go +++ b/pkg/github/notifications_test.go @@ -8,7 +8,7 @@ import ( "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -495,16 +495,6 @@ func Test_DismissNotification(t *testing.T) { expectError: false, expectDone: true, }, - { - name: "invalid threadID format", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "threadID": "notanumber", - "state": "done", - }, - expectError: false, - expectInvalid: true, - }, { name: "missing required threadID", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), @@ -552,8 +542,6 @@ func Test_DismissNotification(t *testing.T) { assert.Contains(t, text, "missing required parameter: threadID") case tc.requestArgs["state"] == nil: assert.Contains(t, text, "missing required parameter: state") - case tc.name == "invalid threadID format": - assert.Contains(t, text, "invalid threadID format") case tc.name == "invalid state value": assert.Contains(t, text, "Invalid state. Must be one of: read, done.") default: @@ -571,9 +559,6 @@ func Test_DismissNotification(t *testing.T) { if tc.expectDone { assert.Contains(t, textContent.Text, "Notification marked as done") } - if tc.expectInvalid { - assert.Contains(t, textContent.Text, "invalid threadID format") - } }) } } diff --git a/pkg/github/params.go b/pkg/github/params.go index 1b45d61bd8..4d57c7c0c6 100644 --- a/pkg/github/params.go +++ b/pkg/github/params.go @@ -6,7 +6,7 @@ import ( "math" "strconv" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" ) diff --git a/pkg/github/params_test.go b/pkg/github/params_test.go index 2254b737eb..bda1715051 100644 --- a/pkg/github/params_test.go +++ b/pkg/github/params_test.go @@ -5,7 +5,7 @@ import ( "math" "testing" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/stretchr/testify/assert" ) diff --git a/pkg/github/projects.go b/pkg/github/projects.go index dcb9193eca..687b9f9e84 100644 --- a/pkg/github/projects.go +++ b/pkg/github/projects.go @@ -13,7 +13,7 @@ import ( "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/shurcooL/githubv4" @@ -618,16 +618,10 @@ func listProjects(ctx context.Context, client *github.Client, args map[string]an var resp *github.Response var projects []*github.ProjectV2 - var queryPtr *string - - if queryStr != "" { - queryPtr = &queryStr - } - minimalProjects := []MinimalProject{} opts := &github.ListProjectsOptions{ ListProjectsPaginationOptions: pagination, - Query: queryPtr, + Query: queryStr, } // If owner_type not provided, fetch from both user and org @@ -801,17 +795,12 @@ func listProjectItems(ctx context.Context, client *github.Client, args map[strin var resp *github.Response var projectItems []*github.ProjectV2Item - var queryPtr *string - - if queryStr != "" { - queryPtr = &queryStr - } opts := &github.ListProjectItemsOptions{ Fields: fields, ListProjectsOptions: github.ListProjectsOptions{ ListProjectsPaginationOptions: pagination, - Query: queryPtr, + Query: queryStr, }, } @@ -1387,16 +1376,16 @@ func extractPaginationOptionsFromArgs(args map[string]any) (github.ListProjectsP } opts := github.ListProjectsPaginationOptions{ - PerPage: &perPage, + PerPage: perPage, } // Only set After/Before if they have non-empty values if after != "" { - opts.After = &after + opts.After = after } if before != "" { - opts.Before = &before + opts.Before = before } return opts, nil diff --git a/pkg/github/projects_test.go b/pkg/github/projects_test.go index 9b0e07292f..34839c9345 100644 --- a/pkg/github/projects_test.go +++ b/pkg/github/projects_test.go @@ -9,7 +9,7 @@ import ( "github.com/github/github-mcp-server/internal/githubv4mock" "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - gh "github.com/google/go-github/v82/github" + gh "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/shurcooL/githubv4" "github.com/stretchr/testify/assert" diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index 9c2a098755..e08813d145 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -8,7 +8,7 @@ import ( "net/http" "github.com/go-viper/mapstructure/v2" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/shurcooL/githubv4" diff --git a/pkg/github/pullrequests_granular.go b/pkg/github/pullrequests_granular.go index 4a616f1b25..a28f14ade3 100644 --- a/pkg/github/pullrequests_granular.go +++ b/pkg/github/pullrequests_granular.go @@ -12,7 +12,7 @@ import ( "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" - gogithub "github.com/google/go-github/v82/github" + gogithub "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/shurcooL/githubv4" diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go index 4f0ec9493b..57a8d4924f 100644 --- a/pkg/github/pullrequests_test.go +++ b/pkg/github/pullrequests_test.go @@ -10,7 +10,7 @@ import ( "github.com/github/github-mcp-server/internal/githubv4mock" "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/shurcooL/githubv4" "github.com/stretchr/testify/assert" diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 9577b37b69..7d8229eb07 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -15,7 +15,7 @@ import ( "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/github/repositories_helper.go b/pkg/github/repositories_helper.go index a347ebdd6c..6800038ad2 100644 --- a/pkg/github/repositories_helper.go +++ b/pkg/github/repositories_helper.go @@ -10,7 +10,7 @@ import ( ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/raw" "github.com/github/github-mcp-server/pkg/utils" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/github/repositories_test.go b/pkg/github/repositories_test.go index d7bb487382..458bc0e8de 100644 --- a/pkg/github/repositories_test.go +++ b/pkg/github/repositories_test.go @@ -14,7 +14,7 @@ import ( "github.com/github/github-mcp-server/pkg/raw" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/assert" diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go index be86cc4519..175592be3c 100644 --- a/pkg/github/repository_resource.go +++ b/pkg/github/repository_resource.go @@ -17,7 +17,7 @@ import ( "github.com/github/github-mcp-server/pkg/octicons" "github.com/github/github-mcp-server/pkg/raw" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/yosida95/uritemplate/v3" ) diff --git a/pkg/github/repository_resource_completions.go b/pkg/github/repository_resource_completions.go index ff9e23398a..1979b10479 100644 --- a/pkg/github/repository_resource_completions.go +++ b/pkg/github/repository_resource_completions.go @@ -6,7 +6,7 @@ import ( "fmt" "strings" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/github/repository_resource_completions_test.go b/pkg/github/repository_resource_completions_test.go index e5f1a35f93..1905125675 100644 --- a/pkg/github/repository_resource_completions_test.go +++ b/pkg/github/repository_resource_completions_test.go @@ -6,7 +6,7 @@ import ( "fmt" "testing" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/github/repository_resource_test.go b/pkg/github/repository_resource_test.go index f0fba30dfb..c03630c802 100644 --- a/pkg/github/repository_resource_test.go +++ b/pkg/github/repository_resource_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/github/github-mcp-server/pkg/raw" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/require" ) diff --git a/pkg/github/search.go b/pkg/github/search.go index d5ddb4a72a..c2b7ba724d 100644 --- a/pkg/github/search.go +++ b/pkg/github/search.go @@ -12,7 +12,7 @@ import ( "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/github/search_test.go b/pkg/github/search_test.go index 85eb21bcb5..6b6b1bde1a 100644 --- a/pkg/github/search_test.go +++ b/pkg/github/search_test.go @@ -8,7 +8,7 @@ import ( "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/github/search_utils.go b/pkg/github/search_utils.go index c5502f6308..3c0fe5c4c2 100644 --- a/pkg/github/search_utils.go +++ b/pkg/github/search_utils.go @@ -10,7 +10,7 @@ import ( ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/utils" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/github/secret_scanning.go b/pkg/github/secret_scanning.go index 676c2c1625..61830a373c 100644 --- a/pkg/github/secret_scanning.go +++ b/pkg/github/secret_scanning.go @@ -12,7 +12,7 @@ import ( "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/github/secret_scanning_test.go b/pkg/github/secret_scanning_test.go index 7c53de35c5..af03b5d164 100644 --- a/pkg/github/secret_scanning_test.go +++ b/pkg/github/secret_scanning_test.go @@ -8,7 +8,7 @@ import ( "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/github/security_advisories.go b/pkg/github/security_advisories.go index e86e220eaf..22ffe60fe8 100644 --- a/pkg/github/security_advisories.go +++ b/pkg/github/security_advisories.go @@ -12,7 +12,7 @@ import ( "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/github/security_advisories_test.go b/pkg/github/security_advisories_test.go index 3d4df43e63..6e25f8b811 100644 --- a/pkg/github/security_advisories_test.go +++ b/pkg/github/security_advisories_test.go @@ -8,7 +8,7 @@ import ( "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/google/jsonschema-go/jsonschema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/github/server_test.go b/pkg/github/server_test.go index 264ffa50fe..d54cc7018f 100644 --- a/pkg/github/server_test.go +++ b/pkg/github/server_test.go @@ -16,7 +16,7 @@ import ( "github.com/github/github-mcp-server/pkg/observability/metrics" "github.com/github/github-mcp-server/pkg/raw" "github.com/github/github-mcp-server/pkg/translations" - gogithub "github.com/google/go-github/v82/github" + gogithub "github.com/google/go-github/v86/github" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/shurcooL/githubv4" "github.com/stretchr/testify/assert" diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 559088f6d6..eff79020ad 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -7,7 +7,7 @@ import ( "github.com/github/github-mcp-server/pkg/inventory" "github.com/github/github-mcp-server/pkg/translations" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/shurcooL/githubv4" ) diff --git a/pkg/lockdown/lockdown.go b/pkg/lockdown/lockdown.go index 6edb4469d9..54137538b5 100644 --- a/pkg/lockdown/lockdown.go +++ b/pkg/lockdown/lockdown.go @@ -8,7 +8,7 @@ import ( "sync" "time" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/muesli/cache2go" "github.com/shurcooL/githubv4" ) diff --git a/pkg/lockdown/lockdown_test.go b/pkg/lockdown/lockdown_test.go index 55e755a3ec..94bbb74e09 100644 --- a/pkg/lockdown/lockdown_test.go +++ b/pkg/lockdown/lockdown_test.go @@ -10,7 +10,7 @@ import ( "time" "github.com/github/github-mcp-server/internal/githubv4mock" - gogithub "github.com/google/go-github/v82/github" + gogithub "github.com/google/go-github/v86/github" "github.com/shurcooL/githubv4" "github.com/stretchr/testify/require" ) diff --git a/pkg/raw/raw.go b/pkg/raw/raw.go index df9cd0ad11..f75dbce322 100644 --- a/pkg/raw/raw.go +++ b/pkg/raw/raw.go @@ -6,7 +6,7 @@ import ( "net/http" "net/url" - gogithub "github.com/google/go-github/v82/github" + gogithub "github.com/google/go-github/v86/github" ) // GetRawClientFn is a function type that returns a RawClient instance. @@ -26,11 +26,10 @@ func NewClient(client *gogithub.Client, rawURL *url.URL) *Client { } func (c *Client) newRequest(ctx context.Context, method string, urlStr string, body any, opts ...gogithub.RequestOption) (*http.Request, error) { - req, err := c.client.NewRequest(method, urlStr, body, opts...) + req, err := c.client.NewRequest(ctx, method, urlStr, body, opts...) if err != nil { return nil, err } - req = req.WithContext(ctx) return req, nil } diff --git a/pkg/raw/raw_test.go b/pkg/raw/raw_test.go index 6897f492f6..090ffdbb18 100644 --- a/pkg/raw/raw_test.go +++ b/pkg/raw/raw_test.go @@ -9,7 +9,7 @@ import ( "strings" "testing" - "github.com/google/go-github/v82/github" + "github.com/google/go-github/v86/github" "github.com/stretchr/testify/require" ) From 56f35f53d45421d495d00f9f6c1557e651079910 Mon Sep 17 00:00:00 2001 From: Iulia B Date: Tue, 12 May 2026 13:56:41 +0000 Subject: [PATCH 2/7] add support for fields in issue read --- pkg/github/issues_test.go | 85 +++++++++++++++++++++++++++++++++++++ pkg/github/minimal_types.go | 77 ++++++++++++++++++++++++--------- 2 files changed, 142 insertions(+), 20 deletions(-) diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index ca45c5130b..5f5cd3f1d0 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -275,6 +275,91 @@ func Test_GetIssue(t *testing.T) { } } +func Test_GetIssue_FieldValues(t *testing.T) { + // Verify that issue_field_values from the REST API are present in the returned object. + serverTool := IssueRead(translations.NullTranslationHelper) + + mockIssueWithFields := &github.Issue{ + Number: github.Ptr(99), + Title: github.Ptr("Issue with field values"), + Body: github.Ptr("body"), + State: github.Ptr("open"), + HTMLURL: github.Ptr("https://github.com/owner/repo/issues/99"), + User: &github.User{ + Login: github.Ptr("testuser"), + }, + IssueFieldValues: []*github.IssueFieldValue{ + { + IssueFieldID: 1001, + NodeID: "FV_node_1", + DataType: "single_select", + Value: "High", + SingleSelectOption: &github.IssueFieldValueSingleSelectOption{ + ID: 42, + Name: "High", + Color: "red", + }, + }, + { + IssueFieldID: 1002, + NodeID: "FV_node_2", + DataType: "text", + Value: "some text value", + }, + }, + } + + mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ + GetReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockIssueWithFields), + }) + + client := github.NewClient(mockedClient) + cache := stubRepoAccessCache(nil, 15*time.Minute) + flags := stubFeatureFlags(map[string]bool{"lockdown-mode": false}) + deps := BaseDeps{ + Client: client, + GQLClient: defaultGQLClient, + RepoAccessCache: cache, + Flags: flags, + } + handler := serverTool.Handler(deps) + + request := createMCPRequest(map[string]any{ + "method": "get", + "owner": "owner", + "repo": "repo", + "issue_number": float64(99), + }) + result, err := handler(ContextWithDeps(context.Background(), deps), &request) + require.NoError(t, err) + require.NotNil(t, result) + + textContent := getTextResult(t, result) + + var returnedIssue MinimalIssue + err = json.Unmarshal([]byte(textContent.Text), &returnedIssue) + require.NoError(t, err) + + require.Len(t, returnedIssue.IssueFieldValues, 2, "expected two issue field values") + + first := returnedIssue.IssueFieldValues[0] + assert.Equal(t, int64(1001), first.IssueFieldID) + assert.Equal(t, "FV_node_1", first.NodeID) + assert.Equal(t, "single_select", first.DataType) + assert.Equal(t, "High", first.Value) + require.NotNil(t, first.SingleSelectOption) + assert.Equal(t, int64(42), first.SingleSelectOption.ID) + assert.Equal(t, "High", first.SingleSelectOption.Name) + assert.Equal(t, "red", first.SingleSelectOption.Color) + + second := returnedIssue.IssueFieldValues[1] + assert.Equal(t, int64(1002), second.IssueFieldID) + assert.Equal(t, "FV_node_2", second.NodeID) + assert.Equal(t, "text", second.DataType) + assert.Equal(t, "some text value", second.Value) + assert.Nil(t, second.SingleSelectOption) +} + func Test_AddIssueComment(t *testing.T) { // Verify tool definition once serverTool := AddIssueComment(translations.NullTranslationHelper) diff --git a/pkg/github/minimal_types.go b/pkg/github/minimal_types.go index a458243cd7..872b71f6e0 100644 --- a/pkg/github/minimal_types.go +++ b/pkg/github/minimal_types.go @@ -169,28 +169,45 @@ type MinimalReactions struct { Eyes int `json:"eyes"` } +// MinimalIssueFieldValueSingleSelectOption is the trimmed output type for a single-select option of an issue field value. +type MinimalIssueFieldValueSingleSelectOption struct { + ID int64 `json:"id"` + Name string `json:"name"` + Color string `json:"color"` +} + +// MinimalIssueFieldValue is the trimmed output type for a custom field value attached to an issue. +type MinimalIssueFieldValue struct { + IssueFieldID int64 `json:"issue_field_id"` + NodeID string `json:"node_id"` + DataType string `json:"data_type"` + Value any `json:"value"` + SingleSelectOption *MinimalIssueFieldValueSingleSelectOption `json:"single_select_option,omitempty"` +} + // MinimalIssue is the trimmed output type for issue objects to reduce verbosity. type MinimalIssue struct { - Number int `json:"number"` - Title string `json:"title"` - Body string `json:"body,omitempty"` - State string `json:"state"` - StateReason string `json:"state_reason,omitempty"` - Draft bool `json:"draft,omitempty"` - Locked bool `json:"locked,omitempty"` - HTMLURL string `json:"html_url,omitempty"` - User *MinimalUser `json:"user,omitempty"` - AuthorAssociation string `json:"author_association,omitempty"` - Labels []string `json:"labels,omitempty"` - Assignees []string `json:"assignees,omitempty"` - Milestone string `json:"milestone,omitempty"` - Comments int `json:"comments,omitempty"` - Reactions *MinimalReactions `json:"reactions,omitempty"` - CreatedAt string `json:"created_at,omitempty"` - UpdatedAt string `json:"updated_at,omitempty"` - ClosedAt string `json:"closed_at,omitempty"` - ClosedBy string `json:"closed_by,omitempty"` - IssueType string `json:"issue_type,omitempty"` + Number int `json:"number"` + Title string `json:"title"` + Body string `json:"body,omitempty"` + State string `json:"state"` + StateReason string `json:"state_reason,omitempty"` + Draft bool `json:"draft,omitempty"` + Locked bool `json:"locked,omitempty"` + HTMLURL string `json:"html_url,omitempty"` + User *MinimalUser `json:"user,omitempty"` + AuthorAssociation string `json:"author_association,omitempty"` + Labels []string `json:"labels,omitempty"` + Assignees []string `json:"assignees,omitempty"` + Milestone string `json:"milestone,omitempty"` + Comments int `json:"comments,omitempty"` + Reactions *MinimalReactions `json:"reactions,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` + ClosedAt string `json:"closed_at,omitempty"` + ClosedBy string `json:"closed_by,omitempty"` + IssueType string `json:"issue_type,omitempty"` + IssueFieldValues []MinimalIssueFieldValue `json:"issue_field_values,omitempty"` } // MinimalIssuesResponse is the trimmed output for a paginated list of issues. @@ -368,6 +385,26 @@ func convertToMinimalIssue(issue *github.Issue) MinimalIssue { m.IssueType = issueType.GetName() } + for _, fv := range issue.IssueFieldValues { + if fv == nil { + continue + } + mfv := MinimalIssueFieldValue{ + IssueFieldID: fv.IssueFieldID, + NodeID: fv.NodeID, + DataType: fv.DataType, + Value: fv.Value, + } + if opt := fv.SingleSelectOption; opt != nil { + mfv.SingleSelectOption = &MinimalIssueFieldValueSingleSelectOption{ + ID: opt.ID, + Name: opt.Name, + Color: opt.Color, + } + } + m.IssueFieldValues = append(m.IssueFieldValues, mfv) + } + if r := issue.Reactions; r != nil { m.Reactions = &MinimalReactions{ TotalCount: r.GetTotalCount(), From 2fcbc88abdf44ddda7464115359b84da4ec7978d Mon Sep 17 00:00:00 2001 From: Iulia B Date: Tue, 12 May 2026 13:56:58 +0000 Subject: [PATCH 3/7] add support for fields in issues write --- pkg/github/__toolsnaps__/issue_write.snap | 23 +++ pkg/github/issues.go | 203 +++++++++++++++++++++- pkg/github/issues_test.go | 148 +++++++++++++++- 3 files changed, 365 insertions(+), 9 deletions(-) diff --git a/pkg/github/__toolsnaps__/issue_write.snap b/pkg/github/__toolsnaps__/issue_write.snap index 24cff5df97..599c490f1b 100644 --- a/pkg/github/__toolsnaps__/issue_write.snap +++ b/pkg/github/__toolsnaps__/issue_write.snap @@ -29,6 +29,29 @@ "description": "Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'.", "type": "number" }, + "issue_fields": { + "description": "Issue field values to set. Each item requires field_name and either value or field_option_name. field_option_name is for single-select fields and is resolved to the corresponding option ID automatically.", + "items": { + "properties": { + "field_name": { + "description": "Issue field name", + "type": "string" + }, + "field_option_name": { + "description": "Single-select option name to resolve and set for the field", + "type": "string" + }, + "value": { + "description": "Value for text/number/date/single-select fields. For single-select, you can use field_option_name instead." + } + }, + "required": [ + "field_name" + ], + "type": "object" + }, + "type": "array" + }, "issue_number": { "description": "Issue number to update", "type": "number" diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 9415b71066..a0949a2074 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -34,6 +34,36 @@ type CloseIssueInput struct { // Used to extend the functionality of the githubv4 library to support closing issues as duplicates. type IssueClosedStateReason string +// IssueWriteFieldInput is a user-friendly issue field input for issue_write. +// Field IDs and option IDs are resolved internally before calling the REST API. +type IssueWriteFieldInput struct { + FieldName string + Value any + FieldOptionName string +} + +type issueFieldMetadataOption struct { + DatabaseID githubv4.Int `graphql:"databaseId"` + Name githubv4.String +} + +type issueFieldMetadataNode struct { + DatabaseID githubv4.Int `graphql:"databaseId"` + Name githubv4.String + DataType githubv4.String + SingleSelectField struct { + Options []issueFieldMetadataOption `graphql:"options"` + } `graphql:"... on IssueFieldSingleSelect"` +} + +type issueFieldMetadataQuery struct { + Repository struct { + IssueFields struct { + Nodes []issueFieldMetadataNode + } `graphql:"issueFields(first: 100)"` + } `graphql:"repository(owner: $owner, name: $repo)"` +} + const ( IssueClosedStateReasonCompleted IssueClosedStateReason = "COMPLETED" IssueClosedStateReasonDuplicate IssueClosedStateReason = "DUPLICATE" @@ -102,6 +132,127 @@ func getCloseStateReason(stateReason string) IssueClosedStateReason { } } +func optionalIssueWriteFields(args map[string]any) ([]IssueWriteFieldInput, error) { + issueFieldsRaw, exists := args["issue_fields"] + if !exists { + return nil, nil + } + + var inputMaps []map[string]any + switch v := issueFieldsRaw.(type) { + case []any: + for _, item := range v { + itemMap, ok := item.(map[string]any) + if !ok { + return nil, fmt.Errorf("each issue_fields item must be an object") + } + inputMaps = append(inputMaps, itemMap) + } + case []map[string]any: + inputMaps = v + default: + return nil, fmt.Errorf("issue_fields must be an array") + } + + issueFields := make([]IssueWriteFieldInput, 0, len(inputMaps)) + for _, itemMap := range inputMaps { + fieldName, err := RequiredParam[string](itemMap, "field_name") + if err != nil || strings.TrimSpace(fieldName) == "" { + return nil, fmt.Errorf("field_name is required for each issue_fields item") + } + + fieldOptionName, err := OptionalParam[string](itemMap, "field_option_name") + if err != nil { + return nil, err + } + + value, hasValue := itemMap["value"] + if hasValue && value == nil { + return nil, fmt.Errorf("value cannot be null for field %q", fieldName) + } + + if hasValue && fieldOptionName != "" { + return nil, fmt.Errorf("issue field %q cannot specify both value and field_option_name", fieldName) + } + + if !hasValue && fieldOptionName == "" { + return nil, fmt.Errorf("issue field %q must specify either value or field_option_name", fieldName) + } + + issueFields = append(issueFields, IssueWriteFieldInput{ + FieldName: fieldName, + Value: value, + FieldOptionName: fieldOptionName, + }) + } + + return issueFields, nil +} + +func resolveIssueRequestFieldValues(ctx context.Context, gqlClient *githubv4.Client, owner, repo string, issueFields []IssueWriteFieldInput) ([]*github.IssueRequestFieldValue, error) { + if len(issueFields) == 0 { + return nil, nil + } + + query := issueFieldMetadataQuery{} + vars := map[string]any{ + "owner": githubv4.String(owner), + "repo": githubv4.String(repo), + } + if err := gqlClient.Query(ctx, &query, vars); err != nil { + return nil, fmt.Errorf("failed to query issue fields metadata: %w", err) + } + + fieldByName := make(map[string]issueFieldMetadataNode, len(query.Repository.IssueFields.Nodes)) + for _, field := range query.Repository.IssueFields.Nodes { + fieldByName[strings.ToLower(strings.TrimSpace(string(field.Name)))] = field + } + + resolved := make([]*github.IssueRequestFieldValue, 0, len(issueFields)) + for _, fieldInput := range issueFields { + field, ok := fieldByName[strings.ToLower(strings.TrimSpace(fieldInput.FieldName))] + if !ok { + return nil, fmt.Errorf("issue field %q was not found in %s/%s", fieldInput.FieldName, owner, repo) + } + + fieldID := int64(field.DatabaseID) + if fieldID == 0 { + return nil, fmt.Errorf("issue field %q is missing databaseId", fieldInput.FieldName) + } + + resolvedValue := fieldInput.Value + if fieldInput.FieldOptionName != "" { + if !strings.EqualFold(string(field.DataType), "single_select") { + return nil, fmt.Errorf("issue field %q is %q, so field_option_name cannot be used", fieldInput.FieldName, field.DataType) + } + + optionFound := false + for _, option := range field.SingleSelectField.Options { + if strings.EqualFold(strings.TrimSpace(string(option.Name)), strings.TrimSpace(fieldInput.FieldOptionName)) { + optionID := int64(option.DatabaseID) + if optionID == 0 { + return nil, fmt.Errorf("issue field option %q on field %q is missing databaseId", fieldInput.FieldOptionName, fieldInput.FieldName) + } + resolvedValue = optionID + optionFound = true + break + } + } + + if !optionFound { + return nil, fmt.Errorf("issue field option %q was not found for field %q", fieldInput.FieldOptionName, fieldInput.FieldName) + } + } + + resolved = append(resolved, &github.IssueRequestFieldValue{ + FieldID: fieldID, + Value: resolvedValue, + }) + } + + return resolved, nil +} + // IssueFragment represents a fragment of an issue node in the GraphQL API. type IssueFragment struct { Number githubv4.Int @@ -1053,6 +1204,27 @@ Options are: Type: "number", Description: "Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'.", }, + "issue_fields": { + Type: "array", + Description: "Issue field values to set. Each item requires field_name and either value or field_option_name. field_option_name is for single-select fields and is resolved to the corresponding option ID automatically.", + Items: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "field_name": { + Type: "string", + Description: "Issue field name", + }, + "value": { + Description: "Value for text/number/date/single-select fields. For single-select, you can use field_option_name instead.", + }, + "field_option_name": { + Type: "string", + Description: "Single-select option name to resolve and set for the field", + }, + }, + Required: []string{"field_name"}, + }, + }, }, Required: []string{"method", "owner", "repo"}, }, @@ -1154,6 +1326,11 @@ Options are: return utils.NewToolResultError("duplicate_of can only be used when state_reason is 'duplicate'"), nil, nil } + issueFields, err := optionalIssueWriteFields(args) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + client, err := deps.GetClient(ctx) if err != nil { return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil @@ -1164,16 +1341,21 @@ Options are: return utils.NewToolResultErrorFromErr("failed to get GraphQL client", err), nil, nil } + issueFieldValues, err := resolveIssueRequestFieldValues(ctx, gqlClient, owner, repo, issueFields) + if err != nil { + return utils.NewToolResultError(fmt.Sprintf("failed to resolve issue_fields: %v", err)), nil, nil + } + switch method { case "create": - result, err := CreateIssue(ctx, client, owner, repo, title, body, assignees, labels, milestoneNum, issueType) + result, err := CreateIssue(ctx, client, owner, repo, title, body, assignees, labels, milestoneNum, issueType, issueFieldValues) return result, nil, err case "update": issueNumber, err := RequiredInt(args, "issue_number") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } - result, err := UpdateIssue(ctx, client, gqlClient, owner, repo, issueNumber, title, body, assignees, labels, milestoneNum, issueType, state, stateReason, duplicateOf) + result, err := UpdateIssue(ctx, client, gqlClient, owner, repo, issueNumber, title, body, assignees, labels, milestoneNum, issueType, issueFieldValues, state, stateReason, duplicateOf) return result, nil, err default: return utils.NewToolResultError("invalid method, must be either 'create' or 'update'"), nil, nil @@ -1183,17 +1365,18 @@ Options are: return st } -func CreateIssue(ctx context.Context, client *github.Client, owner string, repo string, title string, body string, assignees []string, labels []string, milestoneNum int, issueType string) (*mcp.CallToolResult, error) { +func CreateIssue(ctx context.Context, client *github.Client, owner string, repo string, title string, body string, assignees []string, labels []string, milestoneNum int, issueType string, issueFieldValues []*github.IssueRequestFieldValue) (*mcp.CallToolResult, error) { if title == "" { return utils.NewToolResultError("missing required parameter: title"), nil } // Create the issue request issueRequest := &github.IssueRequest{ - Title: github.Ptr(title), - Body: github.Ptr(body), - Assignees: &assignees, - Labels: &labels, + Title: github.Ptr(title), + Body: github.Ptr(body), + Assignees: &assignees, + Labels: &labels, + IssueFieldValues: issueFieldValues, } if milestoneNum != 0 { @@ -1236,7 +1419,7 @@ func CreateIssue(ctx context.Context, client *github.Client, owner string, repo return utils.NewToolResultText(string(r)), nil } -func UpdateIssue(ctx context.Context, client *github.Client, gqlClient *githubv4.Client, owner string, repo string, issueNumber int, title string, body string, assignees []string, labels []string, milestoneNum int, issueType string, state string, stateReason string, duplicateOf int) (*mcp.CallToolResult, error) { +func UpdateIssue(ctx context.Context, client *github.Client, gqlClient *githubv4.Client, owner string, repo string, issueNumber int, title string, body string, assignees []string, labels []string, milestoneNum int, issueType string, issueFieldValues []*github.IssueRequestFieldValue, state string, stateReason string, duplicateOf int) (*mcp.CallToolResult, error) { // Create the issue request with only provided fields issueRequest := &github.IssueRequest{} @@ -1265,6 +1448,10 @@ func UpdateIssue(ctx context.Context, client *github.Client, gqlClient *githubv4 issueRequest.Type = github.Ptr(issueType) } + if len(issueFieldValues) > 0 { + issueRequest.IssueFieldValues = issueFieldValues + } + updatedIssue, resp, err := client.Issues.Edit(ctx, owner, repo, issueNumber, issueRequest) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index 5f5cd3f1d0..8fcdae8e0a 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -795,6 +795,7 @@ func Test_CreateIssue(t *testing.T) { assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "labels") assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "milestone") assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "type") + assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "issue_fields") assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"method", "owner", "repo"}) // Setup mock issue for success case @@ -813,6 +814,7 @@ func Test_CreateIssue(t *testing.T) { tests := []struct { name string mockedClient *http.Client + mockedGQLClient *http.Client requestArgs map[string]any expectError bool expectedIssue *github.Issue @@ -871,6 +873,75 @@ func Test_CreateIssue(t *testing.T) { State: github.Ptr("open"), }, }, + { + name: "successful issue creation with issue fields reconciled by names", + mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ + PostReposIssuesByOwnerByRepo: expectRequestBody(t, map[string]any{ + "title": "Issue with fields", + "body": "", + "labels": []any{}, + "assignees": []any{}, + "issue_field_values": []any{ + map[string]any{"field_id": float64(101), "value": float64(9001)}, + map[string]any{"field_id": float64(102), "value": "Acme"}, + }, + }).andThen( + mockResponse(t, http.StatusCreated, &github.Issue{ + Number: github.Ptr(125), + Title: github.Ptr("Issue with fields"), + HTMLURL: github.Ptr("https://github.com/owner/repo/issues/125"), + State: github.Ptr("open"), + }), + ), + }), + mockedGQLClient: githubv4mock.NewMockedHTTPClient( + githubv4mock.NewQueryMatcher( + issueFieldMetadataQuery{}, + map[string]any{ + "owner": githubv4.String("owner"), + "repo": githubv4.String("repo"), + }, + githubv4mock.DataResponse(map[string]any{ + "repository": map[string]any{ + "issueFields": map[string]any{ + "nodes": []map[string]any{ + { + "databaseId": 101, + "name": "Priority", + "dataType": "single_select", + "options": []map[string]any{ + {"databaseId": 9001, "name": "P1"}, + }, + }, + { + "databaseId": 102, + "name": "Customer", + "dataType": "text", + }, + }, + }, + }, + }), + ), + ), + requestArgs: map[string]any{ + "method": "create", + "owner": "owner", + "repo": "repo", + "title": "Issue with fields", + "issue_fields": []any{ + map[string]any{"field_name": "Priority", "field_option_name": "P1"}, + map[string]any{"field_name": "Customer", "value": "Acme"}, + }, + }, + expectError: false, + expectedIssue: &github.Issue{ + Number: github.Ptr(125), + Title: github.Ptr("Issue with fields"), + HTMLURL: github.Ptr("https://github.com/owner/repo/issues/125"), + State: github.Ptr("open"), + }, + }, { name: "issue creation fails", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ @@ -888,13 +959,32 @@ func Test_CreateIssue(t *testing.T) { expectError: false, expectedErrMsg: "missing required parameter: title", }, + { + name: "issue_fields rejects both value and field_option_name", + mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), + requestArgs: map[string]any{ + "method": "create", + "owner": "owner", + "repo": "repo", + "title": "Invalid fields", + "issue_fields": []any{ + map[string]any{"field_name": "Priority", "value": "P1", "field_option_name": "P1"}, + }, + }, + expectError: false, + expectedErrMsg: "cannot specify both value and field_option_name", + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - gqlClient := githubv4.NewClient(nil) + gqlHTTPClient := tc.mockedGQLClient + if gqlHTTPClient == nil { + gqlHTTPClient = githubv4mock.NewMockedHTTPClient() + } + gqlClient := githubv4.NewClient(gqlHTTPClient) deps := BaseDeps{ Client: client, GQLClient: gqlClient, @@ -1455,6 +1545,7 @@ func Test_UpdateIssue(t *testing.T) { assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "state") assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "state_reason") assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "duplicate_of") + assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "issue_fields") assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"method", "owner", "repo"}) // Mock issues for reuse across test cases @@ -1566,6 +1657,61 @@ func Test_UpdateIssue(t *testing.T) { expectError: false, expectedIssue: mockUpdatedIssue, }, + { + name: "partial update with issue fields reconciled by names", + mockedRESTClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ + PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, map[string]any{ + "issue_field_values": []any{ + map[string]any{"field_id": float64(101), "value": float64(9001)}, + map[string]any{"field_id": float64(102), "value": "Acme"}, + }, + "title": "Updated Title", + }).andThen( + mockResponse(t, http.StatusOK, mockUpdatedIssue), + ), + }), + mockedGQLClient: githubv4mock.NewMockedHTTPClient( + githubv4mock.NewQueryMatcher( + issueFieldMetadataQuery{}, + map[string]any{ + "owner": githubv4.String("owner"), + "repo": githubv4.String("repo"), + }, + githubv4mock.DataResponse(map[string]any{ + "repository": map[string]any{ + "issueFields": map[string]any{ + "nodes": []map[string]any{ + { + "databaseId": 101, + "name": "Priority", + "dataType": "single_select", + "options": []map[string]any{{"databaseId": 9001, "name": "P1"}}, + }, + { + "databaseId": 102, + "name": "Customer", + "dataType": "text", + }, + }, + }, + }, + }), + ), + ), + requestArgs: map[string]any{ + "method": "update", + "owner": "owner", + "repo": "repo", + "issue_number": float64(123), + "title": "Updated Title", + "issue_fields": []any{ + map[string]any{"field_name": "Priority", "field_option_name": "P1"}, + map[string]any{"field_name": "Customer", "value": "Acme"}, + }, + }, + expectError: false, + expectedIssue: mockUpdatedIssue, + }, { name: "issue not found when updating non-state fields only", mockedRESTClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ From 21ddc1d308e8b62e92e338d3343e1bdec6c51291 Mon Sep 17 00:00:00 2001 From: Iulia B Date: Tue, 12 May 2026 14:32:05 +0000 Subject: [PATCH 4/7] add issues write support --- pkg/github/issues.go | 6 +++--- pkg/github/issues_test.go | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/github/issues.go b/pkg/github/issues.go index a0949a2074..96395387e1 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -48,9 +48,9 @@ type issueFieldMetadataOption struct { } type issueFieldMetadataNode struct { - DatabaseID githubv4.Int `graphql:"databaseId"` - Name githubv4.String - DataType githubv4.String + DatabaseID githubv4.Int `graphql:"databaseId"` + Name githubv4.String + DataType githubv4.String SingleSelectField struct { Options []issueFieldMetadataOption `graphql:"options"` } `graphql:"... on IssueFieldSingleSelect"` diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index 8fcdae8e0a..6efce79762 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -812,13 +812,13 @@ func Test_CreateIssue(t *testing.T) { } tests := []struct { - name string - mockedClient *http.Client + name string + mockedClient *http.Client mockedGQLClient *http.Client - requestArgs map[string]any - expectError bool - expectedIssue *github.Issue - expectedErrMsg string + requestArgs map[string]any + expectError bool + expectedIssue *github.Issue + expectedErrMsg string }{ { name: "successful issue creation with all fields", @@ -1685,7 +1685,7 @@ func Test_UpdateIssue(t *testing.T) { "databaseId": 101, "name": "Priority", "dataType": "single_select", - "options": []map[string]any{{"databaseId": 9001, "name": "P1"}}, + "options": []map[string]any{{"databaseId": 9001, "name": "P1"}}, }, { "databaseId": 102, From 93180e8d509bbe494f05953eb29f8c53c926c24c Mon Sep 17 00:00:00 2001 From: Iulia B Date: Tue, 12 May 2026 14:36:30 +0000 Subject: [PATCH 5/7] Revert "add issues write support" This reverts commit 21ddc1d308e8b62e92e338d3343e1bdec6c51291. --- pkg/github/issues.go | 6 +++--- pkg/github/issues_test.go | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 96395387e1..a0949a2074 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -48,9 +48,9 @@ type issueFieldMetadataOption struct { } type issueFieldMetadataNode struct { - DatabaseID githubv4.Int `graphql:"databaseId"` - Name githubv4.String - DataType githubv4.String + DatabaseID githubv4.Int `graphql:"databaseId"` + Name githubv4.String + DataType githubv4.String SingleSelectField struct { Options []issueFieldMetadataOption `graphql:"options"` } `graphql:"... on IssueFieldSingleSelect"` diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index 6efce79762..8fcdae8e0a 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -812,13 +812,13 @@ func Test_CreateIssue(t *testing.T) { } tests := []struct { - name string - mockedClient *http.Client + name string + mockedClient *http.Client mockedGQLClient *http.Client - requestArgs map[string]any - expectError bool - expectedIssue *github.Issue - expectedErrMsg string + requestArgs map[string]any + expectError bool + expectedIssue *github.Issue + expectedErrMsg string }{ { name: "successful issue creation with all fields", @@ -1685,7 +1685,7 @@ func Test_UpdateIssue(t *testing.T) { "databaseId": 101, "name": "Priority", "dataType": "single_select", - "options": []map[string]any{{"databaseId": 9001, "name": "P1"}}, + "options": []map[string]any{{"databaseId": 9001, "name": "P1"}}, }, { "databaseId": 102, From 0c986e909146d23018cbdda82c4f35b296e44825 Mon Sep 17 00:00:00 2001 From: Iulia B Date: Tue, 12 May 2026 14:36:30 +0000 Subject: [PATCH 6/7] Revert "add support for fields in issues write" This reverts commit 2fcbc88abdf44ddda7464115359b84da4ec7978d. --- pkg/github/__toolsnaps__/issue_write.snap | 23 --- pkg/github/issues.go | 203 +--------------------- pkg/github/issues_test.go | 148 +--------------- 3 files changed, 9 insertions(+), 365 deletions(-) diff --git a/pkg/github/__toolsnaps__/issue_write.snap b/pkg/github/__toolsnaps__/issue_write.snap index 599c490f1b..24cff5df97 100644 --- a/pkg/github/__toolsnaps__/issue_write.snap +++ b/pkg/github/__toolsnaps__/issue_write.snap @@ -29,29 +29,6 @@ "description": "Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'.", "type": "number" }, - "issue_fields": { - "description": "Issue field values to set. Each item requires field_name and either value or field_option_name. field_option_name is for single-select fields and is resolved to the corresponding option ID automatically.", - "items": { - "properties": { - "field_name": { - "description": "Issue field name", - "type": "string" - }, - "field_option_name": { - "description": "Single-select option name to resolve and set for the field", - "type": "string" - }, - "value": { - "description": "Value for text/number/date/single-select fields. For single-select, you can use field_option_name instead." - } - }, - "required": [ - "field_name" - ], - "type": "object" - }, - "type": "array" - }, "issue_number": { "description": "Issue number to update", "type": "number" diff --git a/pkg/github/issues.go b/pkg/github/issues.go index a0949a2074..9415b71066 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -34,36 +34,6 @@ type CloseIssueInput struct { // Used to extend the functionality of the githubv4 library to support closing issues as duplicates. type IssueClosedStateReason string -// IssueWriteFieldInput is a user-friendly issue field input for issue_write. -// Field IDs and option IDs are resolved internally before calling the REST API. -type IssueWriteFieldInput struct { - FieldName string - Value any - FieldOptionName string -} - -type issueFieldMetadataOption struct { - DatabaseID githubv4.Int `graphql:"databaseId"` - Name githubv4.String -} - -type issueFieldMetadataNode struct { - DatabaseID githubv4.Int `graphql:"databaseId"` - Name githubv4.String - DataType githubv4.String - SingleSelectField struct { - Options []issueFieldMetadataOption `graphql:"options"` - } `graphql:"... on IssueFieldSingleSelect"` -} - -type issueFieldMetadataQuery struct { - Repository struct { - IssueFields struct { - Nodes []issueFieldMetadataNode - } `graphql:"issueFields(first: 100)"` - } `graphql:"repository(owner: $owner, name: $repo)"` -} - const ( IssueClosedStateReasonCompleted IssueClosedStateReason = "COMPLETED" IssueClosedStateReasonDuplicate IssueClosedStateReason = "DUPLICATE" @@ -132,127 +102,6 @@ func getCloseStateReason(stateReason string) IssueClosedStateReason { } } -func optionalIssueWriteFields(args map[string]any) ([]IssueWriteFieldInput, error) { - issueFieldsRaw, exists := args["issue_fields"] - if !exists { - return nil, nil - } - - var inputMaps []map[string]any - switch v := issueFieldsRaw.(type) { - case []any: - for _, item := range v { - itemMap, ok := item.(map[string]any) - if !ok { - return nil, fmt.Errorf("each issue_fields item must be an object") - } - inputMaps = append(inputMaps, itemMap) - } - case []map[string]any: - inputMaps = v - default: - return nil, fmt.Errorf("issue_fields must be an array") - } - - issueFields := make([]IssueWriteFieldInput, 0, len(inputMaps)) - for _, itemMap := range inputMaps { - fieldName, err := RequiredParam[string](itemMap, "field_name") - if err != nil || strings.TrimSpace(fieldName) == "" { - return nil, fmt.Errorf("field_name is required for each issue_fields item") - } - - fieldOptionName, err := OptionalParam[string](itemMap, "field_option_name") - if err != nil { - return nil, err - } - - value, hasValue := itemMap["value"] - if hasValue && value == nil { - return nil, fmt.Errorf("value cannot be null for field %q", fieldName) - } - - if hasValue && fieldOptionName != "" { - return nil, fmt.Errorf("issue field %q cannot specify both value and field_option_name", fieldName) - } - - if !hasValue && fieldOptionName == "" { - return nil, fmt.Errorf("issue field %q must specify either value or field_option_name", fieldName) - } - - issueFields = append(issueFields, IssueWriteFieldInput{ - FieldName: fieldName, - Value: value, - FieldOptionName: fieldOptionName, - }) - } - - return issueFields, nil -} - -func resolveIssueRequestFieldValues(ctx context.Context, gqlClient *githubv4.Client, owner, repo string, issueFields []IssueWriteFieldInput) ([]*github.IssueRequestFieldValue, error) { - if len(issueFields) == 0 { - return nil, nil - } - - query := issueFieldMetadataQuery{} - vars := map[string]any{ - "owner": githubv4.String(owner), - "repo": githubv4.String(repo), - } - if err := gqlClient.Query(ctx, &query, vars); err != nil { - return nil, fmt.Errorf("failed to query issue fields metadata: %w", err) - } - - fieldByName := make(map[string]issueFieldMetadataNode, len(query.Repository.IssueFields.Nodes)) - for _, field := range query.Repository.IssueFields.Nodes { - fieldByName[strings.ToLower(strings.TrimSpace(string(field.Name)))] = field - } - - resolved := make([]*github.IssueRequestFieldValue, 0, len(issueFields)) - for _, fieldInput := range issueFields { - field, ok := fieldByName[strings.ToLower(strings.TrimSpace(fieldInput.FieldName))] - if !ok { - return nil, fmt.Errorf("issue field %q was not found in %s/%s", fieldInput.FieldName, owner, repo) - } - - fieldID := int64(field.DatabaseID) - if fieldID == 0 { - return nil, fmt.Errorf("issue field %q is missing databaseId", fieldInput.FieldName) - } - - resolvedValue := fieldInput.Value - if fieldInput.FieldOptionName != "" { - if !strings.EqualFold(string(field.DataType), "single_select") { - return nil, fmt.Errorf("issue field %q is %q, so field_option_name cannot be used", fieldInput.FieldName, field.DataType) - } - - optionFound := false - for _, option := range field.SingleSelectField.Options { - if strings.EqualFold(strings.TrimSpace(string(option.Name)), strings.TrimSpace(fieldInput.FieldOptionName)) { - optionID := int64(option.DatabaseID) - if optionID == 0 { - return nil, fmt.Errorf("issue field option %q on field %q is missing databaseId", fieldInput.FieldOptionName, fieldInput.FieldName) - } - resolvedValue = optionID - optionFound = true - break - } - } - - if !optionFound { - return nil, fmt.Errorf("issue field option %q was not found for field %q", fieldInput.FieldOptionName, fieldInput.FieldName) - } - } - - resolved = append(resolved, &github.IssueRequestFieldValue{ - FieldID: fieldID, - Value: resolvedValue, - }) - } - - return resolved, nil -} - // IssueFragment represents a fragment of an issue node in the GraphQL API. type IssueFragment struct { Number githubv4.Int @@ -1204,27 +1053,6 @@ Options are: Type: "number", Description: "Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'.", }, - "issue_fields": { - Type: "array", - Description: "Issue field values to set. Each item requires field_name and either value or field_option_name. field_option_name is for single-select fields and is resolved to the corresponding option ID automatically.", - Items: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "field_name": { - Type: "string", - Description: "Issue field name", - }, - "value": { - Description: "Value for text/number/date/single-select fields. For single-select, you can use field_option_name instead.", - }, - "field_option_name": { - Type: "string", - Description: "Single-select option name to resolve and set for the field", - }, - }, - Required: []string{"field_name"}, - }, - }, }, Required: []string{"method", "owner", "repo"}, }, @@ -1326,11 +1154,6 @@ Options are: return utils.NewToolResultError("duplicate_of can only be used when state_reason is 'duplicate'"), nil, nil } - issueFields, err := optionalIssueWriteFields(args) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - client, err := deps.GetClient(ctx) if err != nil { return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil @@ -1341,21 +1164,16 @@ Options are: return utils.NewToolResultErrorFromErr("failed to get GraphQL client", err), nil, nil } - issueFieldValues, err := resolveIssueRequestFieldValues(ctx, gqlClient, owner, repo, issueFields) - if err != nil { - return utils.NewToolResultError(fmt.Sprintf("failed to resolve issue_fields: %v", err)), nil, nil - } - switch method { case "create": - result, err := CreateIssue(ctx, client, owner, repo, title, body, assignees, labels, milestoneNum, issueType, issueFieldValues) + result, err := CreateIssue(ctx, client, owner, repo, title, body, assignees, labels, milestoneNum, issueType) return result, nil, err case "update": issueNumber, err := RequiredInt(args, "issue_number") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } - result, err := UpdateIssue(ctx, client, gqlClient, owner, repo, issueNumber, title, body, assignees, labels, milestoneNum, issueType, issueFieldValues, state, stateReason, duplicateOf) + result, err := UpdateIssue(ctx, client, gqlClient, owner, repo, issueNumber, title, body, assignees, labels, milestoneNum, issueType, state, stateReason, duplicateOf) return result, nil, err default: return utils.NewToolResultError("invalid method, must be either 'create' or 'update'"), nil, nil @@ -1365,18 +1183,17 @@ Options are: return st } -func CreateIssue(ctx context.Context, client *github.Client, owner string, repo string, title string, body string, assignees []string, labels []string, milestoneNum int, issueType string, issueFieldValues []*github.IssueRequestFieldValue) (*mcp.CallToolResult, error) { +func CreateIssue(ctx context.Context, client *github.Client, owner string, repo string, title string, body string, assignees []string, labels []string, milestoneNum int, issueType string) (*mcp.CallToolResult, error) { if title == "" { return utils.NewToolResultError("missing required parameter: title"), nil } // Create the issue request issueRequest := &github.IssueRequest{ - Title: github.Ptr(title), - Body: github.Ptr(body), - Assignees: &assignees, - Labels: &labels, - IssueFieldValues: issueFieldValues, + Title: github.Ptr(title), + Body: github.Ptr(body), + Assignees: &assignees, + Labels: &labels, } if milestoneNum != 0 { @@ -1419,7 +1236,7 @@ func CreateIssue(ctx context.Context, client *github.Client, owner string, repo return utils.NewToolResultText(string(r)), nil } -func UpdateIssue(ctx context.Context, client *github.Client, gqlClient *githubv4.Client, owner string, repo string, issueNumber int, title string, body string, assignees []string, labels []string, milestoneNum int, issueType string, issueFieldValues []*github.IssueRequestFieldValue, state string, stateReason string, duplicateOf int) (*mcp.CallToolResult, error) { +func UpdateIssue(ctx context.Context, client *github.Client, gqlClient *githubv4.Client, owner string, repo string, issueNumber int, title string, body string, assignees []string, labels []string, milestoneNum int, issueType string, state string, stateReason string, duplicateOf int) (*mcp.CallToolResult, error) { // Create the issue request with only provided fields issueRequest := &github.IssueRequest{} @@ -1448,10 +1265,6 @@ func UpdateIssue(ctx context.Context, client *github.Client, gqlClient *githubv4 issueRequest.Type = github.Ptr(issueType) } - if len(issueFieldValues) > 0 { - issueRequest.IssueFieldValues = issueFieldValues - } - updatedIssue, resp, err := client.Issues.Edit(ctx, owner, repo, issueNumber, issueRequest) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index 8fcdae8e0a..5f5cd3f1d0 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -795,7 +795,6 @@ func Test_CreateIssue(t *testing.T) { assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "labels") assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "milestone") assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "type") - assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "issue_fields") assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"method", "owner", "repo"}) // Setup mock issue for success case @@ -814,7 +813,6 @@ func Test_CreateIssue(t *testing.T) { tests := []struct { name string mockedClient *http.Client - mockedGQLClient *http.Client requestArgs map[string]any expectError bool expectedIssue *github.Issue @@ -873,75 +871,6 @@ func Test_CreateIssue(t *testing.T) { State: github.Ptr("open"), }, }, - { - name: "successful issue creation with issue fields reconciled by names", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - PostReposIssuesByOwnerByRepo: expectRequestBody(t, map[string]any{ - "title": "Issue with fields", - "body": "", - "labels": []any{}, - "assignees": []any{}, - "issue_field_values": []any{ - map[string]any{"field_id": float64(101), "value": float64(9001)}, - map[string]any{"field_id": float64(102), "value": "Acme"}, - }, - }).andThen( - mockResponse(t, http.StatusCreated, &github.Issue{ - Number: github.Ptr(125), - Title: github.Ptr("Issue with fields"), - HTMLURL: github.Ptr("https://github.com/owner/repo/issues/125"), - State: github.Ptr("open"), - }), - ), - }), - mockedGQLClient: githubv4mock.NewMockedHTTPClient( - githubv4mock.NewQueryMatcher( - issueFieldMetadataQuery{}, - map[string]any{ - "owner": githubv4.String("owner"), - "repo": githubv4.String("repo"), - }, - githubv4mock.DataResponse(map[string]any{ - "repository": map[string]any{ - "issueFields": map[string]any{ - "nodes": []map[string]any{ - { - "databaseId": 101, - "name": "Priority", - "dataType": "single_select", - "options": []map[string]any{ - {"databaseId": 9001, "name": "P1"}, - }, - }, - { - "databaseId": 102, - "name": "Customer", - "dataType": "text", - }, - }, - }, - }, - }), - ), - ), - requestArgs: map[string]any{ - "method": "create", - "owner": "owner", - "repo": "repo", - "title": "Issue with fields", - "issue_fields": []any{ - map[string]any{"field_name": "Priority", "field_option_name": "P1"}, - map[string]any{"field_name": "Customer", "value": "Acme"}, - }, - }, - expectError: false, - expectedIssue: &github.Issue{ - Number: github.Ptr(125), - Title: github.Ptr("Issue with fields"), - HTMLURL: github.Ptr("https://github.com/owner/repo/issues/125"), - State: github.Ptr("open"), - }, - }, { name: "issue creation fails", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ @@ -959,32 +888,13 @@ func Test_CreateIssue(t *testing.T) { expectError: false, expectedErrMsg: "missing required parameter: title", }, - { - name: "issue_fields rejects both value and field_option_name", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "method": "create", - "owner": "owner", - "repo": "repo", - "title": "Invalid fields", - "issue_fields": []any{ - map[string]any{"field_name": "Priority", "value": "P1", "field_option_name": "P1"}, - }, - }, - expectError: false, - expectedErrMsg: "cannot specify both value and field_option_name", - }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - gqlHTTPClient := tc.mockedGQLClient - if gqlHTTPClient == nil { - gqlHTTPClient = githubv4mock.NewMockedHTTPClient() - } - gqlClient := githubv4.NewClient(gqlHTTPClient) + gqlClient := githubv4.NewClient(nil) deps := BaseDeps{ Client: client, GQLClient: gqlClient, @@ -1545,7 +1455,6 @@ func Test_UpdateIssue(t *testing.T) { assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "state") assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "state_reason") assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "duplicate_of") - assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "issue_fields") assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"method", "owner", "repo"}) // Mock issues for reuse across test cases @@ -1657,61 +1566,6 @@ func Test_UpdateIssue(t *testing.T) { expectError: false, expectedIssue: mockUpdatedIssue, }, - { - name: "partial update with issue fields reconciled by names", - mockedRESTClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, map[string]any{ - "issue_field_values": []any{ - map[string]any{"field_id": float64(101), "value": float64(9001)}, - map[string]any{"field_id": float64(102), "value": "Acme"}, - }, - "title": "Updated Title", - }).andThen( - mockResponse(t, http.StatusOK, mockUpdatedIssue), - ), - }), - mockedGQLClient: githubv4mock.NewMockedHTTPClient( - githubv4mock.NewQueryMatcher( - issueFieldMetadataQuery{}, - map[string]any{ - "owner": githubv4.String("owner"), - "repo": githubv4.String("repo"), - }, - githubv4mock.DataResponse(map[string]any{ - "repository": map[string]any{ - "issueFields": map[string]any{ - "nodes": []map[string]any{ - { - "databaseId": 101, - "name": "Priority", - "dataType": "single_select", - "options": []map[string]any{{"databaseId": 9001, "name": "P1"}}, - }, - { - "databaseId": 102, - "name": "Customer", - "dataType": "text", - }, - }, - }, - }, - }), - ), - ), - requestArgs: map[string]any{ - "method": "update", - "owner": "owner", - "repo": "repo", - "issue_number": float64(123), - "title": "Updated Title", - "issue_fields": []any{ - map[string]any{"field_name": "Priority", "field_option_name": "P1"}, - map[string]any{"field_name": "Customer", "value": "Acme"}, - }, - }, - expectError: false, - expectedIssue: mockUpdatedIssue, - }, { name: "issue not found when updating non-state fields only", mockedRESTClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ From 8da9972c2c54e9268053350d84532caa0485898f Mon Sep 17 00:00:00 2001 From: Iulia B Date: Tue, 12 May 2026 14:36:30 +0000 Subject: [PATCH 7/7] Revert "add support for fields in issue read" This reverts commit 56f35f53d45421d495d00f9f6c1557e651079910. --- pkg/github/issues_test.go | 85 ------------------------------------- pkg/github/minimal_types.go | 77 +++++++++------------------------ 2 files changed, 20 insertions(+), 142 deletions(-) diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index 5f5cd3f1d0..ca45c5130b 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -275,91 +275,6 @@ func Test_GetIssue(t *testing.T) { } } -func Test_GetIssue_FieldValues(t *testing.T) { - // Verify that issue_field_values from the REST API are present in the returned object. - serverTool := IssueRead(translations.NullTranslationHelper) - - mockIssueWithFields := &github.Issue{ - Number: github.Ptr(99), - Title: github.Ptr("Issue with field values"), - Body: github.Ptr("body"), - State: github.Ptr("open"), - HTMLURL: github.Ptr("https://github.com/owner/repo/issues/99"), - User: &github.User{ - Login: github.Ptr("testuser"), - }, - IssueFieldValues: []*github.IssueFieldValue{ - { - IssueFieldID: 1001, - NodeID: "FV_node_1", - DataType: "single_select", - Value: "High", - SingleSelectOption: &github.IssueFieldValueSingleSelectOption{ - ID: 42, - Name: "High", - Color: "red", - }, - }, - { - IssueFieldID: 1002, - NodeID: "FV_node_2", - DataType: "text", - Value: "some text value", - }, - }, - } - - mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockIssueWithFields), - }) - - client := github.NewClient(mockedClient) - cache := stubRepoAccessCache(nil, 15*time.Minute) - flags := stubFeatureFlags(map[string]bool{"lockdown-mode": false}) - deps := BaseDeps{ - Client: client, - GQLClient: defaultGQLClient, - RepoAccessCache: cache, - Flags: flags, - } - handler := serverTool.Handler(deps) - - request := createMCPRequest(map[string]any{ - "method": "get", - "owner": "owner", - "repo": "repo", - "issue_number": float64(99), - }) - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - require.NoError(t, err) - require.NotNil(t, result) - - textContent := getTextResult(t, result) - - var returnedIssue MinimalIssue - err = json.Unmarshal([]byte(textContent.Text), &returnedIssue) - require.NoError(t, err) - - require.Len(t, returnedIssue.IssueFieldValues, 2, "expected two issue field values") - - first := returnedIssue.IssueFieldValues[0] - assert.Equal(t, int64(1001), first.IssueFieldID) - assert.Equal(t, "FV_node_1", first.NodeID) - assert.Equal(t, "single_select", first.DataType) - assert.Equal(t, "High", first.Value) - require.NotNil(t, first.SingleSelectOption) - assert.Equal(t, int64(42), first.SingleSelectOption.ID) - assert.Equal(t, "High", first.SingleSelectOption.Name) - assert.Equal(t, "red", first.SingleSelectOption.Color) - - second := returnedIssue.IssueFieldValues[1] - assert.Equal(t, int64(1002), second.IssueFieldID) - assert.Equal(t, "FV_node_2", second.NodeID) - assert.Equal(t, "text", second.DataType) - assert.Equal(t, "some text value", second.Value) - assert.Nil(t, second.SingleSelectOption) -} - func Test_AddIssueComment(t *testing.T) { // Verify tool definition once serverTool := AddIssueComment(translations.NullTranslationHelper) diff --git a/pkg/github/minimal_types.go b/pkg/github/minimal_types.go index 872b71f6e0..a458243cd7 100644 --- a/pkg/github/minimal_types.go +++ b/pkg/github/minimal_types.go @@ -169,45 +169,28 @@ type MinimalReactions struct { Eyes int `json:"eyes"` } -// MinimalIssueFieldValueSingleSelectOption is the trimmed output type for a single-select option of an issue field value. -type MinimalIssueFieldValueSingleSelectOption struct { - ID int64 `json:"id"` - Name string `json:"name"` - Color string `json:"color"` -} - -// MinimalIssueFieldValue is the trimmed output type for a custom field value attached to an issue. -type MinimalIssueFieldValue struct { - IssueFieldID int64 `json:"issue_field_id"` - NodeID string `json:"node_id"` - DataType string `json:"data_type"` - Value any `json:"value"` - SingleSelectOption *MinimalIssueFieldValueSingleSelectOption `json:"single_select_option,omitempty"` -} - // MinimalIssue is the trimmed output type for issue objects to reduce verbosity. type MinimalIssue struct { - Number int `json:"number"` - Title string `json:"title"` - Body string `json:"body,omitempty"` - State string `json:"state"` - StateReason string `json:"state_reason,omitempty"` - Draft bool `json:"draft,omitempty"` - Locked bool `json:"locked,omitempty"` - HTMLURL string `json:"html_url,omitempty"` - User *MinimalUser `json:"user,omitempty"` - AuthorAssociation string `json:"author_association,omitempty"` - Labels []string `json:"labels,omitempty"` - Assignees []string `json:"assignees,omitempty"` - Milestone string `json:"milestone,omitempty"` - Comments int `json:"comments,omitempty"` - Reactions *MinimalReactions `json:"reactions,omitempty"` - CreatedAt string `json:"created_at,omitempty"` - UpdatedAt string `json:"updated_at,omitempty"` - ClosedAt string `json:"closed_at,omitempty"` - ClosedBy string `json:"closed_by,omitempty"` - IssueType string `json:"issue_type,omitempty"` - IssueFieldValues []MinimalIssueFieldValue `json:"issue_field_values,omitempty"` + Number int `json:"number"` + Title string `json:"title"` + Body string `json:"body,omitempty"` + State string `json:"state"` + StateReason string `json:"state_reason,omitempty"` + Draft bool `json:"draft,omitempty"` + Locked bool `json:"locked,omitempty"` + HTMLURL string `json:"html_url,omitempty"` + User *MinimalUser `json:"user,omitempty"` + AuthorAssociation string `json:"author_association,omitempty"` + Labels []string `json:"labels,omitempty"` + Assignees []string `json:"assignees,omitempty"` + Milestone string `json:"milestone,omitempty"` + Comments int `json:"comments,omitempty"` + Reactions *MinimalReactions `json:"reactions,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` + ClosedAt string `json:"closed_at,omitempty"` + ClosedBy string `json:"closed_by,omitempty"` + IssueType string `json:"issue_type,omitempty"` } // MinimalIssuesResponse is the trimmed output for a paginated list of issues. @@ -385,26 +368,6 @@ func convertToMinimalIssue(issue *github.Issue) MinimalIssue { m.IssueType = issueType.GetName() } - for _, fv := range issue.IssueFieldValues { - if fv == nil { - continue - } - mfv := MinimalIssueFieldValue{ - IssueFieldID: fv.IssueFieldID, - NodeID: fv.NodeID, - DataType: fv.DataType, - Value: fv.Value, - } - if opt := fv.SingleSelectOption; opt != nil { - mfv.SingleSelectOption = &MinimalIssueFieldValueSingleSelectOption{ - ID: opt.ID, - Name: opt.Name, - Color: opt.Color, - } - } - m.IssueFieldValues = append(m.IssueFieldValues, mfv) - } - if r := issue.Reactions; r != nil { m.Reactions = &MinimalReactions{ TotalCount: r.GetTotalCount(),