All checks were successful
check / check (pull_request) Successful in 3m11s
Closes #80. script/lint decided whether it could skip the pinned image by asking what version was on PATH rather than where it was running, and cache isolation is part of that same question. Both issues are that one defect. The cache was one directory per repo, shared by every worktree on the host. Two checkouts of this repo have identical Go file contents, so their cache keys collide and golangci-lint replays the stored analysis, including the paths recorded when it was produced. The loud direction of that failure - a clean tree failed by a dirty sibling - is the harmless one. The silent direction, a dirty tree passed by a clean sibling, is another way for a gate here to report a green it did not earn. The cache is now keyed on a digest of the worktree path, so a collision is not possible, and it stays persistent per worktree: a warm run is still seconds. Each cache records the worktree it belongs to and is collected when that worktree is gone, so throwaway worktrees do not accumulate caches; the tree lives under XDG_CACHE_HOME and is disposable by definition. Collection makes the tree writable first, since the Go module cache inside it is deliberately read-only, and never lets a failure to tidy up fail the lint: the first cut did exactly that, and a gate failing for a housekeeping reason is its own bug. script/lint-audit is the backstop, and runs on every lint: it rejects output citing any file that is not in the tree being linted, so a result built out of another checkout's analysis is a hard error instead of a silent pass. It runs on clean output too, because that is the case nobody investigates. It never certifies that a run passed - it does not look at whether there were findings - so it cannot itself become a gate that reports a green. golangci-lint's "parallel golangci-lint is running" refusal is now a bounded retry rather than a verdict. It is not a lint result, and exiting non-zero on it is indistinguishable to a caller from real findings; issue #88 measured that a private cache does not remove the contention. Exhausting the retries fails with a message that says the tree was never analysed. The native path now requires VAULTIK_LINT_IN_CONTAINER=1, which only the Dockerfile's lint stage sets, in addition to matching the pin. A developer's locally installed 2.12.2 is a different build reached by a different code path and no longer bypasses the digest pin. /.dockerenv was considered and rejected as the signal: dockerd creates it for `docker run`, but it is not reliably present during a BuildKit `docker build`, which is exactly the case the exception exists for. Inside the container a version mismatch is now a hard error rather than a fall-through, since there is no daemon there to fall through to. Version detection uses `golangci-lint version --short`, the interface meant for it, keeping the banner scrape only as a fallback. script/bootstrap no longer prints "bootstrap complete" on a machine that cannot run the gate. Docker missing, or present with an unreachable daemon, is a hard failure naming exactly what breaks. Installing docker from bootstrap was rejected: it needs root, a daemon, and on macOS a GUI cask, so the attempt would itself fail in the common case and trade one false success for a second failure mode. TODO.md's claim that `make check` became "as trustworthy as script/cibuild" is corrected to what README.md already said: only the lint leg is equivalent, while tests and gofmt still run against the host toolchain. README.md's requirements section gains docker and sqlite3. Verified by reproduction, not inspection: two concurrent lints from two worktrees of differing cleanliness each reported only their own findings with no lock error; a real run made to report paths outside its tree exits 1; a matching linter shimmed onto PATH is never invoked while the pinned image runs; a PATH without docker makes bootstrap fail. script/ cibuild exits 0 with the lint layer executing in the pinned image, which is what proves the in-container path still works.
114 lines
3.9 KiB
Bash
Executable File
114 lines
3.9 KiB
Bash
Executable File
#!/bin/sh
|
|
# script/lint-audit: audit a captured golangci-lint run for output that
|
|
# cannot describe this tree. Called by script/lint on every run; usable
|
|
# on its own against any saved lint output.
|
|
#
|
|
# script/lint-audit <capture-file>
|
|
#
|
|
# Exits 0 when every finding cites a file in this tree, 1 when any does
|
|
# not. It NEVER certifies that a lint run passed - it has no idea
|
|
# whether the run found issues, and does not look. It only rejects
|
|
# output that is impossible for this tree, which is a different and much
|
|
# weaker claim. Do not use it as a gate; use script/lint.
|
|
#
|
|
# Why this exists (issue #99): golangci-lint caches analysis results,
|
|
# and a cache shared between two checkouts of this repo can serve one
|
|
# checkout's stored findings for another, file paths included. The
|
|
# failure is symmetric and only one direction is loud - a clean tree
|
|
# failed by a dirty sibling gets investigated, while a dirty tree passed
|
|
# by a clean sibling is silent. This turns the silent direction into a
|
|
# hard error, which is why it runs on clean output too.
|
|
#
|
|
# The primary fix is that script/lint now keys its cache on the worktree
|
|
# path so the collision cannot happen. This is the backstop, because a
|
|
# backstop that only runs when we already believe things are fine is
|
|
# worth more than one more assumption.
|
|
set -eu
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
|
|
# Where script/lint bind-mounts the tree inside the pinned image. A
|
|
# containerized run that prints absolute paths (`--path-mode abs`)
|
|
# prints them under this, so they are this tree's files under another
|
|
# name. Note the consequence, and why the cache key rather than this
|
|
# check is the real fix: two containerized runs of different checkouts
|
|
# both call themselves /src, so contamination between two container
|
|
# runs is not distinguishable by path alone.
|
|
CONTAINER_ROOT="/src"
|
|
|
|
usage() {
|
|
echo "usage: $(basename "$0") <capture-file>" >&2
|
|
exit 2
|
|
}
|
|
|
|
# Every path cited by a finding that is not a file in this tree.
|
|
#
|
|
# The linter runs with the tree root as its working directory, so a
|
|
# legitimate finding cites either a relative path that resolves inside
|
|
# the tree or an absolute path under the root. A path that escapes
|
|
# (absolute and elsewhere, or with a `..` component) or that names a
|
|
# file which is not here describes something this run did not analyse.
|
|
foreign_paths() {
|
|
capture="$1"
|
|
awk -F: '$1 ~ /\.go$/ && $2 ~ /^[0-9]+$/ { print $1 }' "$capture" |
|
|
sort -u |
|
|
while IFS= read -r path; do
|
|
case "$path" in
|
|
"$ROOT"/*)
|
|
path="${path#"$ROOT"/}"
|
|
;;
|
|
"$CONTAINER_ROOT"/*)
|
|
path="${path#"$CONTAINER_ROOT"/}"
|
|
;;
|
|
/*)
|
|
printf '%s\n' "$path"
|
|
continue
|
|
;;
|
|
../* | */../*)
|
|
printf '%s\n' "$path"
|
|
continue
|
|
;;
|
|
esac
|
|
if [ ! -e "$ROOT/$path" ]; then
|
|
printf '%s\n' "$path"
|
|
fi
|
|
done
|
|
}
|
|
|
|
main() {
|
|
[ "$#" -eq 1 ] || usage
|
|
capture="$1"
|
|
if [ ! -f "$capture" ]; then
|
|
echo "lint-audit: no such capture file: $capture" >&2
|
|
exit 2
|
|
fi
|
|
|
|
foreign="$(foreign_paths "$capture")"
|
|
if [ -z "$foreign" ]; then
|
|
exit 0
|
|
fi
|
|
|
|
cat >&2 <<EOF
|
|
|
|
lint: REJECTED - the linter reported findings for files that are not in
|
|
this tree, so its output does not describe the tree that was linted.
|
|
This result is void, whichever way it went: a pass here would be a pass
|
|
earned by analysing someone else's code.
|
|
|
|
tree: $ROOT
|
|
|
|
Paths reported that are not in this tree:
|
|
EOF
|
|
printf '%s\n' "$foreign" | sed -e 's/^/ /' >&2
|
|
cat >&2 <<EOF
|
|
|
|
This is the signature of analysis replayed from a cache belonging to
|
|
another checkout (issue #99). Clear this tree's lint cache and re-run:
|
|
|
|
rm -rf "\${XDG_CACHE_HOME:-\$HOME/.cache}/vaultik-lint"
|
|
EOF
|
|
exit 1
|
|
}
|
|
|
|
main "$@"
|