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. reading the Makefile.
- Every repo should have a `Dockerfile`. All Dockerfiles must run `make check` - 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 as a build step so the build fails if the branch is not green — the one
`ARG CHECK_EPOCH` and its guard in every stage containing a check-running exception being `Dockerfile.lint`, which runs `make lint` alone because that
`RUN`, per the `CHECK_EPOCH` rule below. Without them a Dockerfile satisfies is its entire purpose — which requires `ARG CHECK_EPOCH` and its guard in
this criterion while its check layers are served from cache, so the build every stage containing a check-running `RUN`, per the `CHECK_EPOCH` rule
cannot fail on a branch that is not green. 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 **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 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: **after** the dependency-install layer so that layer stays cached:
```dockerfile ```dockerfile
ENV LINT_IN_CONTAINER=1
ARG CHECK_EPOCH ARG CHECK_EPOCH
RUN [ -n "$CHECK_EPOCH" ] || exit 1 RUN [ -n "$CHECK_EPOCH" ] || exit 1
RUN echo "check epoch: ${CHECK_EPOCH}" && make check 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`: and in both `script/cibuild` and `script/docker`:
```sh ```sh
@@ -229,6 +237,11 @@ style conventions are in separate documents:
cd "$ROOT" cd "$ROOT"
if [ "${LINT_IN_CONTAINER:-}" = "1" ]; then 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 ./... exec golangci-lint run --config .golangci.yml ./...
fi fi
@@ -263,9 +276,7 @@ style conventions are in separate documents:
# ARG after the dependency layer so only the lint re-runs. # ARG after the dependency layer so only the lint re-runs.
ARG CHECK_EPOCH ARG CHECK_EPOCH
RUN [ -n "$CHECK_EPOCH" ] || exit 1 RUN [ -n "$CHECK_EPOCH" ] || exit 1
RUN echo "lint epoch: ${CHECK_EPOCH}" && \ RUN echo "lint epoch: ${CHECK_EPOCH}" && make lint
golangci-lint config verify --config .golangci.yml
RUN make lint
``` ```
Load-bearing properties: 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`, - **Non-Go repos get the same pattern around their own linter** — `eslint`,
`ruff`, `prettier`, `shellcheck`. Only the base image and the native lint `ruff`, `prettier`, `shellcheck`. Only the base image and the native lint
command change. 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 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 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` `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 3. Add `ENV LINT_IN_CONTAINER=1` to **every** stage of every Dockerfile that
runs checks — the lint stage and the build stage both. runs checks — the lint stage and the build stage both.
4. Delete any golangci-lint install from `script/bootstrap`, with its 4. Delete any golangci-lint install from `script/bootstrap`, with its
version and ref variables and its call site. Nothing on the host lints, version and ref variables and its call site. No lint verdict comes from
so it can only reintroduce version skew. A JS repo's `yarn install` the host any more, so it can only reintroduce version skew. A JS repo's
stays. `yarn install` stays.
5. Delete the per-checkout lint state: `GOLANGCI_LINT_CACHE` and `TMPDIR` 5. Delete the per-checkout lint state: `GOLANGCI_LINT_CACHE` and `TMPDIR`
exports, `--allow-serial-runners`, the retry/VOID wrapper, and exports, `--allow-serial-runners`, the retry/VOID wrapper, and
`.lint-cache/` from both `.gitignore` and `.dockerignore`. `.lint-cache/` from both `.gitignore` and `.dockerignore`.