From ed46d740a63b6951a59b2e2aafa37d2e45f89017 Mon Sep 17 00:00:00 2001 From: Copilot App <223556219+Copilot@users.noreply.github.com> Date: Thu, 3 Sep 2026 05:11:58 -0300 Subject: [PATCH 1/4] perf(http): avoid full argument map allocation in MCP parse middleware WithMCPParse unmarshaled the entire tools/call arguments payload into a map[string]any on every request just to extract owner and repo. Decode into a small typed struct with only those two fields instead, and drop the now-unused Arguments field from MCPMethodInfo (grep confirmed the only consumer was the middleware's own test). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- pkg/context/mcp_info.go | 2 -- pkg/http/middleware/mcp_parse.go | 22 ++++++++++------------ pkg/http/middleware/mcp_parse_test.go | 16 +++++++++++----- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/pkg/context/mcp_info.go b/pkg/context/mcp_info.go index ce55056821..b7172bd0aa 100644 --- a/pkg/context/mcp_info.go +++ b/pkg/context/mcp_info.go @@ -21,8 +21,6 @@ type MCPMethodInfo struct { Owner string // Repo is the repository name from tool call arguments, if present Repo string - // Arguments contains the raw tool arguments for tools/call requests - Arguments map[string]any } // WithMCPMethodInfo stores the MCPMethodInfo in the context. diff --git a/pkg/http/middleware/mcp_parse.go b/pkg/http/middleware/mcp_parse.go index 21848fdeb4..05290c3a30 100644 --- a/pkg/http/middleware/mcp_parse.go +++ b/pkg/http/middleware/mcp_parse.go @@ -97,19 +97,17 @@ func WithMCPParse() func(http.Handler) http.Handler { switch mcpReq.Method { case "tools/call": methodInfo.ItemName = mcpReq.Params.Name - // Parse arguments if present + // Extract owner and repo if present. Only these fields are decoded + // into a typed struct, avoiding a full map allocation over the + // (potentially large) arguments payload. if len(mcpReq.Params.Arguments) > 0 { - var args map[string]any - err := json.Unmarshal(mcpReq.Params.Arguments, &args) - if err == nil { - methodInfo.Arguments = args - // Extract owner and repo if present - if owner, ok := args["owner"].(string); ok { - methodInfo.Owner = owner - } - if repo, ok := args["repo"].(string); ok { - methodInfo.Repo = repo - } + var args struct { + Owner string `json:"owner"` + Repo string `json:"repo"` + } + if err := json.Unmarshal(mcpReq.Params.Arguments, &args); err == nil { + methodInfo.Owner = args.Owner + methodInfo.Repo = args.Repo } } case "prompts/get": diff --git a/pkg/http/middleware/mcp_parse_test.go b/pkg/http/middleware/mcp_parse_test.go index 0965c6db61..9f3b44e986 100644 --- a/pkg/http/middleware/mcp_parse_test.go +++ b/pkg/http/middleware/mcp_parse_test.go @@ -23,7 +23,6 @@ func TestWithMCPParse(t *testing.T) { expectedItem string expectedOwner string expectedRepo string - expectedArgs map[string]any }{ { name: "health check path is skipped", @@ -94,7 +93,17 @@ func TestWithMCPParse(t *testing.T) { expectedItem: "get_file_contents", expectedOwner: "github", expectedRepo: "github-mcp-server", - expectedArgs: map[string]any{"owner": "github", "repo": "github-mcp-server", "path": "README.md"}, + }, + { + name: "tools/call extracts owner and repo with large extra arguments fields", + method: http.MethodPost, + path: "/mcp", + body: `{"jsonrpc":"2.0","method":"tools/call","params":{"name":"get_file_contents","arguments":{"owner":"github","repo":"github-mcp-server","path":"README.md","extra":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}}}`, + expectInfo: true, + expectedMethod: "tools/call", + expectedItem: "get_file_contents", + expectedOwner: "github", + expectedRepo: "github-mcp-server", }, { name: "tools/call with invalid arguments JSON continues without args", @@ -158,9 +167,6 @@ func TestWithMCPParse(t *testing.T) { assert.Equal(t, tt.expectedItem, capturedInfo.ItemName) assert.Equal(t, tt.expectedOwner, capturedInfo.Owner) assert.Equal(t, tt.expectedRepo, capturedInfo.Repo) - if tt.expectedArgs != nil { - assert.Equal(t, tt.expectedArgs, capturedInfo.Arguments) - } } else { assert.False(t, infoCaptured, "MCPMethodInfo should not be present in context") } From fd652103c56a866d86966454f372f8e00180ff5c Mon Sep 17 00:00:00 2001 From: Copilot App <223556219+Copilot@users.noreply.github.com> Date: Thu, 3 Sep 2026 05:27:43 -0300 Subject: [PATCH 2/4] perf(http): decode owner/repo tolerantly and keep deprecated Arguments field Address review feedback: decode owner/repo as json.RawMessage and unmarshal each individually so a wrongly typed field no longer blocks extraction of the other. Restore the MCPMethodInfo.Arguments field as deprecated (no longer populated) to avoid breaking external consumers of pkg/context. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- pkg/context/mcp_info.go | 2 ++ pkg/http/middleware/mcp_parse.go | 20 ++++++++++++++++---- pkg/http/middleware/mcp_parse_test.go | 11 +++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/pkg/context/mcp_info.go b/pkg/context/mcp_info.go index b7172bd0aa..840af6f71f 100644 --- a/pkg/context/mcp_info.go +++ b/pkg/context/mcp_info.go @@ -21,6 +21,8 @@ type MCPMethodInfo struct { Owner string // Repo is the repository name from tool call arguments, if present Repo string + // Deprecated: Arguments is no longer populated by the parse middleware; retained for API compatibility. + Arguments map[string]any } // WithMCPMethodInfo stores the MCPMethodInfo in the context. diff --git a/pkg/http/middleware/mcp_parse.go b/pkg/http/middleware/mcp_parse.go index 05290c3a30..901c8400e7 100644 --- a/pkg/http/middleware/mcp_parse.go +++ b/pkg/http/middleware/mcp_parse.go @@ -101,13 +101,25 @@ func WithMCPParse() func(http.Handler) http.Handler { // into a typed struct, avoiding a full map allocation over the // (potentially large) arguments payload. if len(mcpReq.Params.Arguments) > 0 { + // Decode owner/repo as RawMessage so a wrongly typed field + // doesn't prevent the other from being extracted. var args struct { - Owner string `json:"owner"` - Repo string `json:"repo"` + Owner json.RawMessage `json:"owner"` + Repo json.RawMessage `json:"repo"` } if err := json.Unmarshal(mcpReq.Params.Arguments, &args); err == nil { - methodInfo.Owner = args.Owner - methodInfo.Repo = args.Repo + if len(args.Owner) > 0 { + var owner string + if err := json.Unmarshal(args.Owner, &owner); err == nil { + methodInfo.Owner = owner + } + } + if len(args.Repo) > 0 { + var repo string + if err := json.Unmarshal(args.Repo, &repo); err == nil { + methodInfo.Repo = repo + } + } } } case "prompts/get": diff --git a/pkg/http/middleware/mcp_parse_test.go b/pkg/http/middleware/mcp_parse_test.go index 9f3b44e986..eec25a8797 100644 --- a/pkg/http/middleware/mcp_parse_test.go +++ b/pkg/http/middleware/mcp_parse_test.go @@ -114,6 +114,17 @@ func TestWithMCPParse(t *testing.T) { expectedMethod: "tools/call", expectedItem: "get_file_contents", }, + { + name: "tools/call with wrongly typed owner still extracts repo", + method: http.MethodPost, + path: "/mcp", + body: `{"jsonrpc":"2.0","method":"tools/call","params":{"name":"get_file_contents","arguments":{"owner":123,"repo":"github-mcp-server"}}}`, + expectInfo: true, + expectedMethod: "tools/call", + expectedItem: "get_file_contents", + expectedOwner: "", + expectedRepo: "github-mcp-server", + }, { name: "prompts/get parses name", method: http.MethodPost, From 089f7e287632f17531035614d4367fe6ca491741 Mon Sep 17 00:00:00 2001 From: Copilot App <223556219+Copilot@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:31:49 -0300 Subject: [PATCH 3/4] fix: resolve Windows icon snapshots and cover MCP argument edge cases Strip CRLF terminators from embedded icon data URIs so Windows checkouts match the existing tool snapshots. Cover both line endings and exercise tolerant argument extraction, large nested payloads, and body restoration. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- pkg/http/middleware/mcp_parse_test.go | 59 +++++++++++++++++++++++++++ pkg/octicons/octicons.go | 7 ++-- pkg/octicons/octicons_test.go | 25 ++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) diff --git a/pkg/http/middleware/mcp_parse_test.go b/pkg/http/middleware/mcp_parse_test.go index eec25a8797..7926473c61 100644 --- a/pkg/http/middleware/mcp_parse_test.go +++ b/pkg/http/middleware/mcp_parse_test.go @@ -185,6 +185,65 @@ func TestWithMCPParse(t *testing.T) { } } +func TestWithMCPParse_ArgumentTypes(t *testing.T) { + tests := []struct { + name string + arguments string + owner string + repo string + }{ + {name: "empty object", arguments: `{}`}, + {name: "null arguments", arguments: `null`}, + {name: "array arguments", arguments: `[]`}, + {name: "numeric arguments", arguments: `123`}, + {name: "owner only", arguments: `{"owner":"github"}`, owner: "github"}, + {name: "repo only", arguments: `{"repo":"server"}`, repo: "server"}, + {name: "object owner", arguments: `{"owner":{},"repo":"server"}`, repo: "server"}, + {name: "array owner", arguments: `{"owner":[],"repo":"server"}`, repo: "server"}, + {name: "boolean owner", arguments: `{"owner":true,"repo":"server"}`, repo: "server"}, + {name: "null owner", arguments: `{"owner":null,"repo":"server"}`, repo: "server"}, + {name: "numeric repo", arguments: `{"owner":"github","repo":123}`, owner: "github"}, + {name: "object repo", arguments: `{"owner":"github","repo":{}}`, owner: "github"}, + {name: "array repo", arguments: `{"owner":"github","repo":[]}`, owner: "github"}, + {name: "boolean repo", arguments: `{"owner":"github","repo":false}`, owner: "github"}, + {name: "null repo", arguments: `{"owner":"github","repo":null}`, owner: "github"}, + {name: "both invalid", arguments: `{"owner":[],"repo":{}}`}, + {name: "last duplicate wins", arguments: `{"owner":123,"owner":"github","repo":"server","repo":null}`, owner: "github"}, + { + name: "large nested extra arguments", + arguments: `{"extra":[` + + strings.Repeat(`{"owner":"ignored","repo":"ignored","content":"`+strings.Repeat("x", 1024)+`"},`, 127) + + `{}],"owner":"github","repo":"server"}`, + owner: "github", + repo: "server", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + body := `{"jsonrpc":"2.0","method":"tools/call","params":{"name":"test_tool","arguments":` + tt.arguments + `}}` + var nextCalled bool + next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + nextCalled = true + info, ok := ghcontext.MCPMethod(r.Context()) + require.True(t, ok) + require.NotNil(t, info) + assert.Equal(t, "tools/call", info.Method) + assert.Equal(t, "test_tool", info.ItemName) + assert.Equal(t, tt.owner, info.Owner) + assert.Equal(t, tt.repo, info.Repo) + + restoredBody, err := io.ReadAll(r.Body) + require.NoError(t, err) + assert.Equal(t, body, string(restoredBody)) + }) + req := httptest.NewRequest(http.MethodPost, "/mcp", strings.NewReader(body)) + WithMCPParse()(next).ServeHTTP(httptest.NewRecorder(), req) + assert.True(t, nextCalled) + }) + } +} + func TestWithMCPParse_BodyRestoration(t *testing.T) { originalBody := `{"jsonrpc":"2.0","method":"tools/call","params":{"name":"test_tool"}}` diff --git a/pkg/octicons/octicons.go b/pkg/octicons/octicons.go index 09e8f1275b..d0a06ed226 100644 --- a/pkg/octicons/octicons.go +++ b/pkg/octicons/octicons.go @@ -22,11 +22,12 @@ type dataURIKey struct { theme Theme } -var dataURIs = loadDataURIs() +var dataURIs = loadDataURIs(embeddedDataURIs) -func loadDataURIs() map[dataURIKey]string { +func loadDataURIs(contents string) map[dataURIKey]string { dataURIs := make(map[dataURIKey]string) - for line := range strings.SplitSeq(strings.TrimSpace(embeddedDataURIs), "\n") { + for line := range strings.SplitSeq(strings.TrimSpace(contents), "\n") { + line = strings.TrimSuffix(line, "\r") filename, dataURI, ok := strings.Cut(line, "\t") separator := strings.LastIndexByte(filename, '-') if !ok || separator <= 0 || !strings.HasPrefix(dataURI, "data:image/png;base64,") { diff --git a/pkg/octicons/octicons_test.go b/pkg/octicons/octicons_test.go index 7d9c2e99d0..243ee573a9 100644 --- a/pkg/octicons/octicons_test.go +++ b/pkg/octicons/octicons_test.go @@ -15,6 +15,31 @@ import ( //go:embed icons/*.png var iconPNGs embed.FS +func TestLoadDataURIsLineEndings(t *testing.T) { + const lightURI = "data:image/png;base64,bGlnaHQ=" + const darkURI = "data:image/png;base64,ZGFyaw==" + for _, tc := range []struct { + name string + newline string + }{ + {name: "LF", newline: "\n"}, + {name: "CRLF", newline: "\r\n"}, + } { + t.Run(tc.name, func(t *testing.T) { + contents := strings.Join([]string{ + "repo-light\t" + lightURI, + "repo-dark\t" + darkURI, + "", + }, tc.newline) + + assert.Equal(t, map[dataURIKey]string{ + {name: "repo", theme: ThemeLight}: lightURI, + {name: "repo", theme: ThemeDark}: darkURI, + }, loadDataURIs(contents)) + }) + } +} + func TestDataURI(t *testing.T) { tests := []struct { name string From 3736c93cb2d18aa945c326c5270c50aa82f4531e Mon Sep 17 00:00:00 2001 From: Copilot App <223556219+Copilot@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:51:01 -0300 Subject: [PATCH 4/4] fix: enforce pinned lint tooling and strengthen icon regressions Validate cached golangci-lint versions, align CI's exact pin, correct Go/build and workflow guidance, and add independent canonical icon URI and malformed-entry coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 10 +++++----- .github/workflows/lint.yml | 2 +- docs/installation-guides/README.md | 7 +++---- pkg/octicons/octicons_test.go | 28 ++++++++++++++++++++++++++-- script/lint | 9 +++++++-- 5 files changed, 42 insertions(+), 14 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 975df2a633..b889a51c3a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -5,7 +5,7 @@ This is the **GitHub MCP Server**, a Model Context Protocol (MCP) server that connects AI tools to GitHub's platform. It enables AI agents to manage repositories, issues, pull requests, workflows, and more through natural language. **Key Details:** -- **Language:** Go 1.24+ (~38k lines of code) +- **Language:** Go (minimum version specified in `go.mod`) - **Type:** MCP server application with CLI interface - **Primary Package:** github-mcp-server (stdio MCP server - **this is the main focus**) - **Secondary Package:** mcpcurl (testing utility - don't break it, but not the priority) @@ -92,7 +92,7 @@ go test ./pkg/github -run TestGetMe ### Key Configuration Files -- **go.mod / go.sum:** Go module dependencies (Go 1.24.0+) +- **go.mod / go.sum:** Go module dependencies and minimum supported Go version - **.golangci.yml:** Linter configuration (v2 format, ~15 linters enabled) - **Dockerfile:** Multi-stage build (golang:1.25.8-alpine → distroless) - **server.json:** MCP server metadata for registry @@ -112,10 +112,10 @@ go test ./pkg/github -run TestGetMe ## GitHub Workflows (CI/CD) -All workflows run on push/PR unless noted. Located in `.github/workflows/`: +Workflow triggers vary; consult the files in `.github/workflows/`: 1. **go.yml** - Build and test on ubuntu/windows/macos. Runs `script/test` and builds binary -2. **lint.yml** - Runs golangci-lint-action v2.5 (GitHub Action) with actions/setup-go stable +2. **lint.yml** - Runs golangci-lint-action v9 with Go 1.25 and golangci-lint v2.9.0, matching the version enforced by `script/lint` 3. **docs-check.yml** - Verifies README.md is up-to-date by running generate-docs and checking git diff 4. **code-scanning.yml** - CodeQL security analysis for Go and GitHub Actions 5. **license-check.yml** - Runs `script/licenses-check` to validate compliance @@ -123,7 +123,7 @@ All workflows run on push/PR unless noted. Located in `.github/workflows/`: 7. **goreleaser.yml** - Creates releases (main branch only) 8. **registry-releaser.yml** - Updates MCP registry -**All of these must pass for PR merge.** If docs-check fails, run `script/generate-docs` and commit changes. +**Applicable PR checks must pass for merge.** Publishing workflows run on their configured release events, not on every PR. CodeQL may be intentionally skipped on forks. If docs-check fails, run `script/generate-docs` and commit changes. ## Testing Guidelines diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6119ee9f0f..ab08247676 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.9.0 diff --git a/docs/installation-guides/README.md b/docs/installation-guides/README.md index 46581aa77e..ef5a043bb4 100644 --- a/docs/installation-guides/README.md +++ b/docs/installation-guides/README.md @@ -65,8 +65,8 @@ The GitHub MCP Server can be installed using several methods. **Docker is the mo ### 🔨 Build from Source (Advanced Users) - **Pros**: Latest features, full customization, no external dependencies - **Cons**: Requires Go development environment, more complex setup -- **Prerequisites**: [Go 1.24+](https://go.dev/doc/install) -- **Build command**: `go build -o github-mcp-server cmd/github-mcp-server/main.go` +- **Prerequisites**: [Go](https://go.dev/doc/install), at least the version specified in [`go.mod`](../../go.mod) +- **Build command**: `go build -o github-mcp-server ./cmd/github-mcp-server` - **Best for**: Developers who want the latest features or need custom modifications ### Important Notes on the GitHub MCP Server @@ -82,7 +82,7 @@ All installations with Personal Access Tokens (PAT) require: Optional (depending on installation method): - **Docker** (for Docker-based installations): [Download Docker](https://www.docker.com/) -- **Go 1.24+** (for building from source): [Install Go](https://go.dev/doc/install) +- **Go** (for building from source, at least the version in [`go.mod`](../../go.mod)): [Install Go](https://go.dev/doc/install) ## Security Best Practices @@ -109,4 +109,3 @@ After installation, you may want to explore: - **Toolsets**: Enable/disable specific GitHub API capabilities - **Read-Only Mode**: Restrict to read-only operations - **Lockdown Mode**: Hide public issue details created by users without push access - diff --git a/pkg/octicons/octicons_test.go b/pkg/octicons/octicons_test.go index 243ee573a9..32d95ce06c 100644 --- a/pkg/octicons/octicons_test.go +++ b/pkg/octicons/octicons_test.go @@ -19,11 +19,14 @@ func TestLoadDataURIsLineEndings(t *testing.T) { const lightURI = "data:image/png;base64,bGlnaHQ=" const darkURI = "data:image/png;base64,ZGFyaw==" for _, tc := range []struct { - name string - newline string + name string + newline string + omitFinalNewline bool }{ {name: "LF", newline: "\n"}, {name: "CRLF", newline: "\r\n"}, + {name: "LF without final newline", newline: "\n", omitFinalNewline: true}, + {name: "CRLF without final newline", newline: "\r\n", omitFinalNewline: true}, } { t.Run(tc.name, func(t *testing.T) { contents := strings.Join([]string{ @@ -31,6 +34,9 @@ func TestLoadDataURIsLineEndings(t *testing.T) { "repo-dark\t" + darkURI, "", }, tc.newline) + if tc.omitFinalNewline { + contents = strings.TrimSuffix(contents, tc.newline) + } assert.Equal(t, map[dataURIKey]string{ {name: "repo", theme: ThemeLight}: lightURI, @@ -40,6 +46,23 @@ func TestLoadDataURIsLineEndings(t *testing.T) { } } +func TestLoadDataURIsInvalidEntries(t *testing.T) { + for _, tc := range []struct { + name string + contents string + }{ + {name: "empty manifest", contents: ""}, + {name: "missing tab", contents: "repo-light data:image/png;base64,bGlnaHQ="}, + {name: "missing name", contents: "-light\tdata:image/png;base64,bGlnaHQ="}, + {name: "wrong media type", contents: "repo-light\tdata:image/svg+xml;base64,bGlnaHQ="}, + {name: "unknown theme", contents: "repo-other\tdata:image/png;base64,bGlnaHQ="}, + } { + t.Run(tc.name, func(t *testing.T) { + assert.Panics(t, func() { loadDataURIs(tc.contents) }) + }) + } +} + func TestDataURI(t *testing.T) { tests := []struct { name string @@ -111,6 +134,7 @@ func TestDataURIForEveryEmbeddedIcon(t *testing.T) { require.NoError(t, err) dataURI := DataURI(name, theme) + assert.Equal(t, "data:image/png;base64,"+base64.StdEncoding.EncodeToString(png), dataURI) require.True(t, strings.HasPrefix(dataURI, "data:image/png;base64,")) encodedPNG := strings.TrimPrefix(dataURI, "data:image/png;base64,") decodedPNG, err := base64.StdEncoding.DecodeString(encodedPNG) diff --git a/script/lint b/script/lint index 5b69cbe2ff..6d8e97a8a1 100755 --- a/script/lint +++ b/script/lint @@ -8,8 +8,13 @@ BINARY=$BINDIR/golangci-lint # sync with .github/workflows/lint.yml GOLANGCI_LINT_VERSION=v2.9.0 -if [ ! -f "$BINARY" ]; then +if [ ! -f "$BINARY" ] || [ "$("$BINARY" version --short)" != "${GOLANGCI_LINT_VERSION#v}" ]; then curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b "$BINDIR" "$GOLANGCI_LINT_VERSION" fi -$BINARY run \ No newline at end of file +if [ "$("$BINARY" version --short)" != "${GOLANGCI_LINT_VERSION#v}" ]; then + echo "Expected golangci-lint $GOLANGCI_LINT_VERSION at $BINARY" >&2 + exit 1 +fi + +"$BINARY" run \ No newline at end of file