`--no-cache-filter=lint` is silently ignored by BuildKit when no stage matches the name, so the entire anti-false-green mechanism hung on one unvalidated magic string: renaming or mistyping the `lint` stage would have left the lint layer served from cache and `script/lint` reporting green having linted nothing. Reproduced here — with the filter pointed at a nonexistent stage and no `--target`, an unchanged tree built with `RUN golangci-lint run ... CACHED` and exited 0. `--target lint` closes it: a stage name that does not exist now fails loudly (`target stage "nosuchstage" could not be found`, exit 1) instead of passing. The two flags name the same stage from the same string and validate each other; both the script and the stage definition in Dockerfile.lint carry a comment saying they must be kept in sync. `--output=type=cacheonly` drops the image export. Nothing consumes the image — the deliverable of this build is an exit code — and the export cost seconds per run and left one dangling image behind every time, on a host where pruning is prohibited. The lint stage still executes and a lint failure still exits non-zero, both verified rather than assumed. TODO.md: the 2026-08-07 entry's claim that the repo has no linter pin and lints on the host is marked superseded in place rather than rewritten; the narrowed scaffold exemption now names `.dockerignore` alongside `Dockerfile.lint` and `script/lint`; and the specific wall-clock timings are replaced by the durable property they were evidence for, since they vary per host and per run.
35 lines
1.5 KiB
Docker
35 lines
1.5 KiB
Docker
# Lint-only image: used by script/lint on machines where the docker
|
|
# daemon is remote (no bind mounts possible) — the repo is COPYed into
|
|
# the build context and golangci-lint runs as a build step, so a
|
|
# successful build means a clean lint.
|
|
#
|
|
# Two stages on purpose. script/lint builds with --no-cache-filter=lint so
|
|
# that the lint stage re-executes on every run, including on an unchanged
|
|
# tree (caching of the lint result is explicitly waived: a cached build
|
|
# lints nothing). Keeping `go mod download` in a separate `deps` stage
|
|
# means busting the lint stage does not also re-fetch the module cache
|
|
# over the network every time.
|
|
|
|
# golangci/golangci-lint:v2.12.2 (Debian-based), 2026-08-07
|
|
FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 AS deps
|
|
|
|
WORKDIR /src
|
|
|
|
# Copy go mod files first for better layer caching
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
|
|
# Do not rename this stage without changing script/lint in the same commit:
|
|
# it passes both --target lint and --no-cache-filter=lint by this name.
|
|
FROM deps AS lint
|
|
|
|
# Copy source code
|
|
COPY . .
|
|
|
|
# No `golangci-lint config verify` step here, deliberately (sneak/homoicon
|
|
# has one). It resolves its JSON schema over a live, unpinned HTTPS call:
|
|
# an unpinned network input inside the one step whose whole purpose is a
|
|
# pinned, reproducible gate, and a schema-host outage would show up as a
|
|
# red build. `golangci-lint run` already fails on a malformed config.
|
|
RUN golangci-lint run --config .golangci.yml ./...
|