Run the linter at the pinned version locally too (closes #78)
All checks were successful
check / check (pull_request) Successful in 2m18s

`script/lint` ran whatever `golangci-lint` was on `PATH` while CI ran
the digest-pinned image from the `Dockerfile` lint stage. The two
versions disagree about real findings, so `make check` could be green
on a tree CI fails - and, on this host's 2.10.1, red on a tree CI
passes. A gate that can differ from CI is not a gate.

`script/lint` now runs the pinned image itself. The single source of
truth for the linter version is the `Dockerfile` lint stage `FROM`
line: `script/lint` parses the image reference (tag AND digest) out of
it with awk and runs exactly that image, so bumping the linter is a
one-line edit there and nowhere else. The duplicate pin in the
`Makefile` `deps` target (`go install ...@v2.12.2`) and the unpinned
`golangci-lint` install in `script/bootstrap` are removed rather than
kept in sync: with linting containerized, a second copy on `PATH` is
only a way to drift.

A `golangci-lint` on `PATH` is used only when its version is exactly
equal to the pin - the same binary by definition, and the case that
matters is the lint stage itself, which runs `make lint` inside the
pinned container where no Docker daemon exists. Every other version
goes through Docker, and a missing or unreachable daemon is a hard
error naming the required image, never a silent fallback.

The container run mounts persistent `GOCACHE`, `GOMODCACHE` and
`GOLANGCI_LINT_CACHE` directories under `${XDG_CACHE_HOME:-~/.cache}`
and runs as the invoking uid/gid, so repeat runs stay fast (2.7s warm
vs 1.8s for the ambient binary) and nothing lands root-owned.
`script/lint-fix` delegates to `script/lint --fix` so autofixes come
from the same pinned linter.

README documents that `make check` is authoritative because of this,
and points at `script/cibuild` as the full CI-equivalent gate.
This commit is contained in:
2026-08-09 02:37:18 +00:00
parent e496aa334b
commit 1808773195
7 changed files with 167 additions and 17 deletions

View File

@@ -564,23 +564,34 @@ standard: normalized scripts in `script/` are the entrypoints for the
development workflow, and the Makefile targets are thin shims that call
them. We provide:
* `script/bootstrap` — install all development dependencies (go,
golangci-lint, Go module download)
* `script/bootstrap` — install all development dependencies (go, sqlite3,
Go module download). It deliberately does not install `golangci-lint`;
see `script/lint` below.
* `script/setup` — make a fresh clone ready for development: runs
`script/bootstrap`, then `script/install-precommit`
* `script/projectname` — print the project name (used for the Docker
image tag)
* `script/test` — run the test suite (verbose rerun on failure)
* `script/lint` — run `golangci-lint run ./...`
* `script/lint-fix` — apply the linter's autofixes (rewrites files)
* `script/lint` — run `golangci-lint run ./...` at the exact version CI
uses, by running the digest-pinned `golangci-lint` image declared by
the `Dockerfile` lint stage (requires Docker; it fails loudly rather
than falling back to a differently versioned `golangci-lint` on
`PATH`). That `FROM` line is the single source of truth for the linter
version — bump it there and nowhere else.
* `script/lint-fix` — apply the linter's autofixes (rewrites files),
using the same pinned linter
* `script/fmt` — format all code (writes)
* `script/fmt-check` — check formatting (read-only)
* `script/check` — run `script/test`, `script/lint`, and
`script/fmt-check`
`script/fmt-check`. This is authoritative *because* `script/lint` uses
the pinned linter: a local `make check` and CI cannot disagree about
lint findings.
* `script/docker` — build the Docker image tagged via
`script/projectname`
* `script/cibuild` — CI entrypoint: `docker build .` (the Dockerfile
runs the checks)
runs the checks). This is the full CI-equivalent gate — it runs the
checks in the same containers CI does, from a clean copy of the tree,
so it also catches anything that depends on host state.
* `script/precommit` — pre-commit gate: `go mod tidy` + `go fmt` (must
not change files), then `script/check`
* `script/install-precommit` — install the git pre-commit hook that