From 41251a0582e9db6da137cc91732694a9118dab0d Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Mon, 20 Apr 2026 15:30:18 -0500 Subject: [PATCH 1/5] exclude node_modules from golangci-lint --- .golangci.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.golangci.yaml b/.golangci.yaml index 52eb3148..98fcd09e 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -92,6 +92,8 @@ linters: exclusions: generated: lax + paths: + - node_modules/ presets: - comments - common-false-positives @@ -108,6 +110,10 @@ formatters: - gofumpt - goimports + exclusions: + paths: + - node_modules/ + settings: gci: sections: From 75b93c819065ca0b07f4536c496b098d463136e5 Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Mon, 20 Apr 2026 15:39:18 -0500 Subject: [PATCH 2/5] bump golangci-lint in CI The branch updates `.golangci.yaml` and is being linted locally with `golangci-lint` 2.11.4, but CI was still pinned to 2.9.0. That leaves local and CI checks evaluating different rule sets and can hide or invent failures depending on where the lint runs. Update the workflow pin to 2.11.4 so the GitHub Action fetches the same linter version used locally. Keeping the versions aligned makes the branch's lint results reproducible across environments. --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index de1cf5b1..640d5ee5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -90,7 +90,7 @@ jobs: name: Go lint runs-on: ubuntu-latest env: - GOLANGCI_LINT_VERSION: v2.9.0 + GOLANGCI_LINT_VERSION: v2.11.4 GOPROXY: https://proxy.golang.org,https://u:${{ secrets.RIVERPRO_GO_MOD_CREDENTIAL }}@riverqueue.com/goproxy,direct permissions: contents: read From c66245a7cac8bd75090fa5c38c17ad74e3d24998 Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Mon, 20 Apr 2026 15:39:32 -0500 Subject: [PATCH 3/5] replace httptest.NewRequest in tests Recent `golangci-lint` releases enable the `noctx` checks that now flag plain `httptest.NewRequest` calls in our tests and shared test helpers. Those requests were still valid, but they no longer match the context- aware API the linter expects. Switch the affected tests and helper code to `httptest.NewRequestWithContext`, reusing each test's existing context where possible and falling back to `context.Background()` in the small helper that has no test context available. This keeps the request setup behavior the same while satisfying the stricter lint rule. --- handler_test.go | 2 +- internal/authmiddleware/auth_middleware_test.go | 4 ++-- internal/handlertest/handlertest.go | 2 +- internal/riveruicmd/auth_middleware_test.go | 8 ++++---- internal/riveruicmd/riveruicmd_test.go | 16 ++++++++-------- riverproui/pro_handler_test.go | 2 +- spa_response_writer_test.go | 3 ++- 7 files changed, 19 insertions(+), 18 deletions(-) diff --git a/handler_test.go b/handler_test.go index 1ebdd157..3fb7ece7 100644 --- a/handler_test.go +++ b/handler_test.go @@ -128,7 +128,7 @@ func TestMountStaticFiles(t *testing.T) { var ( recorder = httptest.NewRecorder() - req = httptest.NewRequest(http.MethodGet, "/robots.txt", nil) + req = httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/robots.txt", nil) ) mux.ServeHTTP(recorder, req) diff --git a/internal/authmiddleware/auth_middleware_test.go b/internal/authmiddleware/auth_middleware_test.go index e645d1da..3e0476ae 100644 --- a/internal/authmiddleware/auth_middleware_test.go +++ b/internal/authmiddleware/auth_middleware_test.go @@ -70,7 +70,7 @@ func TestBasicAuth_Middleware(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - req := httptest.NewRequest(http.MethodGet, "/", nil) + req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil) tt.setupRequest(req) rec := httptest.NewRecorder() @@ -112,7 +112,7 @@ func Test_isReqAuthorized(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - req := httptest.NewRequest(http.MethodGet, "/", nil) + req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil) if tt.hasAuth { req.SetBasicAuth(tt.reqUser, tt.reqPass) } diff --git a/internal/handlertest/handlertest.go b/internal/handlertest/handlertest.go index c0726148..6bfd3b19 100644 --- a/internal/handlertest/handlertest.go +++ b/internal/handlertest/handlertest.go @@ -48,7 +48,7 @@ func RunIntegrationTest[TClient any](t *testing.T, createClient func(ctx context body = bytes.NewBuffer(payload) } - req := httptest.NewRequest(method, path, body) + req := httptest.NewRequestWithContext(ctx, method, path, body) recorder := httptest.NewRecorder() t.Logf("--> %s %s", method, path) diff --git a/internal/riveruicmd/auth_middleware_test.go b/internal/riveruicmd/auth_middleware_test.go index a892db44..03031170 100644 --- a/internal/riveruicmd/auth_middleware_test.go +++ b/internal/riveruicmd/auth_middleware_test.go @@ -58,7 +58,7 @@ func TestAuthMiddleware(t *testing.T) { //nolint:tparallel t.Parallel() handler := setup(t, "/") - req := httptest.NewRequest(http.MethodGet, "/api/jobs", nil) + req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/api/jobs", nil) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, req) @@ -70,7 +70,7 @@ func TestAuthMiddleware(t *testing.T) { //nolint:tparallel t.Parallel() handler := setup(t, "/") - req := httptest.NewRequest(http.MethodGet, "/api/jobs", nil) + req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/api/jobs", nil) req.SetBasicAuth(basicAuthUser, basicAuthPassword) recorder := httptest.NewRecorder() @@ -84,7 +84,7 @@ func TestAuthMiddleware(t *testing.T) { //nolint:tparallel t.Parallel() handler := setup(t, "/") - req := httptest.NewRequest(http.MethodGet, "/api/health-checks/complete", nil) + req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/api/health-checks/complete", nil) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, req) @@ -96,7 +96,7 @@ func TestAuthMiddleware(t *testing.T) { //nolint:tparallel t.Parallel() handler := setup(t, "/test-prefix") - req := httptest.NewRequest(http.MethodGet, "/test-prefix/api/health-checks/complete", nil) + req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/test-prefix/api/health-checks/complete", nil) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, req) diff --git a/internal/riveruicmd/riveruicmd_test.go b/internal/riveruicmd/riveruicmd_test.go index 40e881a7..7b2dc201 100644 --- a/internal/riveruicmd/riveruicmd_test.go +++ b/internal/riveruicmd/riveruicmd_test.go @@ -89,7 +89,7 @@ func TestInitServer(t *testing.T) { //nolint:tparallel t.Parallel() initRes, _ := setup(t) - req := httptest.NewRequest(http.MethodGet, "/api/features", nil) + req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/api/features", nil) recorder := httptest.NewRecorder() initRes.uiHandler.ServeHTTP(recorder, req) @@ -106,7 +106,7 @@ func TestInitServer(t *testing.T) { //nolint:tparallel // Cannot be parallelized because of Setenv calls. t.Setenv("RIVER_JOB_LIST_HIDE_ARGS_BY_DEFAULT", "true") initRes, _ := setup(t) - req := httptest.NewRequest(http.MethodGet, "/api/features", nil) + req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/api/features", nil) recorder := httptest.NewRecorder() initRes.uiHandler.ServeHTTP(recorder, req) @@ -123,7 +123,7 @@ func TestInitServer(t *testing.T) { //nolint:tparallel // Cannot be parallelized because of Setenv calls. t.Setenv("RIVER_JOB_LIST_HIDE_ARGS_BY_DEFAULT", "1") initRes, _ := setup(t) - req := httptest.NewRequest(http.MethodGet, "/api/features", nil) + req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/api/features", nil) recorder := httptest.NewRecorder() initRes.uiHandler.ServeHTTP(recorder, req) @@ -198,12 +198,12 @@ func TestSilentHealthchecks_SuppressesLogs(t *testing.T) { initRes := makeServer(t, "/", true) recorder := httptest.NewRecorder() - initRes.httpServer.Handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/api/health-checks/minimal", nil)) + initRes.httpServer.Handler.ServeHTTP(recorder, httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/api/health-checks/minimal", nil)) require.Equal(t, http.StatusOK, recorder.Code) require.Empty(t, memoryHandler.records) recorder = httptest.NewRecorder() - initRes.httpServer.Handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/api/features", nil)) + initRes.httpServer.Handler.ServeHTTP(recorder, httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/api/features", nil)) require.Equal(t, http.StatusOK, recorder.Code) require.NotEmpty(t, memoryHandler.records) @@ -212,7 +212,7 @@ func TestSilentHealthchecks_SuppressesLogs(t *testing.T) { initRes = makeServer(t, "/pfx", true) recorder = httptest.NewRecorder() - initRes.httpServer.Handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/pfx/api/health-checks/minimal", nil)) + initRes.httpServer.Handler.ServeHTTP(recorder, httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/pfx/api/health-checks/minimal", nil)) require.Equal(t, http.StatusOK, recorder.Code) require.Empty(t, memoryHandler.records) @@ -221,7 +221,7 @@ func TestSilentHealthchecks_SuppressesLogs(t *testing.T) { initRes = makeServer(t, "/pfx/", true) recorder = httptest.NewRecorder() - initRes.httpServer.Handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/pfx/api/health-checks/minimal", nil)) + initRes.httpServer.Handler.ServeHTTP(recorder, httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/pfx/api/health-checks/minimal", nil)) require.Equal(t, http.StatusOK, recorder.Code) require.Empty(t, memoryHandler.records) @@ -230,7 +230,7 @@ func TestSilentHealthchecks_SuppressesLogs(t *testing.T) { initRes = makeServer(t, "/", false) recorder = httptest.NewRecorder() - initRes.httpServer.Handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/api/health-checks/minimal", nil)) + initRes.httpServer.Handler.ServeHTTP(recorder, httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/api/health-checks/minimal", nil)) require.Equal(t, http.StatusOK, recorder.Code) require.NotEmpty(t, memoryHandler.records) } diff --git a/riverproui/pro_handler_test.go b/riverproui/pro_handler_test.go index 8a87f70a..02bcc978 100644 --- a/riverproui/pro_handler_test.go +++ b/riverproui/pro_handler_test.go @@ -132,7 +132,7 @@ func TestProFeaturesEndpointResponse(t *testing.T) { }() recorder := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/api/features", nil) + req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/api/features", nil) handler.ServeHTTP(recorder, req) diff --git a/spa_response_writer_test.go b/spa_response_writer_test.go index 5943233e..b0149c90 100644 --- a/spa_response_writer_test.go +++ b/spa_response_writer_test.go @@ -1,6 +1,7 @@ package riverui import ( + "context" "net/http" "net/http/httptest" "strings" @@ -137,7 +138,7 @@ func TestServeIndexHTMLTemplateCaching(t *testing.T) { } func performRequest(handler http.Handler, method string, acceptHeaders []string) *httptest.ResponseRecorder { - req := httptest.NewRequest(method, "/", nil) + req := httptest.NewRequestWithContext(context.Background(), method, "/", nil) for _, header := range acceptHeaders { req.Header.Add("Accept", header) } From 54d99c0bad566ae917d703fc4f9d97fa33c623c9 Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Mon, 20 Apr 2026 16:26:14 -0500 Subject: [PATCH 4/5] confine packager writes to output root The packager writes `.mod`, `.zip`, and `.info` artifacts under a caller-provided output directory. Those writes were assembled with `filepath.Join` and then passed straight to file creation helpers, which is exactly the pattern `gosec` now flags for path traversal. Open the version output directory as an `os.Root` and perform the file writes through that root instead. This keeps the artifact generation behavior the same, but confines every write to the intended output subtree even if a filename is malformed or hostile. --- packager/packager.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packager/packager.go b/packager/packager.go index 5bf0c64d..4e1f8303 100644 --- a/packager/packager.go +++ b/packager/packager.go @@ -65,6 +65,11 @@ func createBundle() error { if err := os.MkdirAll(vOutputDir, 0o700); err != nil { return err } + outputRoot, err := os.OpenRoot(vOutputDir) + if err != nil { + return err + } + defer outputRoot.Close() version := module.Version{ Path: mod, @@ -79,11 +84,11 @@ func createBundle() error { return err } - if err := os.WriteFile(filepath.Join(vOutputDir, modFilename), modFileContents, 0o600); err != nil { + if err := outputRoot.WriteFile(modFilename, modFileContents, 0o600); err != nil { return err } - f, err := os.OpenFile(filepath.Join(vOutputDir, zipFilename), os.O_CREATE|os.O_WRONLY, 0o600) + f, err := outputRoot.OpenFile(zipFilename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600) if err != nil { return err } @@ -98,7 +103,7 @@ func createBundle() error { Time: timestamp, } - infoFile, err := os.Create(filepath.Join(vOutputDir, version.Version+".info")) + infoFile, err := outputRoot.Create(version.Version + ".info") if err != nil { return err } From c5ab83b963cabf21980a12cd703297e493c67b41 Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Mon, 20 Apr 2026 20:11:35 -0500 Subject: [PATCH 5/5] suppress gosec on -healthcheck probe `gosec` flagged the `riverui -healthcheck=...` code path because it constructs an outbound HTTP request using deployment configuration. That probe is an operator-invoked check of the running River UI server's own health endpoint, and we don't treat steering it via env as a meaningful security boundary in this context. Keep the existing behavior and add a narrow `//nolint:gosec` suppression on the request construction and execution lines with an explanation of why the warning is not actionable here. --- internal/riveruicmd/riveruicmd.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/riveruicmd/riveruicmd.go b/internal/riveruicmd/riveruicmd.go index 1fb47220..606e439b 100644 --- a/internal/riveruicmd/riveruicmd.go +++ b/internal/riveruicmd/riveruicmd.go @@ -77,10 +77,12 @@ func checkHealth(ctx context.Context, pathPrefix string, healthCheckName string) hostname := net.JoinHostPort(host, port) url := fmt.Sprintf("http://%s%s/api/health-checks/%s", hostname, pathPrefix, healthCheckName) + //nolint:gosec // `-healthcheck` is an operator-invoked probe of the running River UI server's own HTTP health endpoint. req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return fmt.Errorf("error constructing request to health endpoint: %w", err) } + //nolint:gosec // `-healthcheck` intentionally reuses the configured River UI endpoint and isn't treated as a security boundary here. response, err := http.DefaultClient.Do(req) if err != nil { return fmt.Errorf("error requesting health endpoint: %w", err)