Isolate the lint cache and context-gate the native path (closes #99)
All checks were successful
check / check (push) Successful in 2m23s

Closes #80.

script/lint pointed GOLANGCI_LINT_CACHE at a path shared by every
worktree of this repo. Two worktrees have identical Go file contents, so
their cache keys collided and one tree's stored findings replayed for
another, paths included - observed as 231 findings all citing another
session's worktree, with no parallel-runner message to signal it. The
failure is symmetric and only one direction is loud: a clean tree failed
by a dirty sibling gets investigated, a dirty tree passed by a clean
sibling does not.

The cache is now keyed per worktree on a digest of $ROOT, and remains
persistent. Independently of that, script/lint-audit inspects every run's
output and fails the run if any finding cites a path outside the tree
being linted. That guard is the load-bearing part: it converts a silent
unearned green into a hard error regardless of how the cache is keyed. It
is deliberately built so it can never certify a pass, only reject, so it
cannot itself become a gate that reports green.

The native path was gated on version equality alone, which admitted a
locally installed matching binary and bypassed the digest pin. It now
requires VAULTIK_LINT_IN_CONTAINER=1, set only by the Dockerfile lint
stage, in addition to version equality. /.dockerenv was rejected as the
signal because dockerd creates it for `docker run` but not reliably
during a BuildKit `docker build`, which is the case the exception exists
for. A version mismatch inside the container is now a hard error rather
than a fall-through.

This mattered more than the issue supposed: on this host a matching
golangci-lint exists on PATH, so script/lint was taking the native path
and linting against the global cache without ever running the pinned
image. That is the likely root of the observed contamination, and it is
closed here rather than mitigated.

The parallel-runner error is retried rather than reported. It is not a
lint result, and surfacing it as a non-zero exit is indistinguishable to
a caller from real findings; exhausted retries fail saying the tree was
never analysed. Note that a private cache alone does not remove lock
contention - measured with two concurrent runs using separate cache
directories.

script/bootstrap no longer reports success on a machine that cannot run
the gate: docker is now required by lint, check and precommit, so a
missing binary or unreachable daemon is a hard failure naming what will
not work.
This commit was merged in pull request #102.
This commit is contained in:
2026-08-09 17:15:07 +02:00
parent c51f693527
commit b6e4a218a3
6 changed files with 461 additions and 36 deletions

View File

@@ -48,6 +48,52 @@ missing() {
! command -v "$1" >/dev/null 2>&1
}
# Docker is a hard requirement, not a nice-to-have: script/lint runs the
# digest-pinned golangci-lint image from the Dockerfile's lint stage, and
# script/check and script/precommit both run script/lint. A bootstrap
# that prints "bootstrap complete" on a machine where `make check` cannot
# run is a false success, so this fails instead.
#
# Installing docker from here was considered and rejected: it needs root,
# a running daemon, and on macOS a GUI cask, so an attempt would itself
# fail in the common case - trading one false success for a second
# failure mode. Naming exactly what breaks is more useful.
# Prints the problem and returns 0 when docker cannot be used; returns
# 1 (and prints nothing) when it can.
docker_problem() {
if missing docker; then
echo "docker is not installed"
return 0
fi
if ! docker info >/dev/null 2>&1; then
echo "the docker daemon is not reachable"
return 0
fi
return 1
}
require_docker() {
reason="$(docker_problem)" || return 0
cat >&2 <<EOF
bootstrap: FAILED - $reason.
Docker is required to develop this repo. Without it these do not work:
script/lint runs the digest-pinned golangci-lint image declared
by the Dockerfile's lint stage, which is the single
source of truth for the linter version
script/check runs script/lint
script/precommit runs script/check, so commits are blocked by the
pre-commit hook installed by script/setup
script/cibuild builds the Dockerfile, which is what CI runs
Install docker (and start the daemon, checking DOCKER_HOST and your
group membership), then re-run script/bootstrap. golangci-lint on PATH
is deliberately not a substitute: script/lint will not use it.
EOF
exit 1
}
main() {
cd "$ROOT"
@@ -61,17 +107,19 @@ main() {
# golangci-lint is deliberately NOT installed: script/lint runs the
# digest-pinned golangci-lint image from the Dockerfile's lint stage,
# so whatever a package manager happens to ship would only be a
# shadow of the pinned version that could drift from CI.
if missing docker; then
echo "bootstrap: docker not found; script/lint needs it to run" >&2
echo "bootstrap: the pinned linter (see the Dockerfile lint stage)" >&2
fi
# shadow of the pinned version that could drift from CI. script/lint
# will not use a PATH binary on a host at any version, so installing
# one here would buy nothing.
# sqlite3 CLI: the test suite shells out to it (VACUUM).
if missing sqlite3; then pkg_install sqlite sqlite3 sqlite sqlite; fi
go mod download
# Last, so that everything installable is installed before the one
# thing this script cannot install decides the outcome.
require_docker
echo "bootstrap complete"
}