`go install` writes into GOBIN (or GOPATH/bin), but the linter `make
lint` runs is whichever golangci-lint PATH resolves first. On a host
where a wrong-version binary sits ahead of that directory — a nix
profile, apt, brew, apk, a tarball in /usr/local/bin, or the
/usr/local/bin copy the Dockerfile builder stage makes — the install
landed behind the shadow, changed nothing the gate uses, and bootstrap
still printed "bootstrap complete" and exited 0. That leaves the local
gate linting against a different ruleset than CI while affirmatively
claiming otherwise, and every subsequent run reinstalls forever, so the
second run is never a no-op.
After installing, re-read the effective version. On a mismatch print
the resolved binary, the install directory and both versions to stderr
and exit non-zero. Do not reorder PATH or remove anyone's binary:
diagnose and stop.
Also:
- stop discarding `golangci-lint --version` stderr, so a present but
broken binary (missing shared library, wrong architecture) says why
instead of silently yielding the empty string and reinstalling on
every run forever. Only stdout is parsed, so the parse matrix is
unchanged.
- bound the `--version` call with timeout(1) where it exists, since
bootstrap now executes a binary it previously only located and a
wedged one would otherwise hang the script. Hosts without timeout(1)
run it unbounded, as before.
- use X.Y.Z in the parsing comment so the pinned version stays a single
literal in the script.
script/bootstrap installed the pinned linter only when the command was
absent, so on any host that already had some golangci-lint the pin was
never consulted and a version bump was inert forever. That is how a
host running v2.10.1 against a v2.12.2 pin got a green `make check`
while `make docker` rejected the same commit: the local gate was
linting with a different ruleset than CI, and the disagreement only
surfaced after a push.
The version is now a single value, GOLANGCI_LINT_VERSION, with the
`go install` module ref derived from it, so a future bump cannot
half-apply. A golangci_lint_version helper parses the installed
version out of `golangci-lint --version` (the field after the word
"version", with an optional leading "v" stripped, since the module ref
carries one and the binary's output does not) and yields the empty
string when the tool is absent or unreadable. Any version that is not
the pin -- older, newer, absent or unparseable -- is reinstalled, so a
first run upgrades and a second is a no-op.
git, make and go keep their presence-only checks: they come from the
host package manager, the repo pins no system toolchain versions, and
go.mod governs the language version. That is now stated in a comment
next to them rather than left ambiguous beside a tool that is
version-checked.
Bump the pinned golangci-lint from v2.12.1 to v2.12.2 in the
Dockerfile lint stage (tagged, digest-pinned Debian image) and in
script/bootstrap (go install ref). Replace .golangci.yml with the
canonical config: linter settings (lll, funlen, cyclop, dupl) move
under linters.settings per the v2 schema so they are actually
applied, and the redundant issues.exclude-use-default key is
dropped. No new lint findings surfaced; make check is green.
Bring the repo into conformance with the scripts-to-rule-them-all
(STRTA) scaffold. The real logic that lived inline in the Makefile now
lives in POSIX-sh entrypoints under script/, and the Makefile's standard
targets are thin @script/NAME shims.
- script/: bootstrap, setup, projectname, test, lint, fmt, fmt-check,
check, docker, precommit, install-precommit, cibuild. All are
executable #!/bin/sh entrypoints; the go mod tidy guard from the old
inline hooks recipe moved into script/precommit.
- Makefile: the nine standard targets (bootstrap, setup, test, lint,
fmt, fmt-check, check, docker, hooks) are now thin shims; the
repo-specific sfdupes/build/clean targets and the CGO_ENABLED export
are preserved.
- .gitea/workflows/check.yml: run script/cibuild instead of a bare
docker build.
- Dockerfile: run make check (and the build) as an unprivileged builder
user rather than root. We should never build or run as root, and doing
so also lets the permission-denied tests run legitimately: root
bypasses the chmod(0) that TestScanHardlinkRunFailsTogether relies on,
which made the in-image make check fail. HOME and the Go caches point
at the user's home so go build/test and golangci-lint can write.
make check passes locally and docker build . is green (the in-image
non-root make check passes, including the hardlink permission test).