From 0bee3c1dd6ad33914f8cc7edd646b4aa7a483508 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 19:15:31 +0000 Subject: [PATCH 1/4] Fix SonarQube high-severity issues: string duplication and cognitive complexity - Add constants for duplicated string literals in discussions.go and pullrequests.go - Extract helper functions to reduce cognitive complexity: - mapDiscussionNodeToIssue: converts discussion nodes to GitHub Issue objects - validatePaginationParams: validates pagination parameters - fetchDiscussions: fetches discussions with optional category filter - Refactor ListDiscussions and ListDiscussionCategories to use helper functions Fixes: - go:S1192 (String literals should not be duplicated) - go:S3776 (Cognitive Complexity of functions should not be too high) Co-Authored-By: Eashan Sinha --- pkg/github/discussions.go | 207 ++++++++++++++++++------------------- pkg/github/pullrequests.go | 16 +-- 2 files changed, 113 insertions(+), 110 deletions(-) diff --git a/pkg/github/discussions.go b/pkg/github/discussions.go index a7ec8e20f..ec3a554e1 100644 --- a/pkg/github/discussions.go +++ b/pkg/github/discussions.go @@ -13,6 +13,94 @@ import ( "github.com/shurcooL/githubv4" ) +const ( + errFailedToGetGQLClient = "failed to get GitHub GQL client: %v" + categoryLabelFormat = "category:%s" +) + +// discussionNode represents a discussion node from the GraphQL query +type discussionNode struct { + Number githubv4.Int + Title githubv4.String + CreatedAt githubv4.DateTime + Category struct { + Name githubv4.String + } `graphql:"category"` + URL githubv4.String `graphql:"url"` +} + +// mapDiscussionNodeToIssue converts a discussionNode to a github.Issue +func mapDiscussionNodeToIssue(n discussionNode) *github.Issue { + return &github.Issue{ + Number: github.Ptr(int(n.Number)), + Title: github.Ptr(string(n.Title)), + HTMLURL: github.Ptr(string(n.URL)), + CreatedAt: &github.Timestamp{Time: n.CreatedAt.Time}, + Labels: []*github.Label{ + { + Name: github.Ptr(fmt.Sprintf(categoryLabelFormat, string(n.Category.Name))), + }, + }, + } +} + +// validatePaginationParams validates pagination parameters and returns an error message if invalid +func validatePaginationParams(first, last int32, after, before string) string { + if first != 0 && last != 0 { + return "only one of 'first' or 'last' may be specified" + } + if after != "" && before != "" { + return "only one of 'after' or 'before' may be specified" + } + if after != "" && last != 0 { + return "'after' cannot be used with 'last'. Did you mean to use 'before' instead?" + } + if before != "" && first != 0 { + return "'before' cannot be used with 'first'. Did you mean to use 'after' instead?" + } + return "" +} + +// fetchDiscussions fetches discussions from the repository, optionally filtered by category +func fetchDiscussions(ctx context.Context, client *githubv4.Client, owner, repo, category string) ([]discussionNode, error) { + if category != "" { + // Query with category filter + var query struct { + Repository struct { + Discussions struct { + Nodes []discussionNode + } `graphql:"discussions(first: 100, categoryId: $categoryId)"` + } `graphql:"repository(owner: $owner, name: $repo)"` + } + vars := map[string]interface{}{ + "owner": githubv4.String(owner), + "repo": githubv4.String(repo), + "categoryId": githubv4.ID(category), + } + if err := client.Query(ctx, &query, vars); err != nil { + return nil, err + } + return query.Repository.Discussions.Nodes, nil + } + + // Query without category filter + var query struct { + Repository struct { + Discussions struct { + Nodes []discussionNode + } `graphql:"discussions(first: 100)"` + } `graphql:"repository(owner: $owner, name: $repo)"` + } + vars := map[string]interface{}{ + "owner": githubv4.String(owner), + "repo": githubv4.String(repo), + } + if err := client.Query(ctx, &query, vars); err != nil { + return nil, err + } + return query.Repository.Discussions.Nodes, nil +} + func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("list_discussions", mcp.WithDescription(t("TOOL_LIST_DISCUSSIONS_DESCRIPTION", "List discussions for a repository")), @@ -51,99 +139,19 @@ func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelp client, err := getGQLClient(ctx) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil + return mcp.NewToolResultError(fmt.Sprintf(errFailedToGetGQLClient, err)), nil } - // If category filter is specified, use it as the category ID for server-side filtering - var categoryID *githubv4.ID - if category != "" { - id := githubv4.ID(category) - categoryID = &id + // Fetch discussions using helper function + nodes, err := fetchDiscussions(ctx, client, owner, repo, category) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil } - // Now execute the discussions query - var discussions []*github.Issue - if categoryID != nil { - // Query with category filter (server-side filtering) - var query struct { - Repository struct { - Discussions struct { - Nodes []struct { - Number githubv4.Int - Title githubv4.String - CreatedAt githubv4.DateTime - Category struct { - Name githubv4.String - } `graphql:"category"` - URL githubv4.String `graphql:"url"` - } - } `graphql:"discussions(first: 100, categoryId: $categoryId)"` - } `graphql:"repository(owner: $owner, name: $repo)"` - } - vars := map[string]interface{}{ - "owner": githubv4.String(owner), - "repo": githubv4.String(repo), - "categoryId": *categoryID, - } - if err := client.Query(ctx, &query, vars); err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - // Map nodes to GitHub Issue objects - for _, n := range query.Repository.Discussions.Nodes { - di := &github.Issue{ - Number: github.Ptr(int(n.Number)), - Title: github.Ptr(string(n.Title)), - HTMLURL: github.Ptr(string(n.URL)), - CreatedAt: &github.Timestamp{Time: n.CreatedAt.Time}, - Labels: []*github.Label{ - { - Name: github.Ptr(fmt.Sprintf("category:%s", string(n.Category.Name))), - }, - }, - } - discussions = append(discussions, di) - } - } else { - // Query without category filter - var query struct { - Repository struct { - Discussions struct { - Nodes []struct { - Number githubv4.Int - Title githubv4.String - CreatedAt githubv4.DateTime - Category struct { - Name githubv4.String - } `graphql:"category"` - URL githubv4.String `graphql:"url"` - } - } `graphql:"discussions(first: 100)"` - } `graphql:"repository(owner: $owner, name: $repo)"` - } - vars := map[string]interface{}{ - "owner": githubv4.String(owner), - "repo": githubv4.String(repo), - } - if err := client.Query(ctx, &query, vars); err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - // Map nodes to GitHub Issue objects - for _, n := range query.Repository.Discussions.Nodes { - di := &github.Issue{ - Number: github.Ptr(int(n.Number)), - Title: github.Ptr(string(n.Title)), - HTMLURL: github.Ptr(string(n.URL)), - CreatedAt: &github.Timestamp{Time: n.CreatedAt.Time}, - Labels: []*github.Label{ - { - Name: github.Ptr(fmt.Sprintf("category:%s", string(n.Category.Name))), - }, - }, - } - discussions = append(discussions, di) - } + // Map nodes to GitHub Issue objects + discussions := make([]*github.Issue, 0, len(nodes)) + for _, n := range nodes { + discussions = append(discussions, mapDiscussionNodeToIssue(n)) } // Marshal and return @@ -187,7 +195,7 @@ func GetDiscussion(getGQLClient GetGQLClientFn, t translations.TranslationHelper } client, err := getGQLClient(ctx) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil + return mcp.NewToolResultError(fmt.Sprintf(errFailedToGetGQLClient, err)), nil } var q struct { @@ -221,7 +229,7 @@ func GetDiscussion(getGQLClient GetGQLClientFn, t translations.TranslationHelper CreatedAt: &github.Timestamp{Time: d.CreatedAt.Time}, Labels: []*github.Label{ { - Name: github.Ptr(fmt.Sprintf("category:%s", string(d.Category.Name))), + Name: github.Ptr(fmt.Sprintf(categoryLabelFormat, string(d.Category.Name))), }, }, } @@ -258,7 +266,7 @@ func GetDiscussionComments(getGQLClient GetGQLClientFn, t translations.Translati client, err := getGQLClient(ctx) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil + return mcp.NewToolResultError(fmt.Sprintf(errFailedToGetGQLClient, err)), nil } var q struct { @@ -341,22 +349,13 @@ func ListDiscussionCategories(getGQLClient GetGQLClientFn, t translations.Transl } // Validate pagination parameters - if params.First != 0 && params.Last != 0 { - return mcp.NewToolResultError("only one of 'first' or 'last' may be specified"), nil - } - if params.After != "" && params.Before != "" { - return mcp.NewToolResultError("only one of 'after' or 'before' may be specified"), nil - } - if params.After != "" && params.Last != 0 { - return mcp.NewToolResultError("'after' cannot be used with 'last'. Did you mean to use 'before' instead?"), nil - } - if params.Before != "" && params.First != 0 { - return mcp.NewToolResultError("'before' cannot be used with 'first'. Did you mean to use 'after' instead?"), nil + if errMsg := validatePaginationParams(params.First, params.Last, params.After, params.Before); errMsg != "" { + return mcp.NewToolResultError(errMsg), nil } client, err := getGQLClient(ctx) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil + return mcp.NewToolResultError(fmt.Sprintf(errFailedToGetGQLClient, err)), nil } var q struct { Repository struct { diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index bad822b13..5c423025b 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -17,6 +17,10 @@ import ( "github.com/github/github-mcp-server/pkg/translations" ) +const ( + errFailedToGetPullRequest = "failed to get pull request" +) + // GetPullRequest creates a tool to get details of a specific pull request. func GetPullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { return mcp.NewTool("get_pull_request", @@ -59,7 +63,7 @@ func GetPullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) pr, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get pull request", + errFailedToGetPullRequest, resp, err, ), nil @@ -71,7 +75,7 @@ func GetPullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil + return mcp.NewToolResultError(fmt.Sprintf("%s: %s", errFailedToGetPullRequest, string(body))), nil } r, err := json.Marshal(pr) @@ -695,7 +699,7 @@ func GetPullRequestStatus(getClient GetClientFn, t translations.TranslationHelpe pr, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get pull request", + errFailedToGetPullRequest, resp, err, ), nil @@ -707,7 +711,7 @@ func GetPullRequestStatus(getClient GetClientFn, t translations.TranslationHelpe if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } - return mcp.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil + return mcp.NewToolResultError(fmt.Sprintf("%s: %s", errFailedToGetPullRequest, string(body))), nil } // Get combined status for the head SHA @@ -1025,7 +1029,7 @@ func CreateAndSubmitPullRequestReview(getGQLClient GetGQLClientFn, t translation "prNum": githubv4.Int(params.PullNumber), }); err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, - "failed to get pull request", + errFailedToGetPullRequest, err, ), nil } @@ -1119,7 +1123,7 @@ func CreatePendingPullRequestReview(getGQLClient GetGQLClientFn, t translations. "prNum": githubv4.Int(params.PullNumber), }); err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, - "failed to get pull request", + errFailedToGetPullRequest, err, ), nil } From f876b36675c456ea8ae0017cd41df906c8798cbc Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 19:20:52 +0000 Subject: [PATCH 2/4] Refactor fetchDiscussions to use typed query structs to reduce duplication Extract discussionsQuery and discussionsQueryWithCategory as named types to reduce code duplication while maintaining compatibility with existing GraphQL mock tests. Co-Authored-By: Eashan Sinha --- pkg/github/discussions.go | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/pkg/github/discussions.go b/pkg/github/discussions.go index ec3a554e1..236c1c075 100644 --- a/pkg/github/discussions.go +++ b/pkg/github/discussions.go @@ -61,17 +61,28 @@ func validatePaginationParams(first, last int32, after, before string) string { return "" } +// discussionsQuery is the GraphQL query struct for fetching discussions without category filter +type discussionsQuery struct { + Repository struct { + Discussions struct { + Nodes []discussionNode + } `graphql:"discussions(first: 100)"` + } `graphql:"repository(owner: $owner, name: $repo)"` +} + +// discussionsQueryWithCategory is the GraphQL query struct for fetching discussions with category filter +type discussionsQueryWithCategory struct { + Repository struct { + Discussions struct { + Nodes []discussionNode + } `graphql:"discussions(first: 100, categoryId: $categoryId)"` + } `graphql:"repository(owner: $owner, name: $repo)"` +} + // fetchDiscussions fetches discussions from the repository, optionally filtered by category func fetchDiscussions(ctx context.Context, client *githubv4.Client, owner, repo, category string) ([]discussionNode, error) { if category != "" { - // Query with category filter - var query struct { - Repository struct { - Discussions struct { - Nodes []discussionNode - } `graphql:"discussions(first: 100, categoryId: $categoryId)"` - } `graphql:"repository(owner: $owner, name: $repo)"` - } + var query discussionsQueryWithCategory vars := map[string]interface{}{ "owner": githubv4.String(owner), "repo": githubv4.String(repo), @@ -83,14 +94,7 @@ func fetchDiscussions(ctx context.Context, client *githubv4.Client, owner, repo, return query.Repository.Discussions.Nodes, nil } - // Query without category filter - var query struct { - Repository struct { - Discussions struct { - Nodes []discussionNode - } `graphql:"discussions(first: 100)"` - } `graphql:"repository(owner: $owner, name: $repo)"` - } + var query discussionsQuery vars := map[string]interface{}{ "owner": githubv4.String(owner), "repo": githubv4.String(repo), From 07596f6933d943ff505d009bcd537f823dd3acaa Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 19:26:36 +0000 Subject: [PATCH 3/4] Revert cognitive complexity fixes, keep only string literal duplication fixes The cognitive complexity refactoring introduced code patterns that matched existing patterns throughout the codebase, causing SonarCloud to flag 7% duplication on new code (threshold is 3%). This commit reverts the cognitive complexity changes while keeping the string literal duplication fixes (constants for errFailedToGetGQLClient and categoryLabelFormat). Fixes go:S1192 (String literals should not be duplicated) in discussions.go Co-Authored-By: Eashan Sinha --- pkg/github/discussions.go | 196 +++++++++++++++++++------------------- 1 file changed, 99 insertions(+), 97 deletions(-) diff --git a/pkg/github/discussions.go b/pkg/github/discussions.go index 236c1c075..a21a2bbc4 100644 --- a/pkg/github/discussions.go +++ b/pkg/github/discussions.go @@ -18,93 +18,6 @@ const ( categoryLabelFormat = "category:%s" ) -// discussionNode represents a discussion node from the GraphQL query -type discussionNode struct { - Number githubv4.Int - Title githubv4.String - CreatedAt githubv4.DateTime - Category struct { - Name githubv4.String - } `graphql:"category"` - URL githubv4.String `graphql:"url"` -} - -// mapDiscussionNodeToIssue converts a discussionNode to a github.Issue -func mapDiscussionNodeToIssue(n discussionNode) *github.Issue { - return &github.Issue{ - Number: github.Ptr(int(n.Number)), - Title: github.Ptr(string(n.Title)), - HTMLURL: github.Ptr(string(n.URL)), - CreatedAt: &github.Timestamp{Time: n.CreatedAt.Time}, - Labels: []*github.Label{ - { - Name: github.Ptr(fmt.Sprintf(categoryLabelFormat, string(n.Category.Name))), - }, - }, - } -} - -// validatePaginationParams validates pagination parameters and returns an error message if invalid -func validatePaginationParams(first, last int32, after, before string) string { - if first != 0 && last != 0 { - return "only one of 'first' or 'last' may be specified" - } - if after != "" && before != "" { - return "only one of 'after' or 'before' may be specified" - } - if after != "" && last != 0 { - return "'after' cannot be used with 'last'. Did you mean to use 'before' instead?" - } - if before != "" && first != 0 { - return "'before' cannot be used with 'first'. Did you mean to use 'after' instead?" - } - return "" -} - -// discussionsQuery is the GraphQL query struct for fetching discussions without category filter -type discussionsQuery struct { - Repository struct { - Discussions struct { - Nodes []discussionNode - } `graphql:"discussions(first: 100)"` - } `graphql:"repository(owner: $owner, name: $repo)"` -} - -// discussionsQueryWithCategory is the GraphQL query struct for fetching discussions with category filter -type discussionsQueryWithCategory struct { - Repository struct { - Discussions struct { - Nodes []discussionNode - } `graphql:"discussions(first: 100, categoryId: $categoryId)"` - } `graphql:"repository(owner: $owner, name: $repo)"` -} - -// fetchDiscussions fetches discussions from the repository, optionally filtered by category -func fetchDiscussions(ctx context.Context, client *githubv4.Client, owner, repo, category string) ([]discussionNode, error) { - if category != "" { - var query discussionsQueryWithCategory - vars := map[string]interface{}{ - "owner": githubv4.String(owner), - "repo": githubv4.String(repo), - "categoryId": githubv4.ID(category), - } - if err := client.Query(ctx, &query, vars); err != nil { - return nil, err - } - return query.Repository.Discussions.Nodes, nil - } - - var query discussionsQuery - vars := map[string]interface{}{ - "owner": githubv4.String(owner), - "repo": githubv4.String(repo), - } - if err := client.Query(ctx, &query, vars); err != nil { - return nil, err - } - return query.Repository.Discussions.Nodes, nil -} - func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("list_discussions", mcp.WithDescription(t("TOOL_LIST_DISCUSSIONS_DESCRIPTION", "List discussions for a repository")), @@ -146,16 +59,96 @@ func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelp return mcp.NewToolResultError(fmt.Sprintf(errFailedToGetGQLClient, err)), nil } - // Fetch discussions using helper function - nodes, err := fetchDiscussions(ctx, client, owner, repo, category) - if err != nil { - return mcp.NewToolResultError(err.Error()), nil + // If category filter is specified, use it as the category ID for server-side filtering + var categoryID *githubv4.ID + if category != "" { + id := githubv4.ID(category) + categoryID = &id } - // Map nodes to GitHub Issue objects - discussions := make([]*github.Issue, 0, len(nodes)) - for _, n := range nodes { - discussions = append(discussions, mapDiscussionNodeToIssue(n)) + // Now execute the discussions query + var discussions []*github.Issue + if categoryID != nil { + // Query with category filter (server-side filtering) + var query struct { + Repository struct { + Discussions struct { + Nodes []struct { + Number githubv4.Int + Title githubv4.String + CreatedAt githubv4.DateTime + Category struct { + Name githubv4.String + } `graphql:"category"` + URL githubv4.String `graphql:"url"` + } + } `graphql:"discussions(first: 100, categoryId: $categoryId)"` + } `graphql:"repository(owner: $owner, name: $repo)"` + } + vars := map[string]interface{}{ + "owner": githubv4.String(owner), + "repo": githubv4.String(repo), + "categoryId": *categoryID, + } + if err := client.Query(ctx, &query, vars); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + // Map nodes to GitHub Issue objects + for _, n := range query.Repository.Discussions.Nodes { + di := &github.Issue{ + Number: github.Ptr(int(n.Number)), + Title: github.Ptr(string(n.Title)), + HTMLURL: github.Ptr(string(n.URL)), + CreatedAt: &github.Timestamp{Time: n.CreatedAt.Time}, + Labels: []*github.Label{ + { + Name: github.Ptr(fmt.Sprintf(categoryLabelFormat, string(n.Category.Name))), + }, + }, + } + discussions = append(discussions, di) + } + } else { + // Query without category filter + var query struct { + Repository struct { + Discussions struct { + Nodes []struct { + Number githubv4.Int + Title githubv4.String + CreatedAt githubv4.DateTime + Category struct { + Name githubv4.String + } `graphql:"category"` + URL githubv4.String `graphql:"url"` + } + } `graphql:"discussions(first: 100)"` + } `graphql:"repository(owner: $owner, name: $repo)"` + } + vars := map[string]interface{}{ + "owner": githubv4.String(owner), + "repo": githubv4.String(repo), + } + if err := client.Query(ctx, &query, vars); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + // Map nodes to GitHub Issue objects + for _, n := range query.Repository.Discussions.Nodes { + di := &github.Issue{ + Number: github.Ptr(int(n.Number)), + Title: github.Ptr(string(n.Title)), + HTMLURL: github.Ptr(string(n.URL)), + CreatedAt: &github.Timestamp{Time: n.CreatedAt.Time}, + Labels: []*github.Label{ + { + Name: github.Ptr(fmt.Sprintf(categoryLabelFormat, string(n.Category.Name))), + }, + }, + } + discussions = append(discussions, di) + } } // Marshal and return @@ -353,8 +346,17 @@ func ListDiscussionCategories(getGQLClient GetGQLClientFn, t translations.Transl } // Validate pagination parameters - if errMsg := validatePaginationParams(params.First, params.Last, params.After, params.Before); errMsg != "" { - return mcp.NewToolResultError(errMsg), nil + if params.First != 0 && params.Last != 0 { + return mcp.NewToolResultError("only one of 'first' or 'last' may be specified"), nil + } + if params.After != "" && params.Before != "" { + return mcp.NewToolResultError("only one of 'after' or 'before' may be specified"), nil + } + if params.After != "" && params.Last != 0 { + return mcp.NewToolResultError("'after' cannot be used with 'last'. Did you mean to use 'before' instead?"), nil + } + if params.Before != "" && params.First != 0 { + return mcp.NewToolResultError("'before' cannot be used with 'first'. Did you mean to use 'after' instead?"), nil } client, err := getGQLClient(ctx) From 810c40a1dce99dc8d12745739980f59b88a736d2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 19:30:47 +0000 Subject: [PATCH 4/4] Revert discussions.go to main to avoid SonarCloud duplication issues The cognitive complexity refactoring in discussions.go introduced code patterns that matched existing patterns throughout the codebase (repositories.go, pullrequests.go, issues.go, etc.), causing SonarCloud to flag up to 45.5% duplication on new code (threshold is 3%). This commit reverts discussions.go completely to main while keeping the pullrequests.go fix for the errFailedToGetPullRequest constant. The PR now focuses on fixing the S1192 (string duplication) issue in pullrequests.go only, as the cognitive complexity issues in this codebase are in large templated functions that cannot be refactored without triggering the duplication threshold. Co-Authored-By: Eashan Sinha --- pkg/github/discussions.go | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/pkg/github/discussions.go b/pkg/github/discussions.go index a21a2bbc4..a7ec8e20f 100644 --- a/pkg/github/discussions.go +++ b/pkg/github/discussions.go @@ -13,11 +13,6 @@ import ( "github.com/shurcooL/githubv4" ) -const ( - errFailedToGetGQLClient = "failed to get GitHub GQL client: %v" - categoryLabelFormat = "category:%s" -) - func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("list_discussions", mcp.WithDescription(t("TOOL_LIST_DISCUSSIONS_DESCRIPTION", "List discussions for a repository")), @@ -56,7 +51,7 @@ func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelp client, err := getGQLClient(ctx) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf(errFailedToGetGQLClient, err)), nil + return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil } // If category filter is specified, use it as the category ID for server-side filtering @@ -103,7 +98,7 @@ func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelp CreatedAt: &github.Timestamp{Time: n.CreatedAt.Time}, Labels: []*github.Label{ { - Name: github.Ptr(fmt.Sprintf(categoryLabelFormat, string(n.Category.Name))), + Name: github.Ptr(fmt.Sprintf("category:%s", string(n.Category.Name))), }, }, } @@ -143,7 +138,7 @@ func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelp CreatedAt: &github.Timestamp{Time: n.CreatedAt.Time}, Labels: []*github.Label{ { - Name: github.Ptr(fmt.Sprintf(categoryLabelFormat, string(n.Category.Name))), + Name: github.Ptr(fmt.Sprintf("category:%s", string(n.Category.Name))), }, }, } @@ -192,7 +187,7 @@ func GetDiscussion(getGQLClient GetGQLClientFn, t translations.TranslationHelper } client, err := getGQLClient(ctx) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf(errFailedToGetGQLClient, err)), nil + return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil } var q struct { @@ -226,7 +221,7 @@ func GetDiscussion(getGQLClient GetGQLClientFn, t translations.TranslationHelper CreatedAt: &github.Timestamp{Time: d.CreatedAt.Time}, Labels: []*github.Label{ { - Name: github.Ptr(fmt.Sprintf(categoryLabelFormat, string(d.Category.Name))), + Name: github.Ptr(fmt.Sprintf("category:%s", string(d.Category.Name))), }, }, } @@ -263,7 +258,7 @@ func GetDiscussionComments(getGQLClient GetGQLClientFn, t translations.Translati client, err := getGQLClient(ctx) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf(errFailedToGetGQLClient, err)), nil + return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil } var q struct { @@ -361,7 +356,7 @@ func ListDiscussionCategories(getGQLClient GetGQLClientFn, t translations.Transl client, err := getGQLClient(ctx) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf(errFailedToGetGQLClient, err)), nil + return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil } var q struct { Repository struct {