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.
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 "$@"
|