diff --git a/.dockerignore b/.dockerignore index bae18ae..d6ebb8e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,6 @@ .git/ bin/ +.lint-cache/ *.md LICENSE .editorconfig diff --git a/.gitignore b/.gitignore index 9c22cd8..19e75d9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ bin/ vendor/ data/ +.lint-cache/ .env *.exe /dnswatcher diff --git a/README.md b/README.md index 83f9226..3ac1fee 100644 --- a/README.md +++ b/README.md @@ -387,7 +387,10 @@ them. We provide: - `script/projectname` — print the project name (used for the Docker image tag) - `script/test` — run the test suite (race detector, coverage) -- `script/lint` — run golangci-lint +- `script/lint` — run golangci-lint, with its cache and its lock file + isolated to this checkout (under the git-ignored `.lint-cache/`) so + concurrent checkouts on one host cannot share cache entries or + contend on a single lock - `script/fmt` — format all code (gofmt -s, goimports) - `script/fmt-check` — check formatting (read-only) - `script/check` — run test, lint, and fmt-check diff --git a/TODO.md b/TODO.md index bc4c519..9e17d0b 100644 --- a/TODO.md +++ b/TODO.md @@ -25,6 +25,15 @@ confirm make check still passes. # Completed Steps +- 2026-08-09: `script/lint` now isolates golangci-lint's per-user global + state to the checkout (#121): `GOLANGCI_LINT_CACHE` and `TMPDIR` are + both pointed at the git-ignored, Docker-ignored `.lint-cache/`. The + cache fixes cross-contamination; `TMPDIR` is what moves the lock, + which lives at `$TMPDIR/golangci-lint.lock` and not in the cache + directory. Reproduced both failure modes on the unfixed script (10 of + 12 concurrent runs void with `parallel golangci-lint is running`; 11 + of 12 reporting another checkout's paths) and both are gone at 20-way + concurrency after the fix - 2026-08-07: golangci-lint bumped to v2.12.2 (commit-pinned installs in `Dockerfile` and `script/bootstrap`); `.golangci.yml` set to the org-standard v2-schema config used across the org's repos diff --git a/script/lint b/script/lint index 8017180..94212a9 100755 --- a/script/lint +++ b/script/lint @@ -1,11 +1,40 @@ #!/bin/sh # script/lint: run the linter. +# +# golangci-lint keeps two pieces of per-user global state, and both of +# them break when several checkouts on one host lint concurrently: +# +# 1. Its analysis cache (GOLANGCI_LINT_CACHE, default +# ~/.cache/golangci-lint). Entries are keyed by content, not by +# checkout, so a hit written by another checkout is replayed +# verbatim - including that checkout's file paths. The run then +# reports findings for files it never linted. +# +# 2. Its "one runner at a time" lock, which does NOT live in the +# cache directory: golangci-lint locks +# $(os.TempDir())/golangci-lint.lock, i.e. +# "$TMPDIR"/golangci-lint.lock (pkg/commands/run.go, +# acquireFileLock). It waits 5s, then aborts with "parallel +# golangci-lint is running" - a non-result that looks like a lint +# failure. Setting GOLANGCI_LINT_CACHE alone does not move it. +# +# So both are pinned under the checkout root. The cache is never shared, +# and TMPDIR makes the lock file per-checkout, which keeps the lock +# doing its actual job (serialising runs that share one cache) at the +# right scope. .lint-cache/ is git-ignored and Docker-ignored, and +# caching still works: it persists across runs in this checkout. set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" main() { cd "$ROOT" + + GOLANGCI_LINT_CACHE="$ROOT/.lint-cache/cache" + TMPDIR="$ROOT/.lint-cache/tmp" + export GOLANGCI_LINT_CACHE TMPDIR + mkdir -p "$GOLANGCI_LINT_CACHE" "$TMPDIR" + golangci-lint run --config .golangci.yml ./... }