make lint does not use the pinned linter version, so local green does not mean CI green #78
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?
make lintand CI run different linter versions, and nothing warnsabout it. This has now produced two successive false "make check is
green" claims (commit
7ae470e, thenb960ca3on PR #77), which isenough evidence that it is a tooling defect rather than carelessness.
The gap
script/lintinvokes the linter straight offPATH:CI does not.
Dockerfile:2-3pins it by digest:make depsdoes installgolangci-lint@v2.12.2, but it is a separatetarget that
lintneither depends on nor verifies, so whatever thedeveloper happens to have installed wins. On this machine that is
2.10.1, from
script/bootstrapinstalling the distro packageunpinned.
Why it matters
The two versions disagree about real findings. Under 2.10.1 the PR #77
tree is clean; under the pinned 2.12.2 it fails with four
nolintlintfindings, because gosec in 2.12.2 does not report at four sites where
2.10.1 does, making those
//nolint:gosecdirectives unused.The failure mode is the worst kind:
make checkexits 0, the developerreports green in good faith, and CI then fails on the pinned version. A
gate that can be green locally and red in CI is not a gate.
Definition of done
make lintuses the same pinned linter as CI. Either:script/lintruns the pinned Docker image(
golangci/golangci-lint:v2.12.2-alpine@sha256:91b2…), matching theDockerfile exactly; or
script/lintverifies the installed binary's version equalsthe pin and fails loudly with the expected-vs-actual version if
not, instructing the developer to run
make deps.(a) is preferred — it removes the possibility of drift rather than
detecting it, and matches the repo's docker-only linting policy. Pick
(b) only if the container round-trip makes the inner loop unusably
slow, and say so.
The version pin lives in exactly one place, referenced by
Dockerfile,script/lint, andmake deps. Today it is duplicatedacross
Dockerfile:3andMakefile:62and can drift silently.script/bootstrapno longer installs an unpinnedgolangci-lintthatcan shadow the pinned one, or installs the pinned version explicitly.
Verify the fix catches the real case: with a deliberately wrong local
linter installed,
make lintmust fail or transparently use thepinned version — it must not silently pass.
Document in the README Entrypoints section that
make checkisauthoritative only because it uses the pinned linter, and note
script/cibuildas the full CI-equivalent gate.script/cibuildexits 0.Note
Until this lands,
script/cibuildis the only trustworthy gate inthis repo, and
make checkalone must not be used to claim a change isgreen.
Manager note — dispatching this first, ahead of the rest of the
1.0.0milestone.
Rationale for the ordering: every other issue in the milestone has "
make checkgreen" or "script/cibuildexits 0" in its definition of done.While
make lintcan disagree with CI, none of those definitions of donemeans what it says, and each one is an opportunity to repeat today's
failure. #61 burned two full implement-review cycles on exactly this. Fix
the gate, then the rest of the backlog can be trusted.
Now confirmed on
mainate496aa3:mainis green under the pinned linter (script/cibuild→EXIT=0,0 issues.), so this work starts from a clean baseline — any findingthat appears is caused by the change itself.
Implementation requirements
script/lintruns the digest-pinned Dockerimage. Detecting drift still leaves two ways to run the linter;
removing the ambient path removes the failure mode. It also matches
the docker-only linting policy the repo already follows in CI.
Dockerfile:2-3(image + digest) andMakefile:62(go install ...@v2.12.2). After this change there must be exactly one place ahuman edits to bump the linter, with the others deriving from it.
State in the commit message where that place is.
v2.12.2-alpinealone ismutable;
@sha256:91b27804074a0bacea298707f016911e60cf0cdbc6c7bf5ccacb5f0606d18d60is what makes the gate reproducible.
is: a directive that is unused under 2.12.2 but justified under
2.10.1. Demonstrate that
make lintnow fails on such a case evenwith 2.10.1 installed on the host — e.g. temporarily add a
//nolint:gosecat a site gosec does not flag, confirmmake lintfails, then remove it. Record that check in the PR.
script/lintmust fail with a clear, actionable message — neversilently fall back to the
PATHbinary, which is precisely the bug.mount the Go build/module cache so repeat runs stay usable, and note
the measured before/after in the PR.
script/precommitand any other caller ofscript/lintmust keepworking.
Do not change
.golangci.yml(sha256021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb), anddo not fix the
gomodguarddeprecation — that lives in the canonicalconfig upstream and is tracked in sibling repos.
Implementation plan (option (a), per the manager comment).
Confirmed the drift in both directions on
mainate496aa3: with theambient 2.10.1 on this host,
script/lintcurrently reports4 issues: gosec: 4on a tree that CI (2.12.2) lints clean. So todaymake lintisred locally and green in CI on the same commit — the mirror image of the
failure that produced this issue.
Plan:
Dockerfilelint stageFROMline.It is the one place that can hold the image reference (Docker needs a
literal for
FROM), so everything else derives from it rather thanrestating it.
script/lintextracts the reference withawkon$1 == "FROM" && $4 == "lint", keeping tag and digest, and runsthat exact image.
Makefile:62(go install ...@v2.12.2) and the unpinnedgolangci-lintinstall inscript/bootstrap(DoD item 3). With linting containerized, anambient binary is only a shadowing hazard, so deleting the second and
third copies of the pin is a stronger fix than keeping them in sync.
Exactly one line then needs editing to bump the linter.
script/lintruns the pinned image viadocker run, mounting therepo at
/src, with--user $(id -u):$(id -g)so nothing landsroot-owned, plus persistent
GOCACHE/GOMODCACHE/GOLANGCI_LINT_CACHEmounts under${XDG_CACHE_HOME:-~/.cache}forthe inner loop (requirement 6; before/after timings go in the PR).
Dockerfile:18runsmake lintinsidethe lint image, where there is no Docker daemon.
script/lintrunsthe binary natively only when
golangci-lint versionreports aversion exactly equal to the pin parsed from the
FROMline — that isa verified identity, not a fallback. Any other version, or no binary,
goes to Docker.
dockeris missing ordocker infofails, exit nonzero with the image reference and what to do.There is no path from a mismatched
PATHbinary to a passing lint.script/lint-fixbecomesscript/lint --fix(oneimplementation,
--fixneeds the same pinned autofixer);script/lintwith no arguments is unchanged, so
script/check,script/precommit,make lintandDockerfile:18keep working.//nolint:gosecwhere 2.12.2's gosec does not fire, confirmmake lintfails onnolintlintdespite 2.10.1 being onPATH, thenremove it. Result recorded in the PR body.
TODO.mdupdated in the same commit;gate on
script/cibuild..golangci.ymlis not touched (verifying its sha256 before push), andthe
gomodguarddeprecation is left alone.