From 0c4e0c7ef718693181313469d91d2c1a23d84d64 Mon Sep 17 00:00:00 2001 From: Timur Olzhabayev Date: Thu, 9 Jul 2026 17:59:36 +0200 Subject: [PATCH 1/4] fix(reactcompat): make reported paths relative when temp dir contains symlinks react-detect reports file paths with symlinks resolved, so on macOS (where /var/folders is a symlink to /private/var/folders) the archive dir prefix never matched and absolute temp paths leaked into results, failing the plugincheck2 integration test locally. Try the symlink-resolved archive dir as well. --- .../passes/reactcompat/reactcompat.go | 18 +++++++++++++---- .../passes/reactcompat/reactcompat_test.go | 20 +++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/pkg/analysis/passes/reactcompat/reactcompat.go b/pkg/analysis/passes/reactcompat/reactcompat.go index af11b044..88ff3dae 100644 --- a/pkg/analysis/passes/reactcompat/reactcompat.go +++ b/pkg/analysis/passes/reactcompat/reactcompat.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "os/exec" + "path/filepath" "slices" "strings" "time" @@ -231,14 +232,23 @@ func reportIssues(pass *analysis.Pass, output *reactDetectOutput, archiveDir str // relativeToArchive strips the archive directory prefix from a file path // emitted by react-detect, so reported paths are reproducible across machines. -// Falls back to the original path if it doesn't share the archive prefix. +// react-detect may report paths with symlinks resolved (on macOS the temp dir +// /var/folders/... resolves to /private/var/folders/...), so the resolved +// archiveDir is tried as well. Falls back to the original path if it doesn't +// share the archive prefix. func relativeToArchive(file, archiveDir string) string { if archiveDir == "" { return file } - prefix := strings.TrimRight(archiveDir, "/") + "/" - if strings.HasPrefix(file, prefix) { - return strings.TrimPrefix(file, prefix) + dirs := []string{archiveDir} + if resolved, err := filepath.EvalSymlinks(archiveDir); err == nil && resolved != archiveDir { + dirs = append(dirs, resolved) + } + for _, dir := range dirs { + prefix := strings.TrimRight(dir, "/") + "/" + if rel, ok := strings.CutPrefix(file, prefix); ok { + return rel + } } return file } diff --git a/pkg/analysis/passes/reactcompat/reactcompat_test.go b/pkg/analysis/passes/reactcompat/reactcompat_test.go index a19845c1..14f6881c 100644 --- a/pkg/analysis/passes/reactcompat/reactcompat_test.go +++ b/pkg/analysis/passes/reactcompat/reactcompat_test.go @@ -1,6 +1,7 @@ package reactcompat import ( + "os" "path/filepath" "testing" @@ -248,3 +249,22 @@ func TestNoArchiveDir(t *testing.T) { require.Nil(t, result) require.Len(t, interceptor.Diagnostics, 0) } + +// TestRelativeToArchiveSymlinkedDir verifies that paths reported with the +// archive dir's symlinks resolved (e.g. macOS /var/folders -> /private/var) +// are still made relative. +func TestRelativeToArchiveSymlinkedDir(t *testing.T) { + base := t.TempDir() + target := filepath.Join(base, "target") + require.NoError(t, os.Mkdir(target, 0o755)) + link := filepath.Join(base, "link") + require.NoError(t, os.Symlink(target, link)) + + resolved, err := filepath.EvalSymlinks(link) + require.NoError(t, err) + + require.Equal(t, "src/module.ts", relativeToArchive(resolved+"/src/module.ts", link)) + require.Equal(t, "src/module.ts", relativeToArchive(link+"/src/module.ts", link)) + require.Equal(t, "/elsewhere/src/module.ts", relativeToArchive("/elsewhere/src/module.ts", link)) + require.Equal(t, "/abs/path.ts", relativeToArchive("/abs/path.ts", "")) +} From bcf70dc478455d73265b472462b5fe2acec249d3 Mon Sep 17 00:00:00 2001 From: Timur Olzhabayev Date: Thu, 9 Jul 2026 18:04:08 +0200 Subject: [PATCH 2/4] ci: run the full test suite on pull requests The PR workflow only ran the MCP server tests and a docker build; the rest of the suite (60+ test files covering every analysis pass, the runner, and the plugincheck2 integration tests) stopped running in CI when the old build workflow was retired. Restore it: build plugincheck2 for the integration tests, install semgrep and node so the coderules and reactcompat paths are exercised, and run go test ./pkg/.... This is the gate we need before auto-merging dependency updates. --- .github/workflows/test.yml | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b70ea58a..c12fa88a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ on: - renovate/* jobs: - test-mcp-server: + test: runs-on: ubuntu-x64 permissions: contents: read @@ -20,8 +20,27 @@ jobs: with: go-version-file: go.mod - - name: Run MCP server tests - run: go test -v ./pkg/cmd/mcpserver/... + # node is needed for the passes that shell out to npx + # (reactcompat, plugindocs) and for the MCP server tests. + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 22 + + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: "3.13" + + # semgrep backs the coderules pass; its tests skip when the binary + # is missing, so install it to keep them in the run. + - name: Install semgrep + run: python3 -m pip install semgrep + + # The plugincheck2 integration tests run the built binary from bin/. + - name: Build plugincheck2 + run: go build -o bin/linux_amd64/plugincheck2 ./pkg/cmd/plugincheck2 + + - name: Run tests + run: go test ./pkg/... test-docker-build: runs-on: ubuntu-x64 From 3e09c50836074b60a0ffb208d845a80fc44ee917 Mon Sep 17 00:00:00 2001 From: Timur Olzhabayev Date: Thu, 9 Jul 2026 18:17:21 +0200 Subject: [PATCH 3/4] ci: warm the react-detect npx cache before running tests The parallel integration tests each invoke npx react-detect; on a cold runner the concurrent extractions corrupt npm's _npx cache and react-detect exits 127. --- .github/workflows/test.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c12fa88a..2bdd04e7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,6 +35,17 @@ jobs: - name: Install semgrep run: python3 -m pip install semgrep + # The plugincheck2 integration tests run in parallel and each one + # shells out to `npx @grafana/react-detect`. Concurrent cold-cache + # npx runs corrupt npm's _npx cache (TAR_ENTRY_ERROR, exit 127), so + # fetch the pinned version once up front. The version is read from + # the Go constant so it can't drift. + - name: Warm the react-detect npx cache + run: | + version="$(grep -o 'reactDetectVersion = "[^"]*"' pkg/analysis/passes/reactcompat/reactcompat.go | cut -d'"' -f2)" + cd "$(mktemp -d)" + npx -y "@grafana/react-detect@${version}" --help >/dev/null 2>&1 || true + # The plugincheck2 integration tests run the built binary from bin/. - name: Build plugincheck2 run: go build -o bin/linux_amd64/plugincheck2 ./pkg/cmd/plugincheck2 From a53050826eaccbcec6975d57ad81c559a40d4664 Mon Sep 17 00:00:00 2001 From: Timur Olzhabayev Date: Thu, 9 Jul 2026 18:29:26 +0200 Subject: [PATCH 4/4] test(archivetool): serve the test archive from httptest instead of GitHub The test downloaded a real release zip from GitHub on every run and failed whenever that returned a 5xx (a 504 broke CI today). Build a minimal zip in memory and serve it from a local httptest server; the HTTP fetch path in ReadArchive stays covered. --- pkg/archivetool/archivetool_test.go | 33 +++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/pkg/archivetool/archivetool_test.go b/pkg/archivetool/archivetool_test.go index 7ff3a0e0..72683d94 100644 --- a/pkg/archivetool/archivetool_test.go +++ b/pkg/archivetool/archivetool_test.go @@ -1,15 +1,45 @@ package archivetool import ( + "archive/zip" "bytes" + "net/http" + "net/http/httptest" "testing" . "github.com/smartystreets/goconvey/convey" ) +// testPluginZip builds a minimal plugin archive in memory so the tests +// don't depend on downloading a real release over the network. +func testPluginZip(t *testing.T) []byte { + t.Helper() + var buf bytes.Buffer + w := zip.NewWriter(&buf) + f, err := w.Create("myorg-plugin/plugin.json") + if err != nil { + t.Fatal(err) + } + if _, err := f.Write([]byte(`{"id": "myorg-plugin"}`)); err != nil { + t.Fatal(err) + } + if err := w.Close(); err != nil { + t.Fatal(err) + } + return buf.Bytes() +} + func TestArchive(t *testing.T) { + zipContent := testPluginZip(t) + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write(zipContent) + }), + ) + defer server.Close() + pluginToCheck := server.URL + "/myorg-plugin-1.0.0.zip" + Convey("Test Archive", t, func() { - pluginToCheck := "https://github.com/marcusolsson/grafana-jsonapi-datasource/releases/download/v0.6.0/marcusolsson-json-datasource-0.6.0.zip" Convey("Test readArchive", func() { content, err := ReadArchive(pluginToCheck) So(err, ShouldBeNil) @@ -19,7 +49,6 @@ func TestArchive(t *testing.T) { content, err := ReadArchive(pluginToCheck) So(err, ShouldBeNil) So(len(content), ShouldBeGreaterThan, 0) - bytes.NewReader(content) archiveDir, cleanup, err := ExtractPlugin(bytes.NewReader(content)) So(err, ShouldBeNil) So(archiveDir, ShouldStartWith, "/")