Check the golangci-lint version in bootstrap, not just presence (closes #24) #34

Merged
clawbot merged 2 commits from bootstrap-version-check into main 2026-08-09 08:38:36 +02:00
2 changed files with 86 additions and 13 deletions
Showing only changes of commit 9d06c13777 - Show all commits

36
TODO.md
View File

@@ -43,15 +43,33 @@
(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`
unparseable — is reinstalled. The install is then verified against the
binary `PATH` actually resolves: `go install` writes into `GOBIN` (or
`GOPATH/bin`) while `make lint` runs whichever `golangci-lint` comes
first on `PATH`, so a wrong-version one sitting ahead of it — nix,
apt, brew, apk, or the `/usr/local/bin` copy the `Dockerfile` builder
stage makes — would swallow the install and leave the local gate
disagreeing with CI under an affirmative `bootstrap complete`.
Bootstrap now re-reads the effective version after installing and, on
a mismatch, prints both paths and both versions to stderr and exits
non-zero instead of claiming success; it does not reorder anyone's
`PATH` or delete their binary. The `--version` call keeps its stderr
connected, so a present-but-broken binary says why rather than
reinstalling forever in silence, and is bounded by `timeout(1)` where
that exists, so a wedged binary cannot hang bootstrap. `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 of the real script under `dash` covering a
thirteen-input parse matrix (absent, older, newer, host-style,
image-style, leading-`v`, stderr-only, empty, non-zero exit, impostor
binary, `(devel)`, trailing `version`), a shadowed install that must
exit non-zero, an install destination not on `PATH` at all, `GOBIN`
set, and a wedged binary that must hit the timeout; `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

View File

@@ -5,7 +5,10 @@
# 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"), and is reinstalled
# whenever the installed version differs from that pin.
# whenever the installed version differs from that pin. The install is
# then verified against the binary PATH actually resolves: if the pin is
# still not what would run, bootstrap fails instead of reporting
# success.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
@@ -19,6 +22,11 @@ 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"
# Seconds to allow `golangci-lint --version` to run. Bootstrap now
# executes the binary rather than merely locating it, so a wedged one
# must not hang the script.
GOLANGCI_LINT_VERSION_TIMEOUT="30"
PKGMGR=""
SUDO=""
APT_UPDATED=""
@@ -68,13 +76,24 @@ missing() {
# 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 ...
# golangci-lint has version X.Y.Z 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.
#
# Only stdout is parsed; the binary's stderr is deliberately left
# connected to ours so that a present-but-broken linter (missing shared
# library, wrong architecture) says why instead of silently yielding the
# empty string. The call is bounded by timeout(1) where that exists —
# stock macOS has no timeout(1), and there the call runs unbounded, as
# it did before this check was version-aware.
golangci_lint_version() {
command -v golangci-lint >/dev/null 2>&1 || return 0
golangci-lint --version 2>/dev/null | awk '
if command -v timeout >/dev/null 2>&1; then
timeout "$GOLANGCI_LINT_VERSION_TIMEOUT" golangci-lint --version
else
golangci-lint --version
fi | awk '
{
for (i = 1; i < NF; i++) {
if ($i == "version") {
@@ -88,6 +107,40 @@ golangci_lint_version() {
'
}
# Confirm that the golangci-lint just installed is the one that will
# actually run. `go install` writes into "$(go env GOBIN)" (or
# "$(go env GOPATH)/bin"), but `make lint` runs whatever PATH resolves
# first. When a wrong-version binary sits ahead of that directory — a
# nix profile, apt, brew, apk, or a tarball in /usr/local/bin — the
# install lands behind the shadow and changes nothing the gate uses.
# Exiting 0 there would leave the local gate linting against a different
# ruleset than CI while claiming success, which is the failure this
# whole check exists to prevent. Diagnose and stop: naming both paths is
# what makes it fixable. Reordering PATH or deleting someone else's
# binary is not bootstrap's call.
verify_golangci_lint() {
# Forget any remembered command locations first: the install may have
# created a binary in a directory the shell already searched.
hash -r 2>/dev/null || true
goinstalldir="$(go env GOBIN)"
if [ -z "$goinstalldir" ]; then
goinstalldir="$(go env GOPATH)/bin"
fi
resolved="$(command -v golangci-lint 2>/dev/null || true)"
effective="$(golangci_lint_version)"
if [ "$effective" != "$GOLANGCI_LINT_VERSION" ]; then
echo "bootstrap: installed golangci-lint $GOLANGCI_LINT_VERSION into" \
"$goinstalldir, but the golangci-lint on PATH is" \
"${resolved:-not resolvable} and reports" \
"${effective:-no parseable version}" >&2
echo "bootstrap: the install is shadowed or unreachable; put" \
"$goinstalldir ahead of it on PATH (or remove the shadowing" \
"binary) and re-run" >&2
exit 1
fi
}
main() {
cd "$ROOT"
@@ -107,12 +160,14 @@ main() {
# 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.
# that is not the pin — older or newer — is reinstalled, and the
# install is then verified to be the binary PATH resolves.
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"
verify_golangci_lint
fi
go mod download