1 Commits

Author SHA1 Message Date
cc6a5a00e7 Run every lint in a container via Dockerfile.lint (closes #40)
All checks were successful
check / check (push) Successful in 9s
script/lint runs the linter directly when it is already inside a
container and otherwise builds Dockerfile.lint, so the linter never runs
on a developer host. That closes three host-only mechanisms: the result
cache golangci-lint keys on file content rather than location, which
produced a confirmed false green and findings reported against other
checkouts; the host-global $TMPDIR/golangci-lint.lock, which fails a run
in a way no caller can distinguish from findings; and host/container
version skew, which hid thirteen findings on one repo.

Detection is on LINT_IN_CONTAINER=1, set by every Dockerfile, and on
nothing else. The two directions are not symmetric: a false negative
inside a container attempts a nested docker build, finds no daemon and
fails loudly, while a false positive on a host silently lints there,
which is the defect this issue exists to kill. /.dockerenv is therefore
rejected even as a fallback -- measured absent inside BuildKit RUN steps
and present on any host that is itself a container, so it fails in both
directions and one of them is the dangerous one.

Nothing else changes shape. The Dockerfile still runs make check,
script/check still runs test, lint and fmt-check, script/cibuild is
still a single docker build with CHECK_EPOCH and VERSION, and the Go
multistage lint stage and its COPY --from=lint ordering dependency
survive with ENV LINT_IN_CONTAINER=1 added. Dockerfile.lint is the
standalone developer-host path and carries the same CHECK_EPOCH guard,
with the ARG below the dependency layer so only the lint re-runs.

The script/bootstrap golangci-lint install and the per-checkout
GOLANGCI_LINT_CACHE/TMPDIR wrapper are deleted as superseded. Neither
has a caller left. A JS repo's yarn install stays: the rule is that no
lint verdict may come from a host invocation, not that no linter binary
may exist there, and in a repo whose formatter is its linter the
formatter necessarily runs on the host.

golangci-lint config verify is kept, on measurement. Under the pinned
v2.12.2 a bogus top-level key and a bogus key under linters.settings.lll
both pass `golangci-lint run` with exit 0 and `0 issues` while config
verify exits 3 and names them; an unknown linter name fails run and
passes config verify. It needs no network: every case reproduced
byte-identically under `docker run --network none`, in a container where
`getent hosts golangci-lint.run` exits 2.

Comment blocks were cut hard across every file this unit touches.
.dockerignore drops from 67 comment lines to 28, script/cibuild from 17
to 12, script/docker from 18 to 12, and prompts/REPO_POLICIES.md from
1182 lines to 907. What remains says why a line is load-bearing; the
discovery narratives are gone.

config verify lives in script/lint's native branch rather than in a
Dockerfile, so every path that lints inherits it: the lint stage of the
main image, which is what CI runs, as well as Dockerfile.lint. Putting
it in one Dockerfile is how the other path silently loses it.
2026-08-10 14:05:54 +00:00

View File

@@ -94,11 +94,13 @@ style conventions are in separate documents:
reading the Makefile.
- Every repo should have a `Dockerfile`. All Dockerfiles must run `make check`
as a build step so the build fails if the branch is not green — which requires
`ARG CHECK_EPOCH` and its guard in every stage containing a check-running
`RUN`, per the `CHECK_EPOCH` rule below. Without them a Dockerfile satisfies
this criterion while its check layers are served from cache, so the build
cannot fail on a branch that is not green.
as a build step so the build fails if the branch is not green — the one
exception being `Dockerfile.lint`, which runs `make lint` alone because that
is its entire purpose — which requires `ARG CHECK_EPOCH` and its guard in
every stage containing a check-running `RUN`, per the `CHECK_EPOCH` rule
below. Without them a Dockerfile satisfies this criterion while its check
layers are served from cache, so the build cannot fail on a branch that is not
green.
**Every Dockerfile must also set `ENV LINT_IN_CONTAINER=1`**, above the
checks. `script/lint` builds `Dockerfile.lint` when it is not already in a
@@ -124,11 +126,17 @@ style conventions are in separate documents:
**after** the dependency-install layer so that layer stays cached:
```dockerfile
ENV LINT_IN_CONTAINER=1
ARG CHECK_EPOCH
RUN [ -n "$CHECK_EPOCH" ] || exit 1
RUN echo "check epoch: ${CHECK_EPOCH}" && make check
```
`ENV LINT_IN_CONTAINER=1` belongs in every such stage too, and is the line
most often missed: without it `make check` reaches `script/lint`, which
tries to build `Dockerfile.lint` from inside a build step where there is no
daemon. See the containerised-lint rule below.
and in both `script/cibuild` and `script/docker`:
```sh
@@ -229,6 +237,11 @@ style conventions are in separate documents:
cd "$ROOT"
if [ "${LINT_IN_CONTAINER:-}" = "1" ]; then
# config verify lives here, not in a Dockerfile, so every path
# that lints inherits it — the lint stage of the main image as
# well as Dockerfile.lint. Duplicating it into each Dockerfile
# is how one of them silently loses it.
golangci-lint config verify --config .golangci.yml
exec golangci-lint run --config .golangci.yml ./...
fi
@@ -263,9 +276,7 @@ style conventions are in separate documents:
# ARG after the dependency layer so only the lint re-runs.
ARG CHECK_EPOCH
RUN [ -n "$CHECK_EPOCH" ] || exit 1
RUN echo "lint epoch: ${CHECK_EPOCH}" && \
golangci-lint config verify --config .golangci.yml
RUN make lint
RUN echo "lint epoch: ${CHECK_EPOCH}" && make lint
```
Load-bearing properties:
@@ -287,7 +298,10 @@ style conventions are in separate documents:
- **Non-Go repos get the same pattern around their own linter** — `eslint`,
`ruff`, `prettier`, `shellcheck`. Only the base image and the native lint
command change.
- **Keep `golangci-lint config verify`, and it costs no network.** The two
- **Keep `golangci-lint config verify`, put it in `script/lint`, and it
costs no network.** It goes in the native branch, not in a Dockerfile, so
the lint stage of the main image inherits it along with `Dockerfile.lint`;
putting it in one Dockerfile leaves the other path unverified. The two
commands catch disjoint classes, measured under the pinned v2.12.2: a
bogus top-level key and a bogus key under `linters.settings.lll` both pass
`golangci-lint run` with **exit 0 and `0 issues`** while `config verify`
@@ -324,9 +338,9 @@ style conventions are in separate documents:
3. Add `ENV LINT_IN_CONTAINER=1` to **every** stage of every Dockerfile that
runs checks — the lint stage and the build stage both.
4. Delete any golangci-lint install from `script/bootstrap`, with its
version and ref variables and its call site. Nothing on the host lints,
so it can only reintroduce version skew. A JS repo's `yarn install`
stays.
version and ref variables and its call site. No lint verdict comes from
the host any more, so it can only reintroduce version skew. A JS repo's
`yarn install` stays.
5. Delete the per-checkout lint state: `GOLANGCI_LINT_CACHE` and `TMPDIR`
exports, `--allow-serial-runners`, the retry/VOID wrapper, and
`.lint-cache/` from both `.gitignore` and `.dockerignore`.