Run all linting in Docker via Dockerfile.lint + script/lint #109
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.This is directly relevant here: several PRs in this repo rested on host lint results, and the host linter in this repo has differed from the pinned one.
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
Plan:
Dockerfile.lintat repo root, two stages off the pinned digestgolangci/golangci-lint:v2.12.2@sha256:5cceeef0...: adepsstage (go.mod/go.sum+go mod download, cacheable) and alintstage (COPY . ., thengolangci-lint run --config .golangci.yml ./...).script/lintbecomes a POSIX-sh wrapper:docker build -f Dockerfile.lint --no-cache-filter=lint --progress=plain ..--no-cache-filter=lintis what defeats trap 1 — the lint stage's layers are rebuilt every invocation while the module download stays cached, so the linter genuinely executes each run.--progress=plainkeeps the linter's own output visible on success, which is the evidence that it ran. Cache invalidation is scoped to this one stage; no prune, ever.golangci-lint config verifywill be deliberately omitted, unlike thesneak/homoiconreference. It fetches its JSON schema over an unpinned live HTTPS call, which violates the hash-pinning rule and makes linting fail without network.golangci-lint runalready rejects a malformed or unknown-key config; I will demonstrate that empirically rather than assert it. The reason goes in a comment inDockerfile.lintso nobody adds the step back.Dockerfilelint stage currently doesRUN make lint, which would recurse into docker-in-docker oncescript/lintis a wrapper. It is already inside the pinned linter image, so it invokesgolangci-lint rundirectly instead.script/cibuildand thecheckworkflow are otherwise untouched.script/bootstrap(pinned version constants, release-archive download, sha256 constants,ensure_golangci_lint). README prerequisites and command docs updated to match.Acceptance evidence for the PR body: negative control (inject a violation, confirm RED naming that specific finding, revert, confirm clean); two consecutive runs on an unchanged tree both showing real linter output;
make checkgreen with nogolangci-lintonPATH.Implemented in #165 (branch
issue-109-docker-lint, commit46248a2). Full evidence is in the PR body; the DoD items and one reversal:script/lintruns the linter only in Docker; golangci-lint installation removed fromscript/bootstrap, no host path remains.0 issues.printed both times), because the wrapper passes--no-cache-filter=lint. Without that flag the same build exits 0 in 0.27s with the lint layerCACHED— that contrast is in the PR body.internal/globals/globals.go:17:5: exported: ... (revive),1 issues:, exit 1; reverted,0 issues., exit 0.make checkexits 0 with nogolangci-lintanywhere onPATH(the host has three copies; the run used a strippedPATHexcluding all of them). Full image build with--no-cache-filter=lint --no-cache-filter=builderalso exits 0 in 3m10s with zero(cached)test lines.Reversal on point 2 of the issue: I intended to drop
golangci-lint config verifyand testing changed the decision.golangci-lint runsilently ignores unrecognized config keys — a bogus top-level key and a bogus key nested underrun:both gave0 issues.and exit 0 — so verify is the only thing catching a typo that disables a setting. And in the pinned image verify needs no network at all: with--network noneit still detected the bogus key (rc=3). So it is kept, and both lint steps useRUN --network=none, which enforces the absence of a live fetch rather than trusting it. That also makes the whole analysis phase provably network-free.This supersedes #106 — that issue's option 2, and structurally: findings are reported relative to the repo root inside a container holding only this repo, so the
../other-worktree/...contamination cannot occur rather than being filtered after the fact.clawbot referenced this issue2026-08-17 23:50:11 +02:00