Run all linting in Docker via Dockerfile.lint + script/lint #55
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, and it works even where the docker daemon is remote and bind mounts are impossible.Note this repo's Dockerfile carries a memlock ulimit for memguard; make sure the lint image does not need it and that nothing about the split changes the main build's behaviour.
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. Caching is explicitly waived here, so force the lint layers to execute.golangci-lint config verifyfetches 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/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.lint, two stages off the pinnedgolangci/golangci-lint:v2.12.2@sha256:5ccee...digest:deps(COPYgo.mod/go.sum,go mod download) andlint(COPY the tree,golangci-lint run --config .golangci.yml ./...). Splitting the stages lets the module download stay cached while the lint stage is force-invalidated.script/lintbecomesdocker build --progress=plain --no-cache-filter=lint -f Dockerfile.lint ..--no-cache-filtertargets the namedlintstage, so the linter re-executes on every run on an unchanged tree;--progress=plainkeeps the linter's output visible. No hostgolangci-lintpath remains.Dockerfile: its lint stage currently runsmake fmt-check/make lint.make lintwould now recurse into adocker buildinside a build, so that stage invokesgofmtandgolangci-lintdirectly instead (same shape as the reference repo'sDockerfile). Stage ordering, base image digests, the builder/runtime stages and the memlock ulimit inscript/cibuildare untouched, soscript/cibuildbehaviour is unchanged and CI keeps linting.script/bootstrap: drop thegolangci-lintinstall block; update the README Entrypoints section accordingly.golangci-lint config verify: leaning toward excluding it — it fetches its JSON schema over an unpinned live HTTPS call, which conflicts with the repo's hash-pinning policy and makes lint fail on any network outage. Final decision and rationale go in the PR body.script/lintruns on an unchanged tree, both showing the linter executing; a negative control (deliberate violation -> failure naming that finding -> revert -> clean); confirmation that the lint image needs no memlock ulimit;script/cibuild.TODO.mdgets one additive entry in the same commit.