Merge branch 'bootstrap-version-check': check the linter version, not just presence (closes #24)
All checks were successful
check / check (push) Successful in 4s
All checks were successful
check / check (push) Successful in 4s
script/bootstrap installed the pinned golangci-lint only when the command was absent, so a version bump was inert on any host that already had the tool. This one ran v2.10.1 against a v2.12.2 pin: local make check went green while make docker rejected the same commit with six goconst findings. Bootstrap now compares the installed version against the pin, which lives in exactly one place, and reinstalls on mismatch. It then verifies the install actually took effect: go install writes to GOPATH/bin, but the binary make lint runs is whatever PATH resolves, so a wrong-version linter shadowing it earlier on PATH would otherwise leave bootstrap printing success having changed nothing. On mismatch it now names both paths on stderr and exits non-zero. The first review caught that shadowing case. Re-reviewed independently by a fresh reviewer, who rebuilt the reproduction from scratch across seven PATH layouts, ran a 20-row version-parse matrix confirming no input yields a false match, and forced a cold Docker build after finding the cached one executed nothing. Confirmed along the way that v2.10.1 was hiding no findings on main.
This commit was merged in pull request #34.
This commit is contained in:
41
TODO.md
41
TODO.md
@@ -29,6 +29,47 @@
|
|||||||
|
|
||||||
# Completed Steps
|
# 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. 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
|
- unwind the hash worker pool on the error path (2026-08-09, branch
|
||||||
`hash-pool-cleanup`, closes #6): `hashPhase` used to return the
|
`hash-pool-cleanup`, closes #6): `hashPhase` used to return the
|
||||||
moment `recordRun` failed and abandon the pool — the feeder parked
|
moment `recordRun` failed and abandon the pool — the feeder parked
|
||||||
|
|||||||
109
script/bootstrap
109
script/bootstrap
@@ -4,14 +4,28 @@
|
|||||||
# installed tools are skipped. Base tooling comes from nix, apt, brew,
|
# installed tools are skipped. Base tooling comes from nix, apt, brew,
|
||||||
# or apk (detected in that order); assumes nothing is present.
|
# or apk (detected in that order); assumes nothing is present.
|
||||||
# golangci-lint is installed via `go install` pinned to the same version
|
# 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. 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
|
set -eu
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
# Pinned versions, 2026-08-07 (same version as the Dockerfile lint stage).
|
# Pinned versions, 2026-08-07 (same version as the Dockerfile lint stage).
|
||||||
# golangci-lint v2.12.2
|
# This is the single source of truth for the linter version: the module
|
||||||
GOLANGCI_LINT_REF="github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2"
|
# 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"
|
||||||
|
|
||||||
|
# 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=""
|
PKGMGR=""
|
||||||
SUDO=""
|
SUDO=""
|
||||||
@@ -60,16 +74,101 @@ missing() {
|
|||||||
! command -v "$1" >/dev/null 2>&1
|
! 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 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
|
||||||
|
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") {
|
||||||
|
v = $(i + 1)
|
||||||
|
sub(/^v/, "", v)
|
||||||
|
print v
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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() {
|
main() {
|
||||||
cd "$ROOT"
|
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 git; then pkg_install git git git git; fi
|
||||||
if missing make; then pkg_install gnumake make make make; fi
|
if missing make; then pkg_install gnumake make make make; fi
|
||||||
if missing go; then pkg_install go golang go go; fi
|
if missing go; then pkg_install go golang go go; fi
|
||||||
|
|
||||||
# Lint tooling, pinned via go install (installs into
|
# Lint tooling, pinned via go install (installs into
|
||||||
# "$(go env GOPATH)/bin"; ensure that is on your PATH).
|
# "$(go env GOPATH)/bin"; ensure that is on your PATH). Unlike the
|
||||||
if missing golangci-lint; then go install "$GOLANGCI_LINT_REF"; fi
|
# 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, 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
|
go mod download
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user