Run all linting in Docker via Dockerfile.lint + script/lint #134
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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, which already does exactly this — copy its shape.Dockerfile.lintat the repo root:script/lintbecomes a thin wrapper that builds 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:
docker build -f Dockerfile.lint .on an unchanged tree returns success in well under a second having run no linter. Since caching is explicitly waived here, force the lint layers to execute.golangci-lint config verifyfetches its JSON schema over an unpinned live HTTPS call. If you include that step it makes linting network-dependent and breaks hash-pinning. Decide deliberately.Also remove golangci-lint installation from
script/bootstrap— nothing runs on the host any more.Definition of done
script/lintruns the linter only in Docker; no host golangci-lint path remains.script/lintruns on an unchanged tree both demonstrably execute the linter.make checkstill green.Canonical tracking issue: sneak/prompts#40
Implementation plan
Copying the shape of
sneak/homoicon's rootDockerfile.lintandscript/lint.1.
Dockerfile.lint(new, repo root) — two stages so the forced re-run stays cheap:FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef...ad5240 AS deps—WORKDIR /src,COPY go.mod go.sum ./,RUN go mod download.FROM deps AS lint—COPY . .,RUN golangci-lint run --config .golangci.yml ./....Digest verified before use:
docker buildx imagetools inspect golangci/golangci-lint:v2.12.2reports exactly that index digest, andgolangci-lint --versioninside the pinned image reports2.12.2 ... from c0d3ddc9, matching the org's canonical pinc0d3ddc9cf3faa61a4e378e879ece580256d76e5.2.
script/lint— thin wrapper, docker only, no host code path:--no-cache-filter=lintforces the source copy and the linter run to execute on every invocation while leaving thedepsstage (base image +go mod download) cached, so the "cached build lints nothing" trap cannot fire.--progress=plainmakes the linter's real output visible. No global cache invalidation is used.3.
golangci-lint config verify— deliberately NOT included. It fetches its JSON schema over a live unpinned HTTPS call, which would make linting network-dependent and defeat hash-pinning.4.
script/bootstrap— drop thegolangci-lintgo installand its pinned ref; KEEP thegoimportsinstall, sincescript/fmtandscript/fmt-checkstill run on the host. Header comment updated, plus a non-fatal warning whendockeris absent.5. Root
Dockerfile— required consequence, not scope creep: its builder stage currently runsmake check, which after this change callsscript/lint, which shells out todocker build. There is no docker daemon inside a docker build, soscript/cibuildandscript/dockerwould break. Restructured to the policy/homoicon shape: a dedicated lint stage on the pinnedgolangci-lintimage invoking the linter directly, the builder stage depending on it viaCOPY --from=lint /src/go.sum /dev/nulland runningmake fmt-check,make test,make build. The now-unneededgolangci-lintinstall is removed from the builder stage.6. Docs — README
EntrypointsandBuildingsections updated to describe linting as a docker-only operation;TODO.mdgets a Completed Steps entry in the same commit.Verification I will show: two consecutive
script/lintruns on an unchanged tree with the linter's output present in both; a negative control that introduces a deliberate violation, fails naming that finding, then reverts clean; andmake checkgreen end to end. All viamake/script/entrypoints only.Landing as a single commit on the long-lived
nextbranch.Implemented and pushed as a single commit on
next(cc86473). PR: #136What landed
Dockerfile.lint(new, repo root) ongolangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240. Split into adepsstage (base image +go mod download) and alintstage (COPY . .+golangci-lint run --config .golangci.yml ./...).script/lintreduced to a thin wrapper. No host golangci-lint path remains anywhere in the repo.script/bootstrap: golangci-lint install and its pinned ref removed; header comment rewritten; non-fatal warning whendockeris absent.goimportsinstall kept, sincescript/fmtandscript/fmt-checkstill run it on the host.Dockerfile: its builder stage ranmake check, which after this change callsscript/lint, which shells out todocker build— and there is no docker daemon inside a docker build, soscript/cibuildandscript/dockerwould have broken. It now has its own lint stage on the same pinned image with the linter invoked directly, the builder depending on it viaCOPY --from=lint /src/go.sum /dev/nulland runningmake fmt-check,make test,make build. The now-unneeded golangci-lint install is gone from the builder.EntrypointsandBuildingupdated;TODO.mdentry in the same commit.Digest verification
The quoted digest resolves and is genuinely v2.12.2:
Commit
c0d3ddc9matches the org's canonical pinc0d3ddc9cf3faa61a4e378e879ece580256d76e5.Trap 1: cached build lints nothing
script/lintpasses--no-cache-filter=lint --target lint. The invalidation is scoped to thelintstage only — no global cache wipe,depsstays cached.--progress=plainkeeps the linter's output visible.Two consecutive
make lintruns on an unchanged tree, both executing the linter:A third run shows the scoping working —
depsfrom cache,lintre-executed:Trap 2:
config verifyDeliberately NOT included, for exactly the reason you flagged: it fetches its JSON schema over a live unpinned HTTPS call, making linting network-dependent and defeating hash-pinning. The reason is recorded in a comment at the top of
Dockerfile.lintso it does not get re-added by accident.Trap 3:
.dockerignoreRead and checked. It excludes
.git/,bin/,*.md,LICENSE,.editorconfig,.gitignore— none of which the lint build needs..golangci.ymland the Go tree are present in the context;#6 transferring context: 275.12kBand the linter reporting real findings in the negative control confirm it is not linting an empty tree.Negative control
Added an unused function containing an ineffectual assignment to
internal/config/config.go:make lintexited non-zero naming both findings at their exact lines. File reverted;make lintclean again (0 issues.).Other verification
make checkgreen end to end (test, lint, fmt-check), exit 0.script/cibuildgreen, confirming theDockerfilerestructure does not recurse: lint stage ran (#16 12.56 0 issues.), then#22 [builder 8/9] RUN make testwithPASSlines, then#23 [builder 9/9] RUN make build.make/script/entrypoints only.[manager] Landed on
nextascc86473, carried by #136. Independent review passed.The one result worth recording, because the raw evidence looks like the opposite:
script/cibuildon an unchanged tree returns exit 0 in 0.63s with all 18 layersCACHED, lint stage included — the signature of the false green tracked at #115. It is not that defect. The sourceCOPY . .sits inside the lint stage's cache key, so any source change invalidates it; a violation planted from that same warm cache failed the build in 16.6s naming both findings. The gate is live.Three comment-only defects found during review are split out to #137.