From fcca9062aa2a12b7c43c8f4aefd4f60bbf7a4a44 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Fri, 2 Oct 2026 11:21:54 +0200 Subject: [PATCH] build: upgrade golangci-lint to v2.14.0 Resolve new modernization findings and document narrow false-positive suppressions. Verify Go 1.26.8 and 1.27.1 support and remove obsolete export-data compatibility guidance. Refresh cached linter binaries when the repository pin changes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/lint.yml | 2 +- CONTRIBUTING.md | 4 +--- internal/oauth/testutil_test.go | 2 +- pkg/buffer/buffer_test.go | 4 ++-- pkg/errors/error.go | 6 ++---- pkg/github/actions.go | 3 +-- pkg/github/copilot.go | 4 ++-- pkg/github/projects.go | 21 +++++++-------------- pkg/github/projects_batch.go | 6 ++---- pkg/github/projects_batch_mutation.go | 2 +- pkg/github/projects_batch_test.go | 12 ++++++------ pkg/github/projects_resolver.go | 2 +- pkg/github/secret_scanning.go | 4 ++-- pkg/http/middleware/pat_scope_test.go | 2 +- pkg/http/middleware/scope_challenge_test.go | 2 +- pkg/http/middleware/token_test.go | 10 +++++----- pkg/http/transport/etag_test.go | 12 ++++++------ pkg/http/transport/user_agent_test.go | 2 +- pkg/inventory/server_tool.go | 5 +++-- pkg/toolvalidation/readonlyhint.go | 2 +- script/lint | 4 ++-- 21 files changed, 49 insertions(+), 62 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e4acc354b7..568ea1ca18 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,4 +23,4 @@ jobs: uses: golangci/golangci-lint-action@v9 with: # sync with script/lint - version: v2.9 + version: v2.14.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 76af5d31f5..8c8f294b36 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,9 +27,7 @@ Thanks for contributing and for helping us build toolsets that are truly valuabl These are one time installations required to be able to test your changes locally as part of the pull request (PR) submission process. 1. Install Go 1.26.8 or later [through download](https://go.dev/doc/install) | [through Homebrew](https://formulae.brew.sh/formula/go) -2. [Install golangci-lint v2](https://golangci-lint.run/welcome/install/#local-installation) - -The repository-pinned golangci-lint v2.9.0 supports Go 1.26, but cannot read Go 1.27 export data. When using a newer Go installation, run lint with `GOTOOLCHAIN=go1.26.8 script/lint`. +2. [Install golangci-lint v2.14.0](https://golangci-lint.run/welcome/install/#local-installation), or let `script/lint` install the repository-pinned version. The pinned version supports both Go 1.26 and Go 1.27. ## Submitting a pull request diff --git a/internal/oauth/testutil_test.go b/internal/oauth/testutil_test.go index dc34c3841c..5c0ac501f7 100644 --- a/internal/oauth/testutil_test.go +++ b/internal/oauth/testutil_test.go @@ -73,7 +73,7 @@ func (f *fakeGitHub) handleAuthorize(w http.ResponseWriter, r *http.Request) { f.mu.Unlock() redirect := q.Get("redirect_uri") + "?code=authcode&state=" + url.QueryEscape(q.Get("state")) - http.Redirect(w, r, redirect, http.StatusFound) + http.Redirect(w, r, redirect, http.StatusFound) //nolint:gosec // G710: Fake OAuth server redirects to the test client's callback, not an external user's input. } func (f *fakeGitHub) handleToken(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/buffer/buffer_test.go b/pkg/buffer/buffer_test.go index 86308ec5e3..b66c3bc20b 100644 --- a/pkg/buffer/buffer_test.go +++ b/pkg/buffer/buffer_test.go @@ -83,14 +83,14 @@ func TestProcessResponseAsRingBufferToEnd(t *testing.T) { // Ring buffer size is 5, so we should only keep the last 5 lines var sb strings.Builder for i := 1; i <= 10; i++ { - sb.WriteString(fmt.Sprintf("line%d\n", i)) + fmt.Fprintf(&sb, "line%d\n", i) } // Insert an 11MB line (exceeds maxLineSize of 10MB) longLine := strings.Repeat("x", 11*1024*1024) sb.WriteString(longLine) sb.WriteString("\n") for i := 11; i <= 20; i++ { - sb.WriteString(fmt.Sprintf("line%d\n", i)) + fmt.Fprintf(&sb, "line%d\n", i) } resp := &http.Response{ diff --git a/pkg/errors/error.go b/pkg/errors/error.go index cc3e542cc9..3e45e53081 100644 --- a/pkg/errors/error.go +++ b/pkg/errors/error.go @@ -165,8 +165,7 @@ func NewGitHubAPIErrorResponse(ctx context.Context, message string, resp *github _, _ = addGitHubAPIErrorToContext(ctx, apiErr) // Explicitly ignore error for graceful handling } - var rateLimitErr *github.RateLimitError - if stderrors.As(err, &rateLimitErr) { + if rateLimitErr, ok := stderrors.AsType[*github.RateLimitError](err); ok { resetTime := rateLimitErr.Rate.Reset.Time if !resetTime.IsZero() { retryIn := time.Until(resetTime).Round(time.Second) @@ -179,8 +178,7 @@ func NewGitHubAPIErrorResponse(ctx context.Context, message string, resp *github "%s: GitHub API rate limit exceeded. Wait before retrying.", message)) } - var abuseErr *github.AbuseRateLimitError - if stderrors.As(err, &abuseErr) { + if abuseErr, ok := stderrors.AsType[*github.AbuseRateLimitError](err); ok { if abuseErr.RetryAfter != nil { retryAfter := abuseErr.RetryAfter.Round(time.Second) if retryAfter > 0 { diff --git a/pkg/github/actions.go b/pkg/github/actions.go index 3f5654db10..874677d848 100644 --- a/pkg/github/actions.go +++ b/pkg/github/actions.go @@ -1103,8 +1103,7 @@ func rerunFailedJobs(ctx context.Context, client *github.Client, owner, repo str func cancelWorkflowRun(ctx context.Context, client *github.Client, owner, repo string, runID int64) (*mcp.CallToolResult, any, error) { resp, err := client.Actions.CancelWorkflowRunByID(ctx, owner, repo, runID) if err != nil { - var acceptedErr *github.AcceptedError - if !errors.As(err, &acceptedErr) { + if _, ok := errors.AsType[*github.AcceptedError](err); !ok { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to cancel workflow run", resp, err), nil, nil } } diff --git a/pkg/github/copilot.go b/pkg/github/copilot.go index e5db071499..747355491a 100644 --- a/pkg/github/copilot.go +++ b/pkg/github/copilot.go @@ -39,7 +39,7 @@ func (d *mvpDescription) String() string { sb.WriteString("\n\n") sb.WriteString("This tool can help with the following outcomes:\n") for _, outcome := range d.outcomes { - sb.WriteString(fmt.Sprintf("- %s\n", outcome)) + fmt.Fprintf(&sb, "- %s\n", outcome) } } @@ -47,7 +47,7 @@ func (d *mvpDescription) String() string { sb.WriteString("\n\n") sb.WriteString("More information can be found at:\n") for _, link := range d.referenceLinks { - sb.WriteString(fmt.Sprintf("- %s\n", link)) + fmt.Fprintf(&sb, "- %s\n", link) } } diff --git a/pkg/github/projects.go b/pkg/github/projects.go index 28e15d3bd1..71c29d5720 100644 --- a/pkg/github/projects.go +++ b/pkg/github/projects.go @@ -625,8 +625,7 @@ Use this tool to get details about individual projects, project fields, project } resolvedIDs, resolveErr := resolveFieldNamesToIDs(ctx, gqlClient, owner, ownerType, projectNumber, fieldNames, "fields") if resolveErr != nil { - var structured *ghErrors.StructuredResolutionError - if errors.As(resolveErr, &structured) { + if structured, ok := errors.AsType[*ghErrors.StructuredResolutionError](resolveErr); ok { return ghErrors.NewStructuredResolutionErrorResponse(structured), nil, nil } return utils.NewToolResultError(resolveErr.Error()), nil, nil @@ -984,8 +983,7 @@ func ProjectsWrite(t translations.TranslationHelperFunc) inventory.ServerTool { // Resolve the item by (item_owner, item_repo, issue_number). resolvedItemID, resolveErr := resolveItemIDFromIssueArgs(ctx, gqlClient, owner, ownerType, projectNumber, args) if resolveErr != nil { - var structured *ghErrors.StructuredResolutionError - if errors.As(resolveErr, &structured) { + if structured, ok := errors.AsType[*ghErrors.StructuredResolutionError](resolveErr); ok { return ghErrors.NewStructuredResolutionErrorResponse(structured), nil, nil } return utils.NewToolResultError(resolveErr.Error()), nil, nil @@ -1252,8 +1250,7 @@ func listProjectItems(ctx context.Context, client *github.Client, gqlClient *git if len(fieldNames) > 0 { resolvedIDs, resolveErr := resolveFieldNamesToIDs(ctx, gqlClient, owner, ownerType, projectNumber, fieldNames, "fields") if resolveErr != nil { - var structured *ghErrors.StructuredResolutionError - if errors.As(resolveErr, &structured) { + if structured, ok := errors.AsType[*ghErrors.StructuredResolutionError](resolveErr); ok { return ghErrors.NewStructuredResolutionErrorResponse(structured), nil, nil } return utils.NewToolResultError(resolveErr.Error()), nil, nil @@ -1446,8 +1443,7 @@ func fetchProjectItem(ctx context.Context, client *github.Client, owner, ownerTy func updateProjectItem(ctx context.Context, client *github.Client, gqlClient *githubv4.Client, owner, ownerType string, projectNumber int, itemID int64, fieldValue map[string]any) (*mcp.CallToolResult, any, error) { updatePayload, issueField, err := buildUpdateProjectItem(ctx, gqlClient, owner, ownerType, projectNumber, fieldValue) if err != nil { - var structured *ghErrors.StructuredResolutionError - if errors.As(err, &structured) { + if structured, ok := errors.AsType[*ghErrors.StructuredResolutionError](err); ok { return ghErrors.NewStructuredResolutionErrorResponse(structured), nil, nil } return utils.NewToolResultError(err.Error()), nil, nil @@ -1469,8 +1465,7 @@ func updateProjectItem(ctx context.Context, client *github.Client, gqlClient *gi issueID, resolveErr := projectItemIssueID(projectItem) if resolveErr != nil { - var structured *ghErrors.StructuredResolutionError - if errors.As(resolveErr, &structured) { + if structured, ok := errors.AsType[*ghErrors.StructuredResolutionError](resolveErr); ok { return ghErrors.NewStructuredResolutionErrorResponse(structured), nil, nil } return utils.NewToolResultError(resolveErr.Error()), nil, nil @@ -2136,8 +2131,7 @@ func createProjectView(ctx context.Context, gqlClient *githubv4.Client, args map } configuration, err := projectViewVisibleFieldsInput(ctx, gqlClient, args, owner, ownerType, projectNumber) if err != nil { - var structured *ghErrors.StructuredResolutionError - if errors.As(err, &structured) { + if structured, ok := errors.AsType[*ghErrors.StructuredResolutionError](err); ok { return ghErrors.NewStructuredResolutionErrorResponse(structured), nil, nil } return utils.NewToolResultError(err.Error()), nil, nil @@ -2267,8 +2261,7 @@ func updateProjectView(ctx context.Context, gqlClient *githubv4.Client, args map configuration, err := projectViewVisibleFieldsInput(ctx, gqlClient, args, owner, ownerType, projectNumber) if err != nil { - var structured *ghErrors.StructuredResolutionError - if errors.As(err, &structured) { + if structured, ok := errors.AsType[*ghErrors.StructuredResolutionError](err); ok { return ghErrors.NewStructuredResolutionErrorResponse(structured), nil, nil } return utils.NewToolResultError(err.Error()), nil, nil diff --git a/pkg/github/projects_batch.go b/pkg/github/projects_batch.go index 28493a4bf2..70b590ea6f 100644 --- a/pkg/github/projects_batch.go +++ b/pkg/github/projects_batch.go @@ -182,8 +182,7 @@ func updateProjectItemsBatch(ctx context.Context, client *github.Client, gqlClie } func batchTopLevelError(err error) *mcp.CallToolResult { - var structured *ghErrors.StructuredResolutionError - if errors.As(err, &structured) { + if structured, ok := errors.AsType[*ghErrors.StructuredResolutionError](err); ok { return ghErrors.NewStructuredResolutionErrorResponse(structured) } return utils.NewToolResultError(err.Error()) @@ -796,8 +795,7 @@ func resolveIssueRefs(ctx context.Context, gqlClient *githubv4.Client, projectID } func batchErrorFromResolution(err error) *batchItemError { - var structured *ghErrors.StructuredResolutionError - if errors.As(err, &structured) { + if structured, ok := errors.AsType[*ghErrors.StructuredResolutionError](err); ok { return &batchItemError{ Code: structured.Kind, Message: fmt.Sprintf("%s: %s", structured.Kind, structured.Name), diff --git a/pkg/github/projects_batch_mutation.go b/pkg/github/projects_batch_mutation.go index 0c478aabf9..13913e53bf 100644 --- a/pkg/github/projects_batch_mutation.go +++ b/pkg/github/projects_batch_mutation.go @@ -105,7 +105,7 @@ func executeAliasedMutation(ctx context.Context, gqlClient *githubv4.Client, kin outcomes := make([]mutationAliasOutcome, len(inputs)) elem := mutationPtr.Elem() for i := range inputs { - result, ok := elem.Field(i).Interface().(projectV2ItemMutationResult) + result, ok := reflect.TypeAssert[projectV2ItemMutationResult](elem.Field(i)) if !ok || result.ProjectV2Item.ID == "" { continue } diff --git a/pkg/github/projects_batch_test.go b/pkg/github/projects_batch_test.go index 985cd5bfc7..7e17849e4e 100644 --- a/pkg/github/projects_batch_test.go +++ b/pkg/github/projects_batch_test.go @@ -396,10 +396,10 @@ func Test_ProjectsWrite_UpdateProjectItems_NumericItemIDDeduplicatesRESTLookup(t } gqlClient := newTestGQLClient(transport) - var restCalls int32 + var restCalls atomic.Int32 restClient := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetOrgsProjectsV2ItemsByProjectByItemID: func(w http.ResponseWriter, _ *http.Request) { - atomic.AddInt32(&restCalls, 1) + restCalls.Add(1) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"id":1001,"node_id":"PVTI_item1001"}`)) @@ -427,7 +427,7 @@ func Test_ProjectsWrite_UpdateProjectItems_NumericItemIDDeduplicatesRESTLookup(t require.NoError(t, json.Unmarshal([]byte(getTextResult(t, result).Text), &response)) assert.Equal(t, float64(1), response["succeeded"]) assert.Equal(t, float64(1), response["failed"]) - assert.Equal(t, int32(1), atomic.LoadInt32(&restCalls), "the same numeric item_id must only be resolved once") + assert.Equal(t, int32(1), restCalls.Load(), "the same numeric item_id must only be resolved once") results := response["results"].([]any) assert.Equal(t, "duplicate_target", results[1].(map[string]any)["error"].(map[string]any)["code"]) } @@ -581,10 +581,10 @@ func Test_ProjectsWrite_UpdateProjectItems_DuplicateTargetRejected(t *testing.T) }, } gqlClient := newTestGQLClient(transport) - var restCalls int32 + var restCalls atomic.Int32 restClient := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetOrgsProjectsV2ItemsByProjectByItemID: func(w http.ResponseWriter, _ *http.Request) { - atomic.AddInt32(&restCalls, 1) + restCalls.Add(1) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"id":1001,"node_id":"PVTI_item1"}`)) }, @@ -616,7 +616,7 @@ func Test_ProjectsWrite_UpdateProjectItems_DuplicateTargetRejected(t *testing.T) second := results[1].(map[string]any) assert.Equal(t, "failed", second["status"]) assert.Equal(t, "duplicate_target", second["error"].(map[string]any)["code"]) - assert.Equal(t, int32(1), atomic.LoadInt32(&restCalls)) + assert.Equal(t, int32(1), restCalls.Load()) assert.Len(t, transport.mutationCalls, 1) } diff --git a/pkg/github/projects_resolver.go b/pkg/github/projects_resolver.go index 33e82d1de5..b4fba6c33e 100644 --- a/pkg/github/projects_resolver.go +++ b/pkg/github/projects_resolver.go @@ -606,7 +606,7 @@ func resolveFieldNamesToIDsFromFields(all []ResolvedField, names []string, owner for i, field := range resolved { id, parseErr := parseInt64(field.ID) if parseErr != nil { - return nil, fmt.Errorf("resolved field %q has non-numeric ID %q; pass it via '%s' instead", names[i], field.ID, idParameter) + return nil, fmt.Errorf("resolved field %q has non-numeric ID %q; pass it via '%s' instead", names[i], field.ID, idParameter) //nolint:gosec // G602: resolveFieldsByName returns exactly one field per name in order, or an error. } out = append(out, id) } diff --git a/pkg/github/secret_scanning.go b/pkg/github/secret_scanning.go index 1991d5d2be..f73fede25b 100644 --- a/pkg/github/secret_scanning.go +++ b/pkg/github/secret_scanning.go @@ -85,7 +85,7 @@ func GetSecretScanningAlert(t translations.TranslationHelperFunc) inventory.Serv return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get alert", resp, body), nil, nil } - r, err := json.Marshal(alert) + r, err := json.Marshal(alert) //nolint:gosec // G117: This security_events-scoped tool intentionally returns the alert's secret; the result is labeled private-untrusted. if err != nil { return nil, nil, fmt.Errorf("failed to marshal alert: %w", err) } @@ -200,7 +200,7 @@ func ListSecretScanningAlerts(t translations.TranslationHelperFunc) inventory.Se return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list alerts", resp, body), nil, nil } - r, err := json.Marshal(alerts) + r, err := json.Marshal(alerts) //nolint:gosec // G117: This security_events-scoped tool intentionally returns alert secrets; the result is labeled private-untrusted. if err != nil { return nil, nil, fmt.Errorf("failed to marshal alerts: %w", err) } diff --git a/pkg/http/middleware/pat_scope_test.go b/pkg/http/middleware/pat_scope_test.go index 0607b8cf2b..5182a636fe 100644 --- a/pkg/http/middleware/pat_scope_test.go +++ b/pkg/http/middleware/pat_scope_test.go @@ -55,7 +55,7 @@ func TestWithPATScopes(t *testing.T) { }, { name: "fine-grained PAT skips scope fetching", - tokenInfo: &ghcontext.TokenInfo{ + tokenInfo: &ghcontext.TokenInfo{ //nolint:gosec // G101: Synthetic fine-grained PAT fixture verifies scope fetching is skipped; it cannot authenticate. Token: "github_pat_xxxxxxxxxxxxxxxxxxxxxxx", TokenType: utils.TokenTypeFineGrainedPersonalAccessToken, }, diff --git a/pkg/http/middleware/scope_challenge_test.go b/pkg/http/middleware/scope_challenge_test.go index 1c83ba600c..2beeecf49d 100644 --- a/pkg/http/middleware/scope_challenge_test.go +++ b/pkg/http/middleware/scope_challenge_test.go @@ -359,7 +359,7 @@ func scopeChallengeContext(ctx context.Context) context.Context { } func scopeChallengeContextWithScopes(ctx context.Context, activeScopes []string) context.Context { - ctx = ghcontext.WithTokenInfo(ctx, &ghcontext.TokenInfo{ + ctx = ghcontext.WithTokenInfo(ctx, &ghcontext.TokenInfo{ //nolint:gosec // G101: "oauth-token" is a synthetic context fixture, not an authentication credential. Token: "oauth-token", TokenType: utils.TokenTypeOAuthAccessToken, }) diff --git a/pkg/http/middleware/token_test.go b/pkg/http/middleware/token_test.go index fa8f0ee98e..a8373a1565 100644 --- a/pkg/http/middleware/token_test.go +++ b/pkg/http/middleware/token_test.go @@ -37,7 +37,7 @@ func TestExtractUserToken(t *testing.T) { expectWWWAuth: true, }, // Personal Access Token (classic) - ghp_ prefix - { + { //nolint:gosec // G101: Synthetic token fixture exercises classic PAT prefix detection; it cannot authenticate. name: "personal access token (classic) with Bearer prefix", authHeader: "Bearer ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", expectedStatusCode: http.StatusOK, @@ -45,7 +45,7 @@ func TestExtractUserToken(t *testing.T) { expectedToken: "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", expectTokenInfo: true, }, - { + { //nolint:gosec // G101: Synthetic token fixture exercises lowercase bearer parsing; it cannot authenticate. name: "personal access token (classic) with bearer lowercase", authHeader: "bearer ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", expectedStatusCode: http.StatusOK, @@ -62,7 +62,7 @@ func TestExtractUserToken(t *testing.T) { expectTokenInfo: true, }, // Fine-grained Personal Access Token - github_pat_ prefix - { + { //nolint:gosec // G101: Synthetic token fixture exercises fine-grained PAT prefix detection; it cannot authenticate. name: "fine-grained personal access token with Bearer prefix", authHeader: "Bearer github_pat_xxxxxxxxxxxxxxxxxxxxxxx", expectedStatusCode: http.StatusOK, @@ -70,7 +70,7 @@ func TestExtractUserToken(t *testing.T) { expectedToken: "github_pat_xxxxxxxxxxxxxxxxxxxxxxx", expectTokenInfo: true, }, - { + { //nolint:gosec // G101: Synthetic token fixture exercises fine-grained PAT parsing without a bearer prefix; it cannot authenticate. name: "fine-grained personal access token without Bearer prefix", authHeader: "github_pat_xxxxxxxxxxxxxxxxxxxxxxx", expectedStatusCode: http.StatusOK, @@ -113,7 +113,7 @@ func TestExtractUserToken(t *testing.T) { expectTokenInfo: true, }, // Server-to-Server GitHub App Token (installation token) - ghs_ prefix - { + { //nolint:gosec // G101: Synthetic token fixture exercises installation token prefix detection; it cannot authenticate. name: "server-to-server GitHub App token with Bearer prefix", authHeader: "Bearer ghs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", expectedStatusCode: http.StatusOK, diff --git a/pkg/http/transport/etag_test.go b/pkg/http/transport/etag_test.go index a3905d01ee..dcac5566eb 100644 --- a/pkg/http/transport/etag_test.go +++ b/pkg/http/transport/etag_test.go @@ -46,10 +46,10 @@ func TestETagTransport_ServesCachedBodyOn304(t *testing.T) { const etag = `"abc123"` const body = `{"number":1}` - var requests int32 + var requests atomic.Int32 var lastIfNoneMatch string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - n := atomic.AddInt32(&requests, 1) + n := requests.Add(1) lastIfNoneMatch = r.Header.Get(headers.IfNoneMatchHeader) w.Header().Set(headers.ETagHeader, etag) if n == 1 { @@ -84,7 +84,7 @@ func TestETagTransport_ServesCachedBodyOn304(t *testing.T) { assert.Equal(t, http.StatusOK, status2, "304 is translated to the cached 200") assert.Equal(t, body, body2, "cached body is served on 304") assert.Equal(t, etag, lastIfNoneMatch, "second request sends the cached ETag") - assert.Equal(t, int32(2), atomic.LoadInt32(&requests), "every request still reaches the server") + assert.Equal(t, int32(2), requests.Load(), "every request still reaches the server") } // TestETagTransport_UpdatesRateLimitHeadersFrom304 verifies that a cache-served @@ -95,9 +95,9 @@ func TestETagTransport_UpdatesRateLimitHeadersFrom304(t *testing.T) { const etag = `"v1"` - var requests int32 + var requests atomic.Int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - n := atomic.AddInt32(&requests, 1) + n := requests.Add(1) w.Header().Set(headers.ETagHeader, etag) if n == 1 { w.Header().Set("X-RateLimit-Remaining", "100") @@ -143,7 +143,7 @@ func TestETagTransport_ScopesCacheByAuthorization(t *testing.T) { return } w.WriteHeader(http.StatusOK) - _, _ = io.WriteString(w, r.Header.Get(headers.AuthorizationHeader)) + _, _ = io.WriteString(w, r.Header.Get(headers.AuthorizationHeader)) //nolint:gosec // G705: Test server echoes synthetic tokens to verify cache isolation; no browser consumes the response. })) defer server.Close() diff --git a/pkg/http/transport/user_agent_test.go b/pkg/http/transport/user_agent_test.go index 58eee9b699..9757b4937f 100644 --- a/pkg/http/transport/user_agent_test.go +++ b/pkg/http/transport/user_agent_test.go @@ -14,7 +14,7 @@ import ( func TestUserAgentTransportRequestIsolation(t *testing.T) { t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - _, _ = io.WriteString(w, req.UserAgent()) + _, _ = io.WriteString(w, req.UserAgent()) //nolint:gosec // G705: Test server echoes the test client's user agent to verify transport isolation; no browser consumes the response. })) t.Cleanup(server.Close) transport := &UserAgentTransport{ diff --git a/pkg/inventory/server_tool.go b/pkg/inventory/server_tool.go index 2bc2769593..9dd708685f 100644 --- a/pkg/inventory/server_tool.go +++ b/pkg/inventory/server_tool.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "maps" + "slices" "github.com/github/github-mcp-server/pkg/octicons" "github.com/google/jsonschema-go/jsonschema" @@ -142,8 +143,8 @@ func (st *ServerTool) Handler(deps any) mcp.ToolHandler { // Panics if the tool has no handler - all tools should have handlers. func (st *ServerTool) RegisterFunc(s *mcp.Server, deps any, middleware ...ToolHandlerMiddleware) { handler := st.Handler(deps) // This will panic if HandlerFunc is nil - for i := len(middleware) - 1; i >= 0; i-- { - handler = middleware[i](handler) + for _, m := range slices.Backward(middleware) { + handler = m(handler) } handler = st.wrapAvailabilityCheck(handler) // Make a shallow copy of the tool to avoid mutating the original diff --git a/pkg/toolvalidation/readonlyhint.go b/pkg/toolvalidation/readonlyhint.go index bcde92a5ec..b9877bf847 100644 --- a/pkg/toolvalidation/readonlyhint.go +++ b/pkg/toolvalidation/readonlyhint.go @@ -58,7 +58,7 @@ func (v ReadOnlyHintViolation) String() string { // violations, err := toolvalidation.ScanReadOnlyHint(dir) func ScanReadOnlyHint(dir string) ([]ReadOnlyHintViolation, error) { fset := token.NewFileSet() - pkgs, err := parser.ParseDir(fset, dir, func(info os.FileInfo) bool { + pkgs, err := parser.ParseDir(fset, dir, func(info os.FileInfo) bool { //nolint:staticcheck // SA1019: This source-level validator intentionally scans all files regardless of build tags, without loading packages. // Skip test files: they are allowed to construct mcp.Tool literals // for fixtures or mocks where ReadOnlyHint is not meaningful. return !strings.HasSuffix(info.Name(), "_test.go") diff --git a/script/lint b/script/lint index 5b69cbe2ff..00cfeb1aa0 100755 --- a/script/lint +++ b/script/lint @@ -6,9 +6,9 @@ gofmt -s -w . BINDIR="$(git rev-parse --show-toplevel)"/bin BINARY=$BINDIR/golangci-lint # sync with .github/workflows/lint.yml -GOLANGCI_LINT_VERSION=v2.9.0 +GOLANGCI_LINT_VERSION=v2.14.0 -if [ ! -f "$BINARY" ]; then +if [ ! -f "$BINARY" ] || ! "$BINARY" version --short | grep -qx "${GOLANGCI_LINT_VERSION#v}"; then curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b "$BINDIR" "$GOLANGCI_LINT_VERSION" fi