Run all linting in Docker via Dockerfile.lint + script/lint #41
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— copy its shape: a rootDockerfile.lintbuiltFROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240, which COPYs the repo in and runsgolangci-lint run --config .golangci.yml ./...as a build step, withscript/lintreduced to building it. Linting as a build step means a successful build IS a clean lint.This also answers the open pinning question in this repo. The reason there was no org-blessed place to pin the linter is that it was being installed on the host at all; pinning the lint image by digest in
Dockerfile.lintis the answer, and it removes the host install entirely.It also resolves the false green observed here, where an implementer reported
0 issueson a branch that was genuinely red with agoconstfinding — the shared host cache served another tree's clean result. A container per run has its own cache and lock.Two things to get right, both of which would otherwise ship a false green:
golangci-lint config verifyfetches its JSON schema over an unpinned live HTTPS call. Decide deliberately whether to include it.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 requirements (manager), settling the three repo-specific points before dispatch.
1. This overrides the scaffold exemption, narrowly.
TODO.mdFuture Steps note 3 records sneak's 2026-07-07 ruling that this repo takes no Dockerfile, CI config, orREPO_POLICIES.md, and that exemption is what I cited in #4 when I ruled against a lint container. sneak's ruling here is later and explicit ("he has directed a PR for every repo not already set up this way"), so it wins forDockerfile.lintandscript/lint— and only for those. No CI config, noREPO_POLICIES.md, and no otherscript/entrypoints in this change. Amend note 3 in the same commit to say exactly what is now permitted, so the next reader does not re-derive the old answer.2. Force the lint layers to execute — two stages, not one. A single-stage image with
--no-cachewould re-rungo mod downloadover the network on every lint. Split it: a cacheddepsstage (COPY go.mod go.sum+go mod download), thenFROM deps AS lintcarryingCOPY . .and the lint run.script/lintthen builds with--no-cache-filter=lint, which busts only the stage that lints. Do not use a baredocker buildas the reference repo does — caching is explicitly waived here, and an unchanged tree must still lint.3. Leave
golangci-lint config verifyout, and say so in a comment in the Dockerfile. It resolves its JSON schema over a live unpinned HTTPS call, which is a network dependency and an unpinned input inside a step whose whole purpose is a pinned, reproducible gate; it would also turn a schema-host outage into a red build.golangci-lint runalready fails on a malformed config, so the coverage lost is small and the config here is the shared canonical one, verified where it is maintained. This is a deliberate divergence fromsneak/homoicon, which includes the step.Do not touch
.golangci.yml— thegomodguarddeprecation is #29 and is sneak's to decide.Evidence required in the PR body, since a green docker build is the classic false green: paste the tail of two consecutive
script/lintruns on an unchanged tree showing the lint step actually ran both times (notCACHED), plus the negative control — a deliberate violation failing with that specific finding, then clean after revert.Implementation plan, per the requirements comment above. One commit on a new
nextbranch (it does not exist yet on the remote; there is no opennext->mainPR).1.
Dockerfile.lint(new, repo root). Two stages, so that busting the lint layer does not re-download modules:FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 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 ./....No
golangci-lint config verifystep, with a comment in the file recording why: it resolves its JSON schema over a live unpinned HTTPS call, which is an unpinned network input inside a step whose purpose is a pinned reproducible gate, and a schema-host outage would turn into a red build. This is the deliberate divergence fromsneak/homoicon.2.
script/lint(new). POSIXsh,set -eu, mode 100755, resolves its own repo root the same way homoicon's does. Builds with--no-cache-filter=lintso the lint stage always re-executes on an unchanged tree whiledepsstays cached. Caching of the lint result is explicitly waived here.3.
Makefile.lint:becomes a thin shim calling./script/lint; no hostgolangci-lintinvocation remains anywhere in the repo. The Makefile's header comment currently asserts "no Dockerfile" as part of the exemption, so it gets corrected to match the narrowed exemption.4.
.dockerignore. Excluding.gitonly, after confirming nothing the lint reads lives there —golangci-lint runneeds the Go sources,go.mod/go.sumand.golangci.yml, none of which come from.git.5.
README.md. The one sentence describingmake lintas "(golangci-lint)" is corrected to say it runs in Docker; that description is made wrong by this change.6.
TODO.md. Future Steps note 2 (the 2026-07-07 scaffold exemption) is amended to state the new narrower exemption —Dockerfile.lintandscript/lintare permitted, CI config,REPO_POLICIES.mdand otherscript/entrypoints still are not — plus a Completed Steps entry in the existing style. Same commit as the work..golangci.ymlis not touched.Verification I will run and paste into the PR body: two consecutive
script/lintruns on an unchanged tree with thegolangci-lint runlayer shown executing (notCACHED) both times, with wall-clock for each; the negative control (deliberate violation, failure naming that specific finding, revert, clean again); andmake checkin full.Built and pushed as
599286aonnext, in #43 (the single release-cycle PR —nextdid not exist and there was no open PR, so this opens the cycle).What landed.
Dockerfile.lintat the root, pinned by digest togolangci/golangci-lint:v2.12.2, two stages: adepsstage (WORKDIR /src,COPY go.mod go.sum ./,RUN go mod download) andFROM deps AS lintcarryingCOPY . .andRUN golangci-lint run --config .golangci.yml ./....script/lint(POSIXsh,set -eu, mode100755, resolves its own repo root) builds it with--no-cache-filter=lint, so the lint stage re-executes every run while the module download stays cached.make lintis now a shim overscript/lint; no hostgolangci-lintinvocation remains anywhere in the repo..dockerignoreexcludes.gitonly, after confirming the lint reads only the Go sources,go.mod/go.sumand.golangci.yml.golangci-lint config verifyis deliberately absent, with the reason recorded in a comment inDockerfile.lint: it resolves its JSON schema over a live unpinned HTTPS call, which is an unpinned network input inside a step whose purpose is a pinned reproducible gate, and a schema-host outage would read as a red build..golangci.ymluntouched — thegomodguarddeprecation warning is #29. No CI config, noREPO_POLICIES.md, no otherscript/entrypoint.TODO.md's scaffold-exemption note now states the narrowed exemption, plus a Completed Steps entry, in the same commit;make fmtrun and committed.How I verified it, against the definition of done:
real 0m18.802s, run Breal 0m12.044s, nothing modified between them. In both,#2 WORKDIR,#7 COPY go.mod go.sumand#8 RUN go mod downloadreportCACHEDwhile#10 [lint 2/2] RUN golangci-lint run ...reportsDONE 9.8sandDONE 7.9srespectively, each printing0 issues.after ~6.7s of real work. NeverCACHED.indent-error-flowviolation added togame/dice.gofailed the build atDockerfile.lint:32with exit 1, naming exactly that finding (game/dice.go:11:9: indent-error-flow: if block ends with a return statement ... (revive)) plusfunc negativeControlForLint is unused (unused),2 issues: * revive: 1 * unused: 1. Aftergit checkout -- game/dice.go, exit 0 again.make checkgreen.fmt-check, then the container lint (DONE 6.7s,0 issues., notCACHED), thenok cmd/rogue 1.017s/ok game 2.379sunder-race -cover— real durations, not(cached).EXIT=0.Full captured output is in the PR body.
One thing I did not decide unilaterally, raised for review rather than changed: each run exports an untagged image, so repeated linting leaves dangling images behind. The reference shape does the same and it is small (only the per-run
COPY/RUNlayers differ), but it accumulates on a busy host. Tagging the build or--output=type=cacheonlywould fix it and save ~3.5s per run; both are further divergences fromsneak/homoicon, so they were left out.