All checks were successful
check / check (pull_request) Successful in 2m18s
`script/lint` ran whatever `golangci-lint` was on `PATH` while CI ran
the digest-pinned image from the `Dockerfile` lint stage. The two
versions disagree about real findings, so `make check` could be green
on a tree CI fails - and, on this host's 2.10.1, red on a tree CI
passes. A gate that can differ from CI is not a gate.
`script/lint` now runs the pinned image itself. The single source of
truth for the linter version is the `Dockerfile` lint stage `FROM`
line: `script/lint` parses the image reference (tag AND digest) out of
it with awk and runs exactly that image, so bumping the linter is a
one-line edit there and nowhere else. The duplicate pin in the
`Makefile` `deps` target (`go install ...@v2.12.2`) and the unpinned
`golangci-lint` install in `script/bootstrap` are removed rather than
kept in sync: with linting containerized, a second copy on `PATH` is
only a way to drift.
A `golangci-lint` on `PATH` is used only when its version is exactly
equal to the pin - the same binary by definition, and the case that
matters is the lint stage itself, which runs `make lint` inside the
pinned container where no Docker daemon exists. Every other version
goes through Docker, and a missing or unreachable daemon is a hard
error naming the required image, never a silent fallback.
The container run mounts persistent `GOCACHE`, `GOMODCACHE` and
`GOLANGCI_LINT_CACHE` directories under `${XDG_CACHE_HOME:-~/.cache}`
and runs as the invoking uid/gid, so repeat runs stay fast (2.7s warm
vs 1.8s for the ambient binary) and nothing lands root-owned.
`script/lint-fix` delegates to `script/lint --fix` so autofixes come
from the same pinned linter.
README documents that `make check` is authoritative because of this,
and points at `script/cibuild` as the full CI-equivalent gate.
127 lines
3.6 KiB
Bash
Executable File
127 lines
3.6 KiB
Bash
Executable File
#!/bin/sh
|
|
# script/lint: run the linter.
|
|
#
|
|
# The linter always runs at the version pinned by the Dockerfile's lint
|
|
# stage, so a local run and a CI run of the same tree cannot disagree.
|
|
# That FROM line (image tag plus digest) is the single source of truth
|
|
# for the linter version in this repo: bump it there and nothing else
|
|
# needs editing.
|
|
#
|
|
# Normally that means running the pinned image with docker. The one
|
|
# exception is a golangci-lint on PATH whose version is exactly equal to
|
|
# the pin: that is the same linter, so it is run directly. This is what
|
|
# happens inside the lint container itself (Dockerfile runs `make lint`,
|
|
# and there is no docker daemon in there). A PATH binary at any other
|
|
# version is never used - that silent substitution is the bug this
|
|
# script exists to prevent.
|
|
#
|
|
# Extra arguments are passed through to `golangci-lint run`, before
|
|
# `./...` (see script/lint-fix).
|
|
set -eu
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
DOCKERFILE="$ROOT/Dockerfile"
|
|
|
|
# The image reference of the Dockerfile's lint stage, tag and digest
|
|
# included, e.g.
|
|
# golangci/golangci-lint:v2.12.2-alpine@sha256:91b2...
|
|
lint_image() {
|
|
awk '$1 == "FROM" && $3 == "AS" && $4 == "lint" { print $2; exit }' \
|
|
"$DOCKERFILE"
|
|
}
|
|
|
|
# The bare version that image reference pins, e.g. 2.12.2
|
|
pinned_version() {
|
|
lint_image | sed -e 's/@.*//' -e 's/.*://' -e 's/^v//' -e 's/-.*//'
|
|
}
|
|
|
|
# The version of the golangci-lint on PATH, if any, e.g. 2.12.2
|
|
installed_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 ~ /^[0-9]+\.[0-9]+\.[0-9]+$/) {
|
|
print $i
|
|
exit
|
|
}
|
|
}
|
|
}'
|
|
}
|
|
|
|
require_docker() {
|
|
image="$1"
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
cat >&2 <<EOF
|
|
lint: docker is required to run the pinned linter.
|
|
|
|
pinned image: $image
|
|
|
|
Install docker, or install golangci-lint $(pinned_version) on PATH.
|
|
Linting with any other version is not supported: it is what lets a
|
|
local run pass while CI fails.
|
|
EOF
|
|
exit 1
|
|
fi
|
|
if ! docker info >/dev/null 2>&1; then
|
|
cat >&2 <<EOF
|
|
lint: the docker daemon is not reachable, so the pinned linter cannot
|
|
run.
|
|
|
|
pinned image: $image
|
|
|
|
Start the daemon (and check DOCKER_HOST / your group membership), or
|
|
install golangci-lint $(pinned_version) on PATH. This script will not
|
|
fall back to a different linter version.
|
|
EOF
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Caches for the containerized linter. Keeping them out of the repo and
|
|
# persisting them between runs is what keeps the inner loop fast: a warm
|
|
# run costs about the same as a native one plus container startup.
|
|
cache_root() {
|
|
echo "${XDG_CACHE_HOME:-${HOME:-/tmp}/.cache}/vaultik-lint"
|
|
}
|
|
|
|
run_in_docker() {
|
|
image="$1"
|
|
shift
|
|
require_docker "$image"
|
|
|
|
cache="$(cache_root)"
|
|
mkdir -p "$cache/go-build" "$cache/go-mod" "$cache/golangci-lint"
|
|
|
|
exec docker run --rm \
|
|
--user "$(id -u):$(id -g)" \
|
|
--env HOME=/tmp \
|
|
--env GOFLAGS=-buildvcs=false \
|
|
--env GOCACHE=/cache/go-build \
|
|
--env GOMODCACHE=/cache/go-mod \
|
|
--env GOLANGCI_LINT_CACHE=/cache/golangci-lint \
|
|
--volume "$ROOT:/src" \
|
|
--volume "$cache:/cache" \
|
|
--workdir /src \
|
|
"$image" \
|
|
golangci-lint run "$@" ./...
|
|
}
|
|
|
|
main() {
|
|
cd "$ROOT"
|
|
|
|
image="$(lint_image)"
|
|
if [ -z "$image" ]; then
|
|
echo "lint: no lint stage found in $DOCKERFILE" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$(installed_version)" = "$(pinned_version)" ]; then
|
|
exec golangci-lint run "$@" ./...
|
|
fi
|
|
|
|
run_in_docker "$image" "$@"
|
|
}
|
|
|
|
main "$@"
|