From 7a1ee6e5a8d1ed203adbebc7ac3c1251e22f0faa Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Mon, 21 Sep 2026 19:30:06 +0200 Subject: [PATCH] lint: adopt org-standard .golangci.yml and golangci-lint v2.12.2 (closes #14) The old backend/.golangci.yml declared version "2" but used v1 schema keys, so under v2 it never validated and its thresholds were inert: the linter ran at defaults. Replace it verbatim with the org-standard file, repin the Dockerfile.backend lint stage to golangci-lint v2.12.2, and assert the config's sha256 as the first step of the backend lint target so it cannot silently drift again -- a local hash check, no network. The standard config surfaces findings only in the tests: the repeated IP literals in middleware_test.go become named constants (goconst) and its request switches to NewRequestWithContext (noctx). reportbuf.go's gosec suppression gains a plain justification comment. The rest of the backend, including the fx-based server lifecycle, is already clean. TODO.md updated. Model: opus-4-8 --- Dockerfile.backend | 4 +-- TODO.md | 29 ++++++++++++---- backend/.golangci.yml | 26 ++++++++------- backend/Makefile | 19 +++++++++++ .../internal/middleware/middleware_test.go | 33 ++++++++++++------- backend/internal/reportbuf/reportbuf.go | 4 ++- 6 files changed, 81 insertions(+), 34 deletions(-) diff --git a/Dockerfile.backend b/Dockerfile.backend index e06a617..191a65e 100644 --- a/Dockerfile.backend +++ b/Dockerfile.backend @@ -1,8 +1,8 @@ # Lint stage — fast feedback on formatting and lint issues. The # golangci/golangci-lint image ships Go, gofmt, make and the linter, so # nothing is installed here. -# golangci/golangci-lint:v2.7.2 (2026-08-09) -FROM golangci/golangci-lint@sha256:5d6d5c70a61f1356adfd9dd6316ce286799fefc9d743421356ff1b00842368ba AS lint +# golangci/golangci-lint:v2.12.2 (2026-08-10) +FROM golangci/golangci-lint@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 AS lint WORKDIR /src COPY backend/go.mod backend/go.sum ./ diff --git a/TODO.md b/TODO.md index 49f0492..d4ff3d6 100644 --- a/TODO.md +++ b/TODO.md @@ -10,15 +10,16 @@ # Status -pre-1.0. No git tags. Backend work in flight on feat/reportbuf-storage (dirty: -src/main.js). Frontend is functional; backend is new and unmerged. +pre-1.0. No git tags. `feat/reportbuf-storage` is merged; the backend, the CI +workflow, and the backend repo standard files are all on `main`. Frontend and +backend are both functional. Working toward the 1.0.0 milestone by closing the +remaining repo-compliance issues on the tracker. # Next Step -Land feat/reportbuf-storage: finish the in-progress src/main.js change, get make -check green, and merge the branch to main. The branch adds the backend (buffered -zstd-compressed report storage), the CI workflow, and backend repo standard -files, so merging it also closes most compliance gaps. +Confirm the `.gitea/workflows/check.yml` run is green (main always green +policy). The workflow file is already on `main`; what is unverified is that its +latest run passes. # Completed Steps @@ -36,6 +37,14 @@ files, so merging it also closes most compliance gaps. frame/sniff/referrer/permissions headers) registered before CORS, and trusted-proxy client IP resolution honouring `X-Forwarded-For` / `X-Real-IP` only from a `TRUSTED_PROXIES` allowlist (loopback plus RFC1918 by default) +- 2026-08-10: adopted the org-standard `backend/.golangci.yml` verbatim and + moved the pinned golangci-lint from v2.7.2 to v2.12.2 (the `lint` stage of + `Dockerfile.backend` now pins the `golangci/golangci-lint:v2.12.2` image by + digest); the previous config declared `version: "2"` but used v1 schema keys, + so every threshold in it was inert and its green result was meaningless. + `backend/Makefile`'s `lint` target now asserts the config's sha256 against the + canonical file first, so drift from the org standard fails the build instead + of silently degrading to defaults - 2026-08-10: every interactive control now meets the 44x44 CSS px minimum tap target (`.pin-btn`, `#interval-select`, the debug-log label and, on narrow viewports, `#pause-btn`). The pin button's hit area grows via matching @@ -65,7 +74,7 @@ files, so merging it also closes most compliance gaps. shims, README Entrypoints section - 2026-02-27: backend with buffered zstd-compressed report storage; CI workflow and backend repo standard files; backend Dockerfile fixed (Go 1.25, - golangci-lint) and moved to repo root (feat/reportbuf-storage, unmerged) + golangci-lint) and moved to repo root (feat/reportbuf-storage) - 2026-02-26: host row layout redesigned with CSS grid; overflow and spacing fixes; nginx config extracted; port hardcoded to 8080 - 2026-02-26: debug log panel, median stats, recovery probe, Docker build fix, @@ -87,3 +96,9 @@ files, so merging it also closes most compliance gaps. (main always green policy) - Decide what to do with untracked resume.sh: commit it, gitignore it, or delete it +- Upstream fix needed in `sneak/prompts`: the org-standard `.golangci.yml` + enables `gomodguard`, which golangci-lint v2.12.2 reports as deprecated since + v2.12.0 and replaced by `gomodguard_v2`, so every backend lint run prints a + deprecation warning. The file is standardized and must never be edited in this + repo, so nothing can be done here beyond tracking it — tracked at + diff --git a/backend/.golangci.yml b/backend/.golangci.yml index 34a8e31..26b1610 100644 --- a/backend/.golangci.yml +++ b/backend/.golangci.yml @@ -1,5 +1,9 @@ version: "2" +# Config schema uses the golangci-lint v2 layout (settings live under +# linters.settings, not top-level linters-settings) so that the +# thresholds below are actually applied by golangci-lint >= v2. + run: timeout: 5m modules-download-mode: readonly @@ -14,19 +18,17 @@ linters: - wsl # Deprecated, replaced by wsl_v5 - wrapcheck # Too verbose for internal packages - varnamelen # Short names like db, id are idiomatic Go - -linters-settings: - lll: - line-length: 88 - funlen: - lines: 80 - statements: 50 - cyclop: - max-complexity: 15 - dupl: - threshold: 100 + settings: + lll: + line-length: 88 + funlen: + lines: 80 + statements: 50 + cyclop: + max-complexity: 15 + dupl: + threshold: 100 issues: - exclude-use-default: false max-issues-per-linter: 0 max-same-issues: 0 diff --git a/backend/Makefile b/backend/Makefile index 4660353..96edfb9 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -10,6 +10,17 @@ GOLDFLAGS += -s -w GOLDFLAGS += -X main.Version=$(VERSION) GOLDFLAGS += -X main.Buildarch=$(BUILDARCH) +# macOS ships shasum rather than sha256sum. +SHA256SUM := $(shell command -v sha256sum >/dev/null 2>&1 && echo sha256sum || echo shasum -a 256) + +# .golangci.yml is standardized org-wide and must never be edited here +# (REPO_POLICIES.md). Its last silent drift replaced the v2 schema with +# v1 keys, which left every threshold in the file inert while the build +# stayed green. The lint target therefore asserts the file still matches +# the canonical copy byte for byte. The check is a local hash comparison: +# no network, no remote schema, nothing unpinned in the build path. +GOLANGCI_CONFIG_SHA256 := 021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb + .PHONY: all build test lint fmt fmt-check check docker hooks run clean all: build @@ -22,6 +33,14 @@ test: timeout 30 go test ./... lint: + @actual=$$($(SHA256SUM) .golangci.yml | cut -d' ' -f1); \ + if [ "$$actual" != "$(GOLANGCI_CONFIG_SHA256)" ]; then \ + echo ".golangci.yml has drifted from the org standard."; \ + echo " expected $(GOLANGCI_CONFIG_SHA256)"; \ + echo " actual $$actual"; \ + echo "Restore it verbatim from sneak/prompts; do not edit it."; \ + exit 1; \ + fi golangci-lint run ./... fmt: diff --git a/backend/internal/middleware/middleware_test.go b/backend/internal/middleware/middleware_test.go index 2965593..a664ee6 100644 --- a/backend/internal/middleware/middleware_test.go +++ b/backend/internal/middleware/middleware_test.go @@ -9,6 +9,15 @@ import ( "sneak.berlin/go/netwatch/internal/middleware" ) +const ( + // loopbackPeer is a remote address inside the trusted-proxy allowlist. + loopbackPeer = "127.0.0.1:5000" + // forwardedIP is the client address presented via X-Forwarded-For. + forwardedIP = "203.0.113.7" + // realIP is the client address presented via X-Real-IP. + realIP = "203.0.113.9" +) + func mustPrefixes(t *testing.T, cidrs ...string) []netip.Prefix { t.Helper() @@ -41,32 +50,32 @@ func clientIPCases() []clientIPCase { return []clientIPCase{ { name: "trusted proxy uses forwarded-for", - remoteAddr: "127.0.0.1:5000", - xff: "203.0.113.7", - want: "203.0.113.7", + remoteAddr: loopbackPeer, + xff: forwardedIP, + want: forwardedIP, }, { name: "trusted proxy uses left-most of chain", remoteAddr: "10.1.2.3:5000", - xff: "203.0.113.7, 10.1.2.3", - want: "203.0.113.7", + xff: forwardedIP + ", 10.1.2.3", + want: forwardedIP, }, { name: "trusted proxy falls back to x-real-ip", - remoteAddr: "127.0.0.1:5000", - xRealIP: "203.0.113.9", - want: "203.0.113.9", + remoteAddr: loopbackPeer, + xRealIP: realIP, + want: realIP, }, { name: "untrusted peer ignores forwarded-for", remoteAddr: "198.51.100.4:5000", - xff: "203.0.113.7", + xff: forwardedIP, want: "198.51.100.4", }, { name: "untrusted peer ignores x-real-ip", remoteAddr: "198.51.100.4:5000", - xRealIP: "203.0.113.9", + xRealIP: realIP, want: "198.51.100.4", }, { @@ -76,7 +85,7 @@ func clientIPCases() []clientIPCase { }, { name: "trusted proxy with garbage header uses peer", - remoteAddr: "127.0.0.1:5000", + remoteAddr: loopbackPeer, xff: "not-an-ip", want: "127.0.0.1", }, @@ -119,7 +128,7 @@ func TestSecurityHeaders(t *testing.T) { ) rec := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/", http.NoBody) + req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", http.NoBody) handler.ServeHTTP(rec, req) want := map[string]string{ diff --git a/backend/internal/reportbuf/reportbuf.go b/backend/internal/reportbuf/reportbuf.go index 1ef3b19..7b89266 100644 --- a/backend/internal/reportbuf/reportbuf.go +++ b/backend/internal/reportbuf/reportbuf.go @@ -169,7 +169,9 @@ func (b *Buffer) writeFile(data []byte) { name := fmt.Sprintf("reports-%s.jsonl.zst", ts) path := filepath.Join(b.dataDir, name) - f, err := os.OpenFile( //nolint:gosec // path built from controlled dataDir + timestamp + // path is built from the operator-supplied dataDir plus a + // generated timestamp, so it carries no external input. + f, err := os.OpenFile( //nolint:gosec // see comment above path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, filePerms,