All checks were successful
check / check (pull_request) Successful in 2m58s
Every lint run now happens inside its own container, invoked through script/lint, and linting is a build step rather than a container command: a successful build of the new root Dockerfile.lint IS a clean lint. That shape also works where the docker daemon is remote and bind mounts are impossible. Its FROM line -- golangci/golangci-lint:v2.12.2, pinned by digest -- is now the only pin of the linter version in this repo. A container per run has its own lint cache and its own golangci-lint lock, both discarded with it, so neither cross-worktree contamination nor lock contention exists any more. The machinery that defended against them is therefore gone: the per-worktree cache directories, the lock-retry loop, and script/lint-audit, which existed to catch findings replayed from a cache that no longer exists. So is the host lint path in its entirety -- the native escape hatch, its version detection, and VAULTIK_LINT_IN_CONTAINER in both script/lint and the Dockerfile. Nothing lints on the host, at any version. A cached build lints nothing, so the CHECK_EPOCH mechanism the product Dockerfile already used is what makes a green mean something: ARG CHECK_EPOCH with no default, placed below the module layers so dependency caching survives, a `RUN [ -n "$CHECK_EPOCH" ] || exit 1` guard so a build that withholds the arg fails instead of replaying, and the value expanded into the lint command itself. script/lint computes `epoch="$(date +%s%N)$$"` as a bare assignment on its own line, because inline in the argument a failing substitution does not abort under `set -eu` and yields a constant empty epoch -- which is exactly the false green being prevented. The product Dockerfile loses its lint stage rather than gaining a second linter pin. That stage ran `make lint`, which is now `docker build`: docker-in-docker inside a BuildKit step with no daemon. Calling golangci-lint directly there instead would have meant two independently bumpable digests for one tool. `make fmt-check` moves beside `make test` in the builder stage, and script/cibuild now builds Dockerfile.lint and then Dockerfile, each with its own fresh epoch, failing on either. Consequence, stated in comments rather than left to be discovered: script/docker builds the product image only and no longer lints; script/check and script/cibuild are the gates. `golangci-lint config verify` runs as its own epoch-keyed layer, above the lint. It is not belt-and-braces: `golangci-lint run` rejects a config it cannot PARSE but silently IGNORES an unknown top-level KEY. Renaming .golangci.yml's `linters:` to `linterz:` -- one character -- discards `default: all`, the disable list and every threshold, leaves only the small default linter set running, and exits 0 reporting `0 issues.` on a tree the real config fails with an lll finding, in a run whose lint layer demonstrably executed. That is a set-but- ineffective config falling back to defaults instead of failing loudly, sitting in the gate's own configuration. `config verify` catches it and does so with the network genuinely off at this pin: under `docker run --network none` against the pinned digest it exits 0 on this repo's config and exits 3 on the `linterz:` variant. It is keyed on CHECK_EPOCH like the lint itself, because a cached validation validates nothing. script/lint-fix is kept, reimplemented as a bind-mounted docker run against the image parsed out of Dockerfile.lint -- a build step cannot write fixes back to the worktree -- and its header states outright that it is a developer convenience, never a gate, and needs a local daemon. cmd/vaultik/lintdocker_test.go parses both Dockerfiles and both scripts and fails if any part of the mechanism is dropped: the digest pin, the defaultless ARG below `go mod download`, the emptiness guard, the expansion of the epoch into each check command, the bare per-invocation epoch assignment in both scripts, cibuild building both files, the config verification running before the lint, and -- structurally, not by searching for one retired variable name -- that no script invokes golangci-lint except through docker. Every one of those losses is silent: the build still exits 0 and nothing is checked, which is why they are asserted rather than trusted. The scanner behind the last of those has its own test, because a structural check that goes blind passes on every tree, including a broken one. script/lint takes no arguments now, and says so instead of dropping them: a build step has no command line to pass linter flags to.
109 lines
3.9 KiB
Bash
Executable File
109 lines
3.9 KiB
Bash
Executable File
#!/bin/sh
|
|
# script/lint: run the linter.
|
|
#
|
|
# The linter runs inside the image built by Dockerfile.lint, and it runs
|
|
# there as a BUILD STEP: a successful build of that file IS a clean
|
|
# lint. Nothing lints on the host, at any version, ever. That FROM line
|
|
# is the single source of truth for the linter version in this repo, so
|
|
# a local run and a CI run of the same tree cannot disagree.
|
|
#
|
|
# One container per run means one lint cache and one golangci-lint lock
|
|
# per run, both private to that run and thrown away with it. That is
|
|
# what makes concurrent runs on a shared host safe, and it is why this
|
|
# script no longer carries per-worktree cache directories, a lock-retry
|
|
# loop, or an output audit: there is no shared state left for them to
|
|
# defend (issue https://git.eeqj.de/sneak/vaultik/issues/113).
|
|
#
|
|
# To watch the linter execute, set BUILDKIT_PROGRESS=plain, which docker
|
|
# honours directly:
|
|
#
|
|
# BUILDKIT_PROGRESS=plain script/lint
|
|
#
|
|
# The check layers -- `golangci-lint config verify` and then
|
|
# `golangci-lint run` -- must appear as executing rather than CACHED on
|
|
# every run; see the CHECK_EPOCH comment in Dockerfile.lint.
|
|
set -eu
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
DOCKERFILE="$ROOT/Dockerfile.lint"
|
|
|
|
require_docker() {
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
cat >&2 <<EOF
|
|
lint: docker is required to run the pinned linter.
|
|
|
|
lint image declared by: $DOCKERFILE
|
|
|
|
Install docker. Linting with any other golangci-lint is not supported:
|
|
it is what lets a local run pass while CI fails. A golangci-lint on
|
|
PATH is never used, whatever its version.
|
|
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.
|
|
|
|
lint image declared by: $DOCKERFILE
|
|
|
|
Start the daemon (and check DOCKER_HOST / your group membership). This
|
|
script will not fall back to a different linter version or to an
|
|
unpinned binary on PATH.
|
|
EOF
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
usage() {
|
|
cat >&2 <<EOF
|
|
usage: $(basename "$0")
|
|
|
|
script/lint takes no arguments. The linter runs as a build step, so
|
|
there is no command line to pass flags to; anything accepted here would
|
|
have to be silently dropped. To apply autofixes, use script/lint-fix,
|
|
which runs the same pinned image as a container for exactly this
|
|
reason.
|
|
EOF
|
|
exit 2
|
|
}
|
|
|
|
main() {
|
|
[ "$#" -eq 0 ] || usage
|
|
|
|
cd "$ROOT"
|
|
require_docker
|
|
|
|
# A fresh epoch per invocation is what forces the check layers to
|
|
# execute; the layers above the ARG in Dockerfile.lint still cache,
|
|
# so a run is not cold. The value must be unique per invocation, not
|
|
# per second: `date +%s` is second-granular, so two concurrent
|
|
# invocations in the same second would get identical epochs and the
|
|
# later one could be served from cache -- the false green in
|
|
# miniature. `%N` alone does not fix it either, because busybox
|
|
# silently drops %N, exits 0, and hands back second granularity with
|
|
# no warning. `$$` is what makes this correct regardless, since
|
|
# concurrent invocations have different pids.
|
|
#
|
|
# Assign it on its own line rather than inline in the argument.
|
|
# Under `set -eu` a command substitution that fails inside an
|
|
# argument does NOT abort the script: CHECK_EPOCH would become an
|
|
# empty string, an empty string is a constant, and a constant epoch
|
|
# is exactly the cached-lint false green this guards against. As a
|
|
# bare assignment, `set -e` catches a failing `date` and no build
|
|
# starts.
|
|
epoch="$(date +%s%N)$$"
|
|
|
|
# cacheonly: the lint verdict is the build's exit status, and the
|
|
# image it would otherwise produce is never run. Exporting it costs
|
|
# most of the wall time of a warm run and leaves a dangling image
|
|
# behind on every invocation, on a host that may be running many.
|
|
docker build \
|
|
--output=type=cacheonly \
|
|
--build-arg CHECK_EPOCH="$epoch" \
|
|
-f "$DOCKERFILE" \
|
|
"$ROOT"
|
|
}
|
|
|
|
main "$@"
|