All checks were successful
check / check (pull_request) Successful in 4m0s
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. Two decisions taken deliberately and documented where they apply. `golangci-lint config verify` is omitted: it fetches its JSON schema over an unpinned live HTTPS call, which would make the gate depend on a remote resource outside this repo's hash-pinning discipline and turn an upstream outage or an egress-less runner into a red that is not a lint verdict. 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, and the absence of any host-lint escape hatch. 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. 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.
82 lines
4.0 KiB
Docker
82 lines
4.0 KiB
Docker
# Lint image.
|
|
#
|
|
# Every lint run in this repo happens inside this image, invoked through
|
|
# script/lint, and linting is a BUILD STEP rather than a container
|
|
# command: a successful build of this file IS a clean lint. That shape
|
|
# also works where the docker daemon is remote and bind mounts are
|
|
# impossible, which `docker run` against a mounted worktree does not.
|
|
#
|
|
# This FROM line is the single source of truth for the linter version in
|
|
# this repo. Nothing else pins golangci-lint: the product Dockerfile has
|
|
# no lint stage, deliberately, so there is no second digest to bump and
|
|
# no pair of pins that can drift apart. Bump the tag AND the digest here
|
|
# and nowhere else.
|
|
#
|
|
# Note for readers coming from REPO_POLICIES.md: that document still
|
|
# describes the older pattern, a lint stage inside the product
|
|
# Dockerfile wired up with `COPY --from=lint /src/go.sum /dev/null`.
|
|
# That pattern is superseded here by the owner's ruling recorded in
|
|
# https://git.eeqj.de/sneak/vaultik/issues/113 -- lint runs in its own
|
|
# image, per run, with its own cache and its own lock, which is what
|
|
# makes concurrent runs on one host safe. The policy text is org-wide
|
|
# and is being amended separately; this file is what this repo does.
|
|
#
|
|
# golangci/golangci-lint:v2.12.2, 2026-08-10
|
|
FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240
|
|
|
|
WORKDIR /src
|
|
|
|
# Copy the dependency manifests first so the module download layer stays
|
|
# cached until they change. Everything above the ARG below is cacheable
|
|
# on purpose; a cold module download on every lint would make the inner
|
|
# loop unusable and buys nothing, because it is not what the gate is
|
|
# asserting.
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
|
|
COPY . .
|
|
|
|
# `golangci-lint config verify` is deliberately NOT run here.
|
|
#
|
|
# It validates .golangci.yml against a JSON schema that it fetches over
|
|
# live HTTPS from an unpinned URL at run time. Running it would make the
|
|
# lint gate depend on a remote resource that is outside this repo's
|
|
# hash-pinning discipline, and would turn an upstream outage or an
|
|
# egress-less runner into a red that is not a lint verdict -- the exact
|
|
# false-red class this repo has spent several issues eliminating. The
|
|
# config is not unvalidated in practice either: `golangci-lint run`
|
|
# below rejects an unparseable or unknown-key config itself, at the
|
|
# version that is actually gating.
|
|
#
|
|
# If it is ever added, it must first be demonstrated to work with the
|
|
# network genuinely off (`docker run --network none`) at the pinned
|
|
# version, with that output recorded.
|
|
|
|
# Force the lint layer to execute on every invocation.
|
|
#
|
|
# CHECK_EPOCH must stay immediately above the RUNs below. Those layers
|
|
# are keyed on its value, so they are cache-eligible only for a value
|
|
# already built against this same tree; script/lint and script/cibuild
|
|
# each pass a fresh value on every invocation, which is what makes their
|
|
# green mean the linter really ran. Without it, `docker build -f
|
|
# Dockerfile.lint .` on an unchanged tree exits 0 in well under a second
|
|
# having linted nothing.
|
|
#
|
|
# The value is expanded into the lint command itself rather than left to
|
|
# a bare declaration, so the cache miss does not depend on BuildKit's
|
|
# unreferenced-ARG handling staying as it is. It also puts the epoch in
|
|
# the build log, where a reader can see the layer was keyed fresh.
|
|
#
|
|
# The guard is what makes a build that omits --build-arg fail instead of
|
|
# lie. An unset ARG is an empty string, and an empty string is a
|
|
# perfectly stable cache key: without the guard the first such build
|
|
# lints and every one after it on an unchanged tree replays this layer,
|
|
# executes nothing, and still exits 0. Failed steps are never cached, so
|
|
# the guard fails on EVERY invocation rather than once. Do not give
|
|
# CHECK_EPOCH a default value; a default would satisfy the guard with a
|
|
# constant and restore the hole.
|
|
ARG CHECK_EPOCH
|
|
RUN [ -n "$CHECK_EPOCH" ] || exit 1
|
|
RUN echo "check epoch: ${CHECK_EPOCH}" && \
|
|
golangci-lint run --config .golangci.yml ./...
|