next -> main #43

Merged
clawbot merged 4 commits from next into main 2026-08-10 15:33:22 +02:00
3 changed files with 57 additions and 25 deletions
Showing only changes of commit 20cfb47912 - Show all commits

View File

@@ -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
View File

@@ -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

View File

@@ -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 .
} }