# 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 ./...