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

Closed
opened 2026-08-10 13:14:33 +02:00 by clawbot · 3 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.

This supersedes the version-pinning approach scoped for script/lint here, and it also removes the escape-into-another-worktree problem observed in this repo — a host lint run reported findings whose paths pointed into a different agent's worktree entirely.

Note this repo already builds lint in a separate hash-pinned Docker stage; the change is to make script/lint the only path and drop any host invocation, not to introduce Docker where there was none.

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.

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


SPLIT 2026-09-03: #104 could not be completed inside a single agent run (three attempts died mid-run having pushed nothing). Split into:

  • issue #107 — add hash-pinned Dockerfile.lint, make script/lint Docker-only (mechanical)
  • issue #108 — prove the gate actually lints: layer-cache defeat + negative control (blocked on 107)

This issue stays open as the parent tracker and should be closed by hand once both children merge. Do not dispatch work directly against #104.

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. This supersedes the version-pinning approach scoped for `script/lint` here, and it also removes the escape-into-another-worktree problem observed in this repo — a host lint run reported findings whose paths pointed into a different agent's worktree entirely. Note this repo already builds lint in a separate hash-pinned Docker stage; the change is to make `script/lint` the only path and drop any host invocation, not to introduce Docker where there was none. 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. ## 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 --- **SPLIT 2026-09-03:** #104 could not be completed inside a single agent run (three attempts died mid-run having pushed nothing). Split into: - [issue #107](https://git.eeqj.de/sneak/pixa/issues/107) — add hash-pinned `Dockerfile.lint`, make `script/lint` Docker-only (mechanical) - [issue #108](https://git.eeqj.de/sneak/pixa/issues/108) — prove the gate actually lints: layer-cache defeat + negative control (blocked on 107) This issue stays open as the parent tracker and should be closed by hand once both children merge. Do not dispatch work directly against #104.
clawbot added this to the 1.0.0 milestone 2026-08-10 16:13:13 +02:00
Author
Collaborator

Plan. The two sub-issues were closed as out-of-fleet, so this issue is dispatched directly as one unit, with their definitions of done folded in: Dockerfile.lint hash-pinned, script/lint reduced to building it, the lint layers forced to run every time, a negative control, and the config verify question decided in the PR body.

One interaction the sub-issues did not name: once script/lint is Docker-only, the lint stage inside Dockerfile can no longer run make lint (there is no Docker inside a build). Copy sneak/homoicon's shape for that stage as well, so the same linter runs the same way in CI and locally.

Base branch next. Runs after nothing; #101 follows it because both change how the checks run in Docker.

model: claude-fable-5

Plan. The two sub-issues were closed as out-of-fleet, so this issue is dispatched directly as one unit, with their definitions of done folded in: `Dockerfile.lint` hash-pinned, `script/lint` reduced to building it, the lint layers forced to run every time, a negative control, and the `config verify` question decided in the PR body. One interaction the sub-issues did not name: once `script/lint` is Docker-only, the lint stage inside `Dockerfile` can no longer run `make lint` (there is no Docker inside a build). Copy `sneak/homoicon`'s shape for that stage as well, so the same linter runs the same way in CI and locally. Base branch `next`. Runs after nothing; https://git.eeqj.de/sneak/pixa/issues/101 follows it because both change how the checks run in Docker. model: claude-fable-5
Author
Collaborator

Plan for #104, folding in #107 and #108.

  • Add root Dockerfile.lint built from the pinned golangci/golangci-lint:v2.12.2-alpine image this repo's Dockerfile already uses, not the Debian digest the issue quotes from homoicon: pixa is CGO/libvips, so the lint image must carry vips-dev/libheif-dev/build-base/pkgconfig for the type-aware linters to compile, and reusing the one digest already pinned here avoids two v2.12.2 images drifting.
  • script/lint becomes a single docker build -f Dockerfile.lint; no host or nix-shell golangci-lint path remains.
  • Cache defeat: script/lint passes a unique CACHEBUST build-arg and Dockerfile.lint fails the build when it is unset, so the lint layer re-executes every run and cannot silently serve a cached, un-run result.
  • The Dockerfile lint stage can no longer call make lint (no Docker inside a build), so it invokes golangci-lint run directly, same image and config.
  • golangci-lint config verify is excluded: it fetches its schema over an unpinned live HTTPS call, which REPO_POLICIES forbids without exception.
  • Negative control and the two-run cache-defeat check are run and their outcome recorded on the PR; full transcripts go to the manager, per the no-passing-logs rule.

model: claude-opus-4-8

Plan for https://git.eeqj.de/sneak/pixa/issues/104, folding in https://git.eeqj.de/sneak/pixa/issues/107 and https://git.eeqj.de/sneak/pixa/issues/108. - Add root `Dockerfile.lint` built from the pinned `golangci/golangci-lint:v2.12.2-alpine` image this repo's `Dockerfile` already uses, not the Debian digest the issue quotes from `homoicon`: pixa is CGO/libvips, so the lint image must carry `vips-dev`/`libheif-dev`/`build-base`/`pkgconfig` for the type-aware linters to compile, and reusing the one digest already pinned here avoids two `v2.12.2` images drifting. - `script/lint` becomes a single `docker build -f Dockerfile.lint`; no host or nix-shell golangci-lint path remains. - Cache defeat: `script/lint` passes a unique `CACHEBUST` build-arg and `Dockerfile.lint` fails the build when it is unset, so the lint layer re-executes every run and cannot silently serve a cached, un-run result. - The `Dockerfile` lint stage can no longer call `make lint` (no Docker inside a build), so it invokes `golangci-lint run` directly, same image and config. - `golangci-lint config verify` is excluded: it fetches its schema over an unpinned live HTTPS call, which REPO_POLICIES forbids without exception. - Negative control and the two-run cache-defeat check are run and their outcome recorded on the PR; full transcripts go to the manager, per the no-passing-logs rule. model: claude-opus-4-8
Author
Collaborator

Implemented as #122 (base next).

All linting now runs only inside Docker: script/lint builds a hash-pinned root Dockerfile.lint, and the host/nix-shell golangci-lint invocation is removed. A per-run CACHEBUST build-arg forces the lint step to re-execute every run, so an unchanged tree cannot pass having linted nothing. Dockerfile's lint stage now runs golangci-lint directly, since make lint builds a container and there is no Docker inside a build. golangci-lint config verify stays out: it fetches its schema over an unpinned live HTTPS call.

Verified: two consecutive script/lint runs both re-ran the linter; the negative control failed with its specific finding and was clean after revert; the authoritative gate docker build --target builder . passed.

Model: opus-4-8

Implemented as https://git.eeqj.de/sneak/pixa/pulls/122 (base `next`). All linting now runs only inside Docker: `script/lint` builds a hash-pinned root `Dockerfile.lint`, and the host/nix-shell `golangci-lint` invocation is removed. A per-run `CACHEBUST` build-arg forces the lint step to re-execute every run, so an unchanged tree cannot pass having linted nothing. `Dockerfile`'s lint stage now runs `golangci-lint` directly, since `make lint` builds a container and there is no Docker inside a build. `golangci-lint config verify` stays out: it fetches its schema over an unpinned live HTTPS call. Verified: two consecutive `script/lint` runs both re-ran the linter; the negative control failed with its specific finding and was clean after revert; the authoritative gate `docker build --target builder .` passed. Model: opus-4-8
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/pixa#104