feat(doctor): report golangci-lint built with a Go older than GOROOT
CI / Check / Test (push) Successful in 3s
CI / Tag release (push) Successful in 1s

Second recurrence of the same host fault (infra#375; first hit
openbanking-api 2026-09-03, then cobalt-dingo 2026-09-08). golangci-lint
embeds the go/types export-data reader from the Go it was BUILT with, so a
newer GOROOT makes it unable to decode the stdlib:

  panic: file requires newer Go version go1.27 (application built with go1.26)

An earlier form printed a typecheck error naming the user's own source file,
which sends people to debug code that is fine. It cost time in two repos before
anyone recognised it as a host fault rather than a repo fault.

CI never sees it — the runner image has matching versions — so it breaks
exactly the local `task check` that AGENT.md requires before every push, and
nothing else. The fix was always one reinstall; the missing thing was the
signal, which is what doctor is for.

Fails open by design: an unparseable version, or no Go at all, is a normal host
and not a fault doctor may assert. Version comparison is major.minor in POSIX
sh rather than sort -V, since this runs on macOS too and the export-data format
is minor-versioned.

Also makes the suite hermetic. The check reads the real host, so on a machine
whose golangci-lint is genuinely stale every unrelated fixture started failing.
The harness now prepends a matching fake pair for the whole file, and only the
four cases that opt in exercise the check.

Verified against koala, which is currently in exactly this state:

  FAIL toolchain: golangci-lint built with go1.26.1 but GOROOT is go1.27.0
       — lint panics on every Go repo; reinstall it under the current
       toolchain (infra#375)

doctor: 25 passed, 0 failed. Full suite green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-08 21:34:44 +02:00
co-authored by Claude Opus 5
parent 6cc224f151
commit 1328dcd8f3
2 changed files with 120 additions and 0 deletions
+38
View File
@@ -327,6 +327,44 @@ if [ "$ONLINE" -eq 1 ]; then
fi
fi
# --- toolchain: golangci-lint must be built with Go >= GOROOT ---------------
# infra#375, second recurrence. golangci-lint embeds the go/types export-data
# reader from the Go it was BUILT with. When GOROOT is newer, that reader
# cannot decode the stdlib and the linter dies:
#
# panic: file requires newer Go version go1.27 (application built with go1.26)
#
# An earlier form printed a typecheck error naming the user's own file, which
# sends people to debug code that is fine. It cost time in two repos before
# anyone recognised it as a host fault rather than a repo fault.
#
# It is invisible to CI -- the runner image has matching versions -- so it
# breaks exactly the local `task check` that AGENT.md requires before a push,
# and nothing else. That is why it belongs in doctor: the signal was missing,
# not the fix.
#
# Fails open on anything unparseable. A host with no Go toolchain is a normal
# host, not a broken one, and doctor runs in a SessionStart hook everywhere.
ver_lt() {
# 0 when $1 < $2 on major.minor. Deliberately not sort -V: this is POSIX sh
# that also runs on macOS, and the export-data format is minor-versioned, so
# patch level does not enter into it.
_a=${1%%.*}; _ar=${1#*.}; _am=${_ar%%.*}
_b=${2%%.*}; _br=${2#*.}; _bm=${_br%%.*}
case "$_a$_am$_b$_bm" in *[!0-9]*) return 1 ;; esac
[ "$_a" -lt "$_b" ] && return 0
[ "$_a" -gt "$_b" ] && return 1
[ "$_am" -lt "$_bm" ]
}
if command -v golangci-lint >/dev/null 2>&1 && command -v go >/dev/null 2>&1; then
lint_go=$(golangci-lint --version 2>/dev/null | sed -n 's/.*built with go\([0-9][0-9.]*\).*/\1/p' | head -1)
root_go=$(go version 2>/dev/null | sed -n 's/.*go version go\([0-9][0-9.]*\).*/\1/p' | head -1)
if [ -n "$lint_go" ] && [ -n "$root_go" ] && ver_lt "$lint_go" "$root_go"; then
add FAIL toolchain "golangci-lint built with go$lint_go but GOROOT is go$root_go — lint panics on every Go repo; reinstall it under the current toolchain (infra#375)"
fi
fi
# --- output -----------------------------------------------------------------
if [ "$JSON" -eq 1 ]; then
printf '{"checkout":"%s","expected":%s,"plugin":{"installed":"%s","available":"%s"},"findings":[%s]}\n' \