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