1 Commits

Author SHA1 Message Date
1e21653f44 Run every lint in a container via Dockerfile.lint (closes #40)
All checks were successful
check / check (push) Successful in 15s
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.
2026-08-10 13:45:44 +00:00

View File

@@ -94,13 +94,11 @@ 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 — the one as a build step so the build fails if the branch is not green — which requires
exception being `Dockerfile.lint`, which runs `make lint` alone because that `ARG CHECK_EPOCH` and its guard in every stage containing a check-running
is its entire purpose — which requires `ARG CHECK_EPOCH` and its guard in `RUN`, per the `CHECK_EPOCH` rule below. Without them a Dockerfile satisfies
every stage containing a check-running `RUN`, per the `CHECK_EPOCH` rule this criterion while its check layers are served from cache, so the build
below. Without them a Dockerfile satisfies this criterion while its check cannot fail on a branch that is not green.
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
@@ -126,17 +124,11 @@ 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
@@ -237,11 +229,6 @@ 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
@@ -276,7 +263,9 @@ 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}" && make lint RUN echo "lint epoch: ${CHECK_EPOCH}" && \
golangci-lint config verify --config .golangci.yml
RUN make lint
``` ```
Load-bearing properties: Load-bearing properties:
@@ -298,10 +287,7 @@ 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`, put it in `script/lint`, and it - **Keep `golangci-lint config verify`, and it costs no network.** The two
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`
@@ -338,9 +324,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. No lint verdict comes from version and ref variables and its call site. Nothing on the host lints,
the host any more, so it can only reintroduce version skew. A JS repo's so it can only reintroduce version skew. A JS repo's `yarn install`
`yarn install` stays. 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`.