#!/bin/sh # script/verify-linter-pin: fail unless a golangci-lint binary is exactly # the version script/bootstrap pins. Takes the binary to check as its # argument, defaulting to whatever PATH resolves. Our own extension to # scripts-to-rule-them-all, not one of its entrypoints. # # The Dockerfile build stage runs this on the linter it copies out of the # lint stage, before anything else runs there. Without it, drift between # the two stages is silently absorbed: script/bootstrap reinstalls its # pinned version from source, verifies that, and the build goes green # with the lint stage having linted at one version and `make check` # having run at another. Bumping the lint stage image alone is enough to # produce that, and this is the check that turns it into a build failure # naming both versions. # # The pin is read out of script/bootstrap rather than restated here. # script/bootstrap is the single source of truth for the linter version, # and a second hardcoded copy of it is exactly the drift this script # exists to catch. A pin that cannot be read is therefore a hard failure # and not a skip: silently comparing against an empty string would turn # this check into the kind of unearned green it was written to stop. set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" # Seconds to allow `golangci-lint --version` to run, so a wedged binary # stops the build instead of hanging it. Bounded by timeout(1) where that # exists; stock macOS has none, and there the call runs unbounded. VERSION_TIMEOUT="30" version_output() { if command -v timeout >/dev/null 2>&1; then timeout "$VERSION_TIMEOUT" "$1" --version else "$1" --version fi } main() { # Resolve the argument before changing directory, so a relative path # means what the caller meant by it. bin="${1:-golangci-lint}" resolved="$(command -v "$bin" 2>/dev/null || true)" cd "$ROOT" pin="$( sed -n 's/^GOLANGCI_LINT_VERSION="\([^"]*\)".*/\1/p' script/bootstrap )" if [ -z "$pin" ]; then echo "verify-linter-pin: no GOLANGCI_LINT_VERSION assignment found" \ "in script/bootstrap; that file is the single source of truth" \ "for the linter version and this check cannot run without it" >&2 exit 1 fi if [ -z "$resolved" ]; then echo "verify-linter-pin: $bin: not found (pin is $pin)" >&2 exit 1 fi # Same output shape script/bootstrap parses: # 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", with # any leading "v" stripped. stderr is left connected so a binary that # cannot execute (wrong architecture, missing shared library) says why # rather than being reported as merely unparseable. if ! out="$(version_output "$resolved")"; then echo "verify-linter-pin: $resolved --version failed; the binary" \ "cannot be executed or timed out (pin is $pin)" >&2 exit 1 fi found="$( echo "$out" | awk ' { for (i = 1; i < NF; i++) { if ($i == "version") { v = $(i + 1) sub(/^v/, "", v) print v exit } } } ' )" if [ "$found" != "$pin" ]; then echo "verify-linter-pin: $resolved reports" \ "${found:-no parseable version}, but script/bootstrap pins" \ "$pin" >&2 echo "verify-linter-pin: these must be the same version — bump the" \ "Dockerfile lint stage image and GOLANGCI_LINT_VERSION in" \ "script/bootstrap together" >&2 exit 1 fi echo "verify-linter-pin: $resolved is $found, matching the" \ "script/bootstrap pin" } main "$@"