From 9e924721e6d2b4287061d7cdf2f1f0647cf26237 Mon Sep 17 00:00:00 2001 From: clawbot Date: Sun, 9 Aug 2026 05:52:55 +0000 Subject: [PATCH] Check the golangci-lint version in bootstrap, not just presence (closes #24) 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. --- TODO.md | 23 +++++++++++++++++++++ script/bootstrap | 54 +++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/TODO.md b/TODO.md index e42194b..6be7f1a 100644 --- a/TODO.md +++ b/TODO.md @@ -29,6 +29,29 @@ # Completed Steps +- check the installed golangci-lint version in `script/bootstrap` + instead of only its presence (2026-08-09, branch + `bootstrap-version-check`, closes #24): `missing golangci-lint` meant + any linter already on `PATH` satisfied the check, so the pin was never + consulted and the v2.12.2 bump from #3 was inert on every host that + already had one — this host ran v2.10.1 against a v2.12.2 pin, + `make check` went green, and `make docker` then rejected the same + commit with findings the local gate never saw. The version now lives + in one place, `GOLANGCI_LINT_VERSION`, with the `go install` module + ref derived from it so a bump cannot half-apply; a + `golangci_lint_version` helper parses `golangci-lint --version` + (taking the field after the word `version` and tolerating an optional + leading `v`, which the module ref carries and the binary's output does + not), and any version that is not the pin — older, newer, absent or + unparseable — is reinstalled. `git`, `make` and `go` keep their + presence-only checks and now say why in a comment: they are host + package-manager tools the repo deliberately does not pin, with + `go.mod` governing the language version and the digest-pinned images + covering reproducible builds. Verified on this host by bootstrapping + from v2.10.1 to v2.12.2 and running it again to a no-op, plus stub + runs under `dash` covering the absent, older, newer, image-style and + leading-`v` cases; `make check` and `make lint` are clean at v2.12.2, + so v2.10.1 was not hiding any findings on `main` - unwind the hash worker pool on the error path (2026-08-09, branch `hash-pool-cleanup`, closes #6): `hashPhase` used to return the moment `recordRun` failed and abandon the pool — the feeder parked diff --git a/script/bootstrap b/script/bootstrap index 1103c49..78709fc 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -4,14 +4,20 @@ # installed tools are skipped. Base tooling comes from nix, apt, brew, # or apk (detected in that order); assumes nothing is present. # golangci-lint is installed via `go install` pinned to the same version -# the Dockerfile lint stage uses (never "latest"). +# the Dockerfile lint stage uses (never "latest"), and is reinstalled +# whenever the installed version differs from that pin. set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" # Pinned versions, 2026-08-07 (same version as the Dockerfile lint stage). -# golangci-lint v2.12.2 -GOLANGCI_LINT_REF="github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2" +# This is the single source of truth for the linter version: the module +# ref below and the version comparison in main() are both derived from +# it, so a bump here cannot half-apply. Written without a leading "v", +# the way `golangci-lint --version` reports it. +GOLANGCI_LINT_VERSION="2.12.2" +GOLANGCI_LINT_MODULE="github.com/golangci/golangci-lint/v2/cmd/golangci-lint" +GOLANGCI_LINT_REF="$GOLANGCI_LINT_MODULE@v$GOLANGCI_LINT_VERSION" PKGMGR="" SUDO="" @@ -60,16 +66,54 @@ missing() { ! command -v "$1" >/dev/null 2>&1 } +# Echo the installed golangci-lint version, or nothing when the tool is +# absent. The binary reports e.g. +# golangci-lint has version 2.12.2 built with go1.26.5 from abc1234 ... +# so the version is the field after the literal word "version", and it +# carries no leading "v" (the module ref does). Some builds do print a +# leading "v", so strip one if present and compare bare versions. +golangci_lint_version() { + command -v golangci-lint >/dev/null 2>&1 || return 0 + golangci-lint --version 2>/dev/null | awk ' + { + for (i = 1; i < NF; i++) { + if ($i == "version") { + v = $(i + 1) + sub(/^v/, "", v) + print v + exit + } + } + } + ' +} + main() { cd "$ROOT" + # System tooling, deliberately unpinned: these come from the host + # package manager and whatever version it ships is what the host + # gets, so a presence check is the right check. The repo pins no + # system toolchain versions — the Go language version is governed by + # go.mod, and builds that must be reproducible run in the Docker + # image, whose base images are pinned by digest. if missing git; then pkg_install git git git git; fi if missing make; then pkg_install gnumake make make make; fi if missing go; then pkg_install go golang go go; fi # Lint tooling, pinned via go install (installs into - # "$(go env GOPATH)/bin"; ensure that is on your PATH). - if missing golangci-lint; then go install "$GOLANGCI_LINT_REF"; fi + # "$(go env GOPATH)/bin"; ensure that is on your PATH). Unlike the + # system tools above this is version-checked, not presence-checked: + # the Dockerfile lint stage runs a digest-pinned linter, so a host + # running any other version lints against different rules and + # `make check` can go green on a commit CI then rejects. Any version + # that is not the pin — older or newer — is reinstalled. + installed="$(golangci_lint_version)" + if [ "$installed" != "$GOLANGCI_LINT_VERSION" ]; then + echo "bootstrap: golangci-lint ${installed:-absent or unparseable}," \ + "want $GOLANGCI_LINT_VERSION; installing" + go install "$GOLANGCI_LINT_REF" + fi go mod download