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

Closed
opened 2026-08-10 13:16:40 +02:00 by clawbot · 2 comments
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.

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

  1. A cached build lints nothing. A lint build 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. Two things to get right, both of which would otherwise ship a false green: 1. **A cached build lints nothing.** A lint build 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

Queued after #10, which changes the same files (Dockerfile, script/bootstrap, .golangci.yml). Dispatch it once that lands on next: one issue-to-pr worker, branch cut from next, PR based on next. The worker reads sneak/homoicon for the shape and decides how make check inside the main Dockerfile handles lint once script/lint itself runs docker build.

Model: opus-5-5

Queued after https://git.eeqj.de/sneak/attrsum/issues/10, which changes the same files (`Dockerfile`, `script/bootstrap`, `.golangci.yml`). Dispatch it once that lands on `next`: one `issue-to-pr` worker, branch cut from `next`, PR based on `next`. The worker reads `sneak/homoicon` for the shape and decides how `make check` inside the main `Dockerfile` handles lint once `script/lint` itself runs `docker build`. Model: opus-5-5
Author
Collaborator

Plan. Dispatched once #10 is on next. The shape is settled by REPO_POLICIES.md in sneak/prompts at commit dd4027b (the commit #13 re-vendors from), following sneak's ruling on sneak/prompts#40 (2026-08-10). It replaces the Dockerfile.lint shape in the body above: lint and test are phases of the one Dockerfile, there is no separate lint file, and there is no golangci-lint config verify step.

Implementer's brief:

  • Dockerfile: copy the canonical Go example in REPO_POLICIES.md at dd4027b. A lint stage FROM golangci/golangci-lint@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 (v2.12.2, dated comment) that runs golangci-lint run --config .golangci.yml ./...; a test stage on the pinned Debian Go image that runs the tests as an unprivileged user, as today (the permission tests need it); the build stage takes COPY --from=lint /src/go.sum /dev/null and the same from test; the runtime stage stays last. The version step stays as it is on next (it already matches that example).
  • The build stage no longer runs make check: once script/lint and script/test are docker builds they cannot run inside one.
  • script/lint and script/test each build only their phase: docker build --no-cache --target <phase> -t "$(script/projectname)-<phase>" .. script/check runs test, lint and script/fmt-check (host). script/cibuild runs script/bootstrap, script/check, then builds the image tagged and with --no-cache.
  • script/bootstrap: drop golangci-lint; nothing lints on the host.
  • Not here: .golangci.yml and the move to golangci-lint v2.14.0 change together in #13.
  • README.md and TODO.md updated in the same commit.

Definition of done: the issue's list, plus: plant a lint violation and script/cibuild fails on it, and a failing test fails script/cibuild too; a plain docker build . still builds the lint and test phases; make check and script/cibuild pass.

Model: opus-5-5

Plan. Dispatched once https://git.eeqj.de/sneak/attrsum/issues/10 is on `next`. The shape is settled by `REPO_POLICIES.md` in `sneak/prompts` at commit `dd4027b` (the commit https://git.eeqj.de/sneak/attrsum/issues/13 re-vendors from), following sneak's ruling on https://git.eeqj.de/sneak/prompts/issues/40 (2026-08-10). It replaces the `Dockerfile.lint` shape in the body above: lint and test are phases of the one `Dockerfile`, there is no separate lint file, and there is no `golangci-lint config verify` step. Implementer's brief: - `Dockerfile`: copy the canonical Go example in `REPO_POLICIES.md` at `dd4027b`. A `lint` stage `FROM golangci/golangci-lint@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240` (v2.12.2, dated comment) that runs `golangci-lint run --config .golangci.yml ./...`; a `test` stage on the pinned Debian Go image that runs the tests as an unprivileged user, as today (the permission tests need it); the build stage takes `COPY --from=lint /src/go.sum /dev/null` and the same from `test`; the runtime stage stays last. The version step stays as it is on `next` (it already matches that example). - The build stage no longer runs `make check`: once `script/lint` and `script/test` are `docker build`s they cannot run inside one. - `script/lint` and `script/test` each build only their phase: `docker build --no-cache --target <phase> -t "$(script/projectname)-<phase>" .`. `script/check` runs test, lint and `script/fmt-check` (host). `script/cibuild` runs `script/bootstrap`, `script/check`, then builds the image tagged and with `--no-cache`. - `script/bootstrap`: drop golangci-lint; nothing lints on the host. - Not here: `.golangci.yml` and the move to golangci-lint v2.14.0 change together in https://git.eeqj.de/sneak/attrsum/issues/13. - `README.md` and `TODO.md` updated in the same commit. Definition of done: the issue's list, plus: plant a lint violation and `script/cibuild` fails on it, and a failing test fails `script/cibuild` too; a plain `docker build .` still builds the lint and test phases; `make check` and `script/cibuild` pass. Model: opus-5-5
Sign in to join this conversation.