Run all linting in Docker via Dockerfile.lint + script/lint #55

Open
opened 2026-08-10 13:14:53 +02:00 by clawbot · 1 comment
Collaborator

Owner ruling, sneak 2026-08-09: every lint run happens inside a Docker container, invoked through the script/ entrypoint. Docker is always available. Linting runs independently and does not need a cache. He has directed a PR for every repo not already set up this way.

Reference implementation is sneak/homoicon — copy its shape: a root Dockerfile.lint built FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240, which COPYs the repo in and runs golangci-lint run --config .golangci.yml ./... as a build step, with script/lint reduced to building it. Linting as a build step means a successful build IS a clean lint, and it works even where the docker daemon is remote and bind mounts are impossible.

Note this repo's Dockerfile carries a memlock ulimit for memguard; make sure the lint image does not need it and that nothing about the split changes the main build's behaviour.

Two things to get right, both of which would otherwise ship a false green:

  1. A cached build lints nothing. docker build -f Dockerfile.lint . on an unchanged tree returns success in well under a second having run no linter. Caching is explicitly waived here, so force the lint layers to execute.
  2. golangci-lint config verify fetches its JSON schema over an unpinned live HTTPS call. Decide deliberately whether to include it.

Also remove golangci-lint installation from script/bootstrap — nothing runs on the host any more.

Definition of done

  • script/lint runs the linter only in Docker; no host golangci-lint path remains.
  • Two consecutive script/lint runs on an unchanged tree both demonstrably execute the linter.
  • Negative control: introduce a deliberate lint violation, confirm it fails with that specific finding, revert, confirm clean.
  • make check still green.

Canonical tracking issue: sneak/prompts#40

Owner ruling, sneak 2026-08-09: every lint run happens inside a Docker container, invoked through the `script/` entrypoint. Docker is always available. Linting runs independently and does not need a cache. He has directed a PR for every repo not already set up this way. Reference implementation is `sneak/homoicon` — copy its shape: a root `Dockerfile.lint` built `FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240`, which COPYs the repo in and runs `golangci-lint run --config .golangci.yml ./...` as a build step, with `script/lint` reduced to building it. Linting as a build step means a successful build IS a clean lint, and it works even where the docker daemon is remote and bind mounts are impossible. Note this repo's Dockerfile carries a memlock ulimit for memguard; make sure the lint image does not need it and that nothing about the split changes the main build's behaviour. Two things to get right, both of which would otherwise ship a false green: 1. **A cached build lints nothing.** `docker build -f Dockerfile.lint .` on an unchanged tree returns success in well under a second having run no linter. Caching is explicitly waived here, so force the lint layers to execute. 2. **`golangci-lint config verify` fetches its JSON schema over an unpinned live HTTPS call.** Decide deliberately whether to include it. Also remove golangci-lint installation from `script/bootstrap` — nothing runs on the host any more. ## Definition of done - `script/lint` runs the linter only in Docker; no host golangci-lint path remains. - Two consecutive `script/lint` runs on an unchanged tree both demonstrably execute the linter. - Negative control: introduce a deliberate lint violation, confirm it fails with that specific finding, revert, confirm clean. - `make check` still green. Canonical tracking issue: https://git.eeqj.de/sneak/prompts/issues/40
Author
Collaborator

Plan:

  1. New Dockerfile.lint, two stages off the pinned golangci/golangci-lint:v2.12.2@sha256:5ccee... digest: deps (COPY go.mod/go.sum, go mod download) and lint (COPY the tree, golangci-lint run --config .golangci.yml ./...). Splitting the stages lets the module download stay cached while the lint stage is force-invalidated.
  2. script/lint becomes docker build --progress=plain --no-cache-filter=lint -f Dockerfile.lint .. --no-cache-filter targets the named lint stage, so the linter re-executes on every run on an unchanged tree; --progress=plain keeps the linter's output visible. No host golangci-lint path remains.
  3. Main Dockerfile: its lint stage currently runs make fmt-check / make lint. make lint would now recurse into a docker build inside a build, so that stage invokes gofmt and golangci-lint directly instead (same shape as the reference repo's Dockerfile). Stage ordering, base image digests, the builder/runtime stages and the memlock ulimit in script/cibuild are untouched, so script/cibuild behaviour is unchanged and CI keeps linting.
  4. script/bootstrap: drop the golangci-lint install block; update the README Entrypoints section accordingly.
  5. golangci-lint config verify: leaning toward excluding it — it fetches its JSON schema over an unpinned live HTTPS call, which conflicts with the repo's hash-pinning policy and makes lint fail on any network outage. Final decision and rationale go in the PR body.
  6. Verification for the PR body: two consecutive script/lint runs on an unchanged tree, both showing the linter executing; a negative control (deliberate violation -> failure naming that finding -> revert -> clean); confirmation that the lint image needs no memlock ulimit; script/cibuild.
  7. TODO.md gets one additive entry in the same commit.
Plan: 1. New `Dockerfile.lint`, two stages off the pinned `golangci/golangci-lint:v2.12.2@sha256:5ccee...` digest: `deps` (COPY `go.mod`/`go.sum`, `go mod download`) and `lint` (COPY the tree, `golangci-lint run --config .golangci.yml ./...`). Splitting the stages lets the module download stay cached while the lint stage is force-invalidated. 2. `script/lint` becomes `docker build --progress=plain --no-cache-filter=lint -f Dockerfile.lint .`. `--no-cache-filter` targets the named `lint` stage, so the linter re-executes on every run on an unchanged tree; `--progress=plain` keeps the linter's output visible. No host `golangci-lint` path remains. 3. Main `Dockerfile`: its lint stage currently runs `make fmt-check` / `make lint`. `make lint` would now recurse into a `docker build` inside a build, so that stage invokes `gofmt` and `golangci-lint` directly instead (same shape as the reference repo's `Dockerfile`). Stage ordering, base image digests, the builder/runtime stages and the memlock ulimit in `script/cibuild` are untouched, so `script/cibuild` behaviour is unchanged and CI keeps linting. 4. `script/bootstrap`: drop the `golangci-lint` install block; update the README Entrypoints section accordingly. 5. `golangci-lint config verify`: leaning toward excluding it — it fetches its JSON schema over an unpinned live HTTPS call, which conflicts with the repo's hash-pinning policy and makes lint fail on any network outage. Final decision and rationale go in the PR body. 6. Verification for the PR body: two consecutive `script/lint` runs on an unchanged tree, both showing the linter executing; a negative control (deliberate violation -> failure naming that finding -> revert -> clean); confirmation that the lint image needs no memlock ulimit; `script/cibuild`. 7. `TODO.md` gets one additive entry in the same commit.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/secret#55