Run all linting in Docker via Dockerfile.lint (closes #109)
All checks were successful
check / check (push) Successful in 2m45s

golangci-lint no longer runs on the host. script/lint builds
Dockerfile.lint, which copies the repo into the digest-pinned linter
image, so the container holds only this repo and the cross-worktree
cache contamination of #106 becomes structurally impossible rather than
filtered after the fact. Five workers hit that contamination in one
evening, in both directions.

Three properties are load-bearing. --no-cache-filter=lint forces the
lint stage to re-execute while deps keeps its cache, because a cached
build lints nothing in 0.27s and exits 0. script/lint does not trust
that flag, since Docker silently ignores an unmatched stage name: it
asserts golangci-lint's own summary line appears, so no summary means no
lint whatever the exit code says. And both lint steps run
--network=none, which enforces rather than assumes that config verify
does not fetch its schema — verify is kept, because golangci-lint run
silently ignores unrecognized config keys and it is the only thing that
catches a typo that disables a setting.

Independently reviewed four times. Three passed the behaviour; the
remaining rounds were a README merge against #151, whose premise was
that the file contains no false statements. Six statements this change
falsified were found and corrected across those rounds — the last
reviewer re-derived every countable claim against the tree rather than
reading for plausibility, and found no seventh.

Supersedes #106.
This commit was merged in pull request #165.
This commit is contained in:
2026-08-18 01:07:16 +02:00
parent 41ff16a817
commit 992b3c68f5
5 changed files with 157 additions and 83 deletions

View File

@@ -1,12 +1,55 @@
#!/bin/sh
# script/lint: run the linter.
# script/lint: run the linter. golangci-lint is never installed locally: it
# runs via docker only, one way, everywhere — script/lint builds
# Dockerfile.lint, which COPYs the repo into the pinned golangci-lint image
# and lints as a build step. This works even when the docker daemon is remote
# and bind mounts are impossible, and it removes the host linter's shared
# cache, which has attributed other checkouts' findings to this one.
#
# --no-cache-filter=lint forces the lint stage to re-execute on every run; a
# cached lint stage exits 0 in under a second having linted nothing. The deps
# stage keeps its cache, so module downloads are not repeated.
# --progress=plain keeps the linter's own output visible on success, so a
# passing run shows the issue count rather than nothing.
# --output=type=cacheonly leaves no image behind to clean up.
#
# docker silently ignores --no-cache-filter for a stage name that does not
# match, so a rename or a typo would restore the cached false green with no
# warning and a fast exit 0. The flag is therefore not trusted: the build
# output is teed to a log and a run is only a pass if golangci-lint's own
# summary line ("N issues." / "N issues:") is in it. No summary, no lint,
# whatever the exit code says.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() {
cd "$ROOT"
golangci-lint run --config .golangci.yml ./...
log="$(mktemp -t webhooker-lint.XXXXXXXX)"
rcfile="$(mktemp -t webhooker-lint-rc.XXXXXXXX)"
trap 'rm -f "$log" "$rcfile"' EXIT INT TERM
# The pipeline's status is tee's, and POSIX sh has no pipefail, so the
# build's status travels via a file. Output still streams live.
{
docker build \
-f Dockerfile.lint \
--no-cache-filter=lint \
--progress=plain \
--output=type=cacheonly \
. 2>&1 && echo 0 >"$rcfile" || echo $? >"$rcfile"
} | tee "$log" >&2
rc="$(cat "$rcfile")"
[ "$rc" -eq 0 ] || exit "$rc"
if ! grep -qE '[0-9]+ issues[.:]' "$log"; then
echo "script/lint: golangci-lint printed no summary line; the linter" >&2
echo " did not run. Check that the stage named in --no-cache-filter" >&2
echo " still matches a stage in Dockerfile.lint." >&2
exit 1
fi
}
main "$@"