Files
rgoue/script/lint
sneak 20cfb47912 build: define the lint stage name once so the two flags cannot diverge
The previous commit claimed --target and --no-cache-filter "validate each
other's magic string". They do not. --target validates only its own
argument; a typo confined to --no-cache-filter left the build green and
linting nothing:

    docker build --target lint --no-cache-filter=lnit ...
    #10 [lint 2/2] RUN golangci-lint run ... CACHED   exit 0

Three of the four edit paths were caught and one was not, so the original
false green survived in the narrow case.

The duplication was the defect: the stage name appeared twice on one
command line and nothing tied the copies together. Correcting only the
prose would have left the hazard live and merely warned about, so the name
is now written once, as `stage=lint`, and passed to both flags. Divergence
is unrepresentable rather than documented — there is a single name to get
wrong, and --target rejects it loudly when it is not a stage in
Dockerfile.lint, which now covers the filter too because it is the same
string.

The comments in script/lint and Dockerfile.lint and the TODO.md entry drop
the false "validate each other" claim and state the real property, along
with the residual hazard that is genuinely unguarded: --target checks that
the name exists, not that it names the stage which actually runs
golangci-lint, and it stops the build there, so relocating the lint step
or appending a stage after it would go unnoticed.
2026-08-10 13:11:42 +00:00

52 lines
2.3 KiB
Bash
Executable File

#!/bin/sh
# script/lint: run the linter. golangci-lint is never installed locally:
# it runs via docker only, one way, everywhere — script/lint builds
# Dockerfile.lint, which COPYs the repo into the pinned golangci-lint
# image and lints as a build step. This works even when the docker daemon
# is remote and bind mounts are impossible.
#
# --no-cache-filter forces the lint stage to re-execute every run, so an
# unchanged tree is still actually linted; the deps stage keeps its cache,
# so the module download is not repeated. Both flags below must stay: do
# not "simplify" either one away.
#
# The stage name is written ONCE, in $stage, and passed to both --target
# and --no-cache-filter, so the two flags cannot come to name different
# stages. That is the whole point of the variable. BuildKit silently
# ignores --no-cache-filter when no stage matches its argument: a filter
# naming a stage that does not exist is a no-op, the lint layer is served
# from cache, and script/lint reports green having linted nothing — the
# false green this setup exists to prevent. --target, by contrast, fails
# loudly on a name that is not in the file. With a single shared name, a
# typo or a stale rename therefore becomes a hard error instead of a
# silent skip, because the one name reaches both flags.
#
# What the tooling does NOT check, and is left to whoever edits this:
# $stage must name the stage in Dockerfile.lint that actually runs
# golangci-lint. --target verifies that the name exists, not that it is
# the right stage, and it stops the build at that stage — so moving the
# lint step into a different stage, or adding a stage after this one,
# would not be caught here. Keep this file and Dockerfile.lint in sync.
#
# --output=type=cacheonly skips the image export. Nothing consumes the
# image — the deliverable of this build is an exit code — and exporting it
# costs seconds per run and leaves a dangling image behind every time. The
# lint stage still executes and a lint failure still exits non-zero.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Must match the stage name in Dockerfile.lint.
stage=lint
main() {
cd "$ROOT"
docker build \
--target "$stage" \
--no-cache-filter="$stage" \
--output=type=cacheonly \
-f Dockerfile.lint .
}
main "$@"