From 9d4259afa3ffb430d09f1c03bd68705b45e55614 Mon Sep 17 00:00:00 2001 From: sneak Date: Sun, 4 Oct 2026 02:22:22 +0000 Subject: [PATCH] Run golangci-lint only in docker, on every run (closes #55) script/lint builds the new Dockerfile.lint, where golangci-lint runs as a build step. The lint stage is rebuilt on every run, so an unchanged tree is linted too; the module download stays cached. script/bootstrap no longer installs golangci-lint. The Dockerfile lint stage calls golangci-lint directly, since make lint now starts a docker build. golangci-lint config verify is not run: it fetches its schema live over unpinned HTTPS. Model: opus-5-5 --- Dockerfile | 3 ++- Dockerfile.lint | 19 +++++++++++++++++++ README.md | 9 ++++++--- TODO.md | 7 +++++++ script/bootstrap | 7 +------ script/lint | 18 ++++++++++++++---- 6 files changed, 49 insertions(+), 14 deletions(-) create mode 100644 Dockerfile.lint diff --git a/Dockerfile b/Dockerfile index 4ecea59..df5ea97 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,8 @@ RUN go mod download COPY . . RUN make fmt-check -RUN make lint +# Not make lint: script/lint is a docker build, which cannot run in here. +RUN golangci-lint run --config .golangci.yml ./... # Build stage — tests and compilation # golang 1.24.13-alpine (2026-03-10) diff --git a/Dockerfile.lint b/Dockerfile.lint new file mode 100644 index 0000000..da5b64b --- /dev/null +++ b/Dockerfile.lint @@ -0,0 +1,19 @@ +# Lint image, built by script/lint: golangci-lint runs as a build step, so a +# successful build is a clean lint. Works where the docker daemon is remote +# and bind mounts are impossible. + +# golangci/golangci-lint:v2.12.2 (Debian-based), 2026-08-07 +FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 AS deps + +WORKDIR /src + +COPY go.mod go.sum ./ +RUN go mod download + +# script/lint rebuilds this stage on every run, by this name; the module +# download above stays cached. +FROM deps AS lint + +COPY . . + +RUN golangci-lint run --config .golangci.yml ./... diff --git a/README.md b/README.md index 9bcd9fb..6bac13a 100644 --- a/README.md +++ b/README.md @@ -496,15 +496,18 @@ 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 dependencies (Go, golangci-lint, Go - module download), idempotently +- `script/bootstrap` — install all dependencies (Go, Go module + download), idempotently; golangci-lint is not installed, it runs in + docker - `script/setup` — make a fresh clone ready for development: runs `script/bootstrap`, then `script/install-precommit` - `script/projectname` — output the project name (`secret`); used by other scripts such as `script/docker` - `script/test` — run `go vet` and the test suite (verbose rerun on failure) -- `script/lint` — run `golangci-lint` +- `script/lint` — run `golangci-lint` in docker only: builds + `Dockerfile.lint`, where the linter is a build step that runs on every + call, also on an unchanged tree - `script/fmt` — format all Go code (writes) - `script/fmt-check` — check formatting without writing - `script/check` — run `script/test`, `script/lint`, and diff --git a/TODO.md b/TODO.md index 94d3603..d84dd33 100644 --- a/TODO.md +++ b/TODO.md @@ -25,6 +25,13 @@ Bring the repo into policy compliance in one commit: # Completed Steps +- 2026-10-04: Lint runs only in docker: `script/lint` builds + `Dockerfile.lint`, where golangci-lint is a build step rebuilt on + every run (`--no-cache-filter`), so an unchanged tree is linted too; + the module download stays cached. `script/bootstrap` no longer + installs golangci-lint, and the `Dockerfile` lint stage calls it + directly instead of `make lint`. `golangci-lint config verify` is not + run: it fetches its schema live over unpinned HTTPS. - 2026-10-04: A PGP unlocker whose metadata has no usable GPG key ID no longer panics: `GetID()` warns with the unlocker's directory and returns `pgp-unknown`. `ListUnlockers` skips, with a warning, an diff --git a/script/bootstrap b/script/bootstrap index c40c5e5..da4230e 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -6,6 +6,7 @@ # make, node, yarn, go, or python). Node is used directly if installed; # otherwise a pinned version is installed via nvm (installing nvm # itself first, from a hash-verified release archive, never curl | sh). +# golangci-lint is never installed: script/lint runs it in docker. # # Uncomment the language sections in main() that apply to this repo. set -eu @@ -136,12 +137,6 @@ main() { # ---- Go repos ---- if missing go; then pkg_install go golang go go; fi - # golangci-lint: packaged in nix, brew, and apk. On apt there is no - # package: download a specific release archive from GitHub and - # verify its hash (verify_sha256), never curl | sh. - if missing golangci-lint; then - pkg_install golangci-lint golangci-lint golangci-lint golangci-lint - fi go mod download # ---- Python repos ---- diff --git a/script/lint b/script/lint index 36162c8..75e2e15 100755 --- a/script/lint +++ b/script/lint @@ -1,14 +1,24 @@ #!/bin/sh -# script/lint: run the linter. +# script/lint: run the linter, in docker only. Builds Dockerfile.lint, +# where golangci-lint runs as a build step. +# +# A cached build lints nothing, so --no-cache-filter rebuilds the lint +# stage on every run, an unchanged tree included. It ignores a stage name +# that does not exist, so --target names the same stage: a rename then +# fails the build instead of serving the lint from cache. cacheonly keeps +# no image; only the build's success matters. set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" main() { cd "$ROOT" - # CGO is required (Makefile exports this too) - export CGO_ENABLED=1 - golangci-lint run --timeout 5m + docker build \ + --progress=plain \ + --target lint \ + --no-cache-filter=lint \ + --output=type=cacheonly \ + -f Dockerfile.lint . } main "$@" -- 2.54.0