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.
This commit is contained in:
@@ -19,8 +19,13 @@ WORKDIR /src
|
|||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
# Do not rename this stage without changing script/lint in the same commit:
|
# This stage must stay the one that runs golangci-lint, and its name must
|
||||||
# it passes both --target lint and --no-cache-filter=lint by this name.
|
# match $stage in script/lint, which passes that single name to both
|
||||||
|
# --target and --no-cache-filter. Renaming here without updating script/lint
|
||||||
|
# fails the build loudly (--target rejects a name that is not in this file),
|
||||||
|
# so a mismatch cannot pass silently — but moving the lint step to another
|
||||||
|
# stage, or adding a stage after this one, would not be caught. Change the
|
||||||
|
# two files together.
|
||||||
FROM deps AS lint
|
FROM deps AS lint
|
||||||
|
|
||||||
# Copy source code
|
# Copy source code
|
||||||
|
|||||||
34
TODO.md
34
TODO.md
@@ -66,16 +66,30 @@ is finished.
|
|||||||
from cache; wall-clock durations vary per host and per run, so they are not
|
from cache; wall-clock durations vary per host and per run, so they are not
|
||||||
recorded here.
|
recorded here.
|
||||||
|
|
||||||
Hardened 2026-08-10 after review. `script/lint` now also passes
|
Hardened 2026-08-10 after review. `script/lint` now also passes `--target`
|
||||||
`--target lint` and `--output=type=cacheonly`. `--target` is what makes
|
and `--output=type=cacheonly`. `--target` is what makes `--no-cache-filter`
|
||||||
`--no-cache-filter=lint` trustworthy: BuildKit silently ignores the filter
|
trustworthy: BuildKit silently ignores the filter when no stage matches its
|
||||||
when no stage matches the name, so a rename or typo of the `lint` stage
|
argument, so a rename or typo of the `lint` stage would have left the lint
|
||||||
would have left the lint layer cached and `script/lint` green having linted
|
layer cached and `script/lint` green having linted nothing — the same false
|
||||||
nothing — the same false green in a new place. `--target` fails loudly on a
|
green in a new place. `--target` fails loudly on a name that is not in the
|
||||||
name that does not exist, so the two flags validate each other and must be
|
file. `--output=type=cacheonly` skips the image export: nothing consumes the
|
||||||
kept in sync. `--output=type=cacheonly` skips the image export: nothing
|
image (the deliverable is an exit code), and exporting it cost seconds per
|
||||||
consumes the image (the deliverable is an exit code), and exporting it cost
|
run and left a dangling image behind each time.
|
||||||
seconds per run and left a dangling image behind each time.
|
|
||||||
|
Corrected 2026-08-10 after a second review, which was right to reject the
|
||||||
|
claim first made here that the two flags "validate each other". They did
|
||||||
|
not: `--target` validates only its own argument, so a typo confined to
|
||||||
|
`--no-cache-filter` still built `CACHED` at exit 0 — the original defect,
|
||||||
|
surviving in the narrow case. The duplicated stage name was the defect, so
|
||||||
|
it is now written once, as `stage=lint` in `script/lint`, and passed to both
|
||||||
|
flags. The true property is that there is only one name to get wrong, and
|
||||||
|
`--target` rejects it loudly if it is not a stage in `Dockerfile.lint`, so a
|
||||||
|
typo or a stale rename is a hard error rather than a silent skip. What
|
||||||
|
remains on the editor, and is not checked by anything: `$stage` must name
|
||||||
|
the stage that actually runs `golangci-lint`. `--target` verifies the name
|
||||||
|
exists, not that it is the right stage, and it stops the build there — so
|
||||||
|
moving the lint step to another stage, or adding a stage after it, would go
|
||||||
|
unnoticed.
|
||||||
|
|
||||||
- 2026-08-09 `TestAutoSaveOnSignalRacesTurnLoop` de-flaked at the cause
|
- 2026-08-09 `TestAutoSaveOnSignalRacesTurnLoop` de-flaked at the cause
|
||||||
(`fix/autosave-turn-budget-36`, closes #36). The failure text was captured
|
(`fix/autosave-turn-budget-36`, closes #36). The failure text was captured
|
||||||
|
|||||||
39
script/lint
39
script/lint
@@ -5,18 +5,28 @@
|
|||||||
# image and lints as a build step. This works even when the docker daemon
|
# image and lints as a build step. This works even when the docker daemon
|
||||||
# is remote and bind mounts are impossible.
|
# is remote and bind mounts are impossible.
|
||||||
#
|
#
|
||||||
# --no-cache-filter=lint forces the lint stage to re-execute every run, so
|
# --no-cache-filter forces the lint stage to re-execute every run, so an
|
||||||
# an unchanged tree is still actually linted; the deps stage keeps its
|
# unchanged tree is still actually linted; the deps stage keeps its cache,
|
||||||
# cache, so the module download is not repeated.
|
# so the module download is not repeated. Both flags below must stay: do
|
||||||
|
# not "simplify" either one away.
|
||||||
#
|
#
|
||||||
# --target lint and --no-cache-filter=lint must BOTH be present, and both
|
# The stage name is written ONCE, in $stage, and passed to both --target
|
||||||
# must keep naming the stage that Dockerfile.lint calls `lint`. Do not
|
# and --no-cache-filter, so the two flags cannot come to name different
|
||||||
# "simplify" either one away. BuildKit silently ignores --no-cache-filter
|
# stages. That is the whole point of the variable. BuildKit silently
|
||||||
# when no stage matches the name: rename or typo the stage and the filter
|
# ignores --no-cache-filter when no stage matches its argument: a filter
|
||||||
# becomes a no-op, the lint layer is served from cache, and script/lint
|
# naming a stage that does not exist is a no-op, the lint layer is served
|
||||||
# reports green having linted nothing — the exact false green this whole
|
# from cache, and script/lint reports green having linted nothing — the
|
||||||
# setup exists to prevent. --target fails loudly on a name that does not
|
# false green this setup exists to prevent. --target, by contrast, fails
|
||||||
# exist, so the two flags validate each other's magic string.
|
# 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
|
# --output=type=cacheonly skips the image export. Nothing consumes the
|
||||||
# image — the deliverable of this build is an exit code — and exporting it
|
# image — the deliverable of this build is an exit code — and exporting it
|
||||||
@@ -26,11 +36,14 @@ set -eu
|
|||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
# Must match the stage name in Dockerfile.lint.
|
||||||
|
stage=lint
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
docker build \
|
docker build \
|
||||||
--target lint \
|
--target "$stage" \
|
||||||
--no-cache-filter=lint \
|
--no-cache-filter="$stage" \
|
||||||
--output=type=cacheonly \
|
--output=type=cacheonly \
|
||||||
-f Dockerfile.lint .
|
-f Dockerfile.lint .
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user