All checks were successful
check / check (push) Successful in 29s
The linter is no longer installed on the host and no longer invoked there. script/lint is now `docker build -f Dockerfile.lint .` and nothing else, with the linter running as a build step, so a successful build of that file is a clean lint — and it works unchanged where the docker daemon is remote and bind mounts are impossible. That removes three host-only failure mechanisms rather than mitigating them: the result cache keyed on file content rather than location, which produced a confirmed false green and a string of findings reported against other checkouts; the host-global $TMPDIR/golangci-lint.lock, which fails a run with `parallel golangci-lint is running` in a way no caller can distinguish from findings; and host/container version skew, which hid thirteen findings on one repo. A container per run has its own cache, its own lock and a binary pinned by digest. Resolving the recursion this creates. script/lint is a docker build, so a Dockerfile that runs `make check` would nest a build inside a build step where there is no daemon. Fixed by direction, not detection: the main Dockerfile runs script/test and script/fmt-check individually, with a comment saying why `make check` must not come back, and script/cibuild runs script/lint first for fail-fast feedback. script/check still runs all three, so developers and the pre-commit hook are unaffected. Dockerfile.lint carries the same CHECK_EPOCH guard as the main image, with the ARG placed below the dependency layer so only the lint steps re-run. Blanket --no-cache was rejected: it re-runs the dependency install on every lint and makes linting network-dependent. golangci-lint config verify is kept, on measurement rather than preference. Under the pinned v2.12.2, a bogus top-level key and a bogus key nested 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. The two catch disjoint classes, and `run` alone silently ignores the class where a threshold reads as configured and is not applied. The concern that config verify fetches its JSON schema over live HTTPS does not hold for this version: every case reproduced byte-identically under `docker run --network none`, in a container where `getent hosts golangci-lint.run` exits 2. The schema is embedded in the pinned binary. Two canonical forms are superseded and deleted rather than left standing beside the new one, because consuming repos read these documents literally and two contradictory canonical script/lint forms is worse than either. The script/bootstrap golangci-lint install landed for #28 is removed: nothing invokes a host linter now, so it can only reintroduce the skew it was written to close. Its version-enforcement principle — compare version not presence, re-resolve through PATH after installing, let a mis-parse fall through to reinstall, and call it — stays documented for any other pinned host tool. The per-checkout GOLANGCI_LINT_CACHE/TMPDIR wrapper is removed with it; its entire subject was making a host run trustworthy. Adopting repos delete .lint-cache/ from .gitignore and .dockerignore too. The Go multistage lint stage and its COPY --from=lint ordering trick go the same way: that stage ran `make lint`, which is now a docker build. Corrected everywhere the claim that a successful docker build implies lint passed — REPO_POLICIES.md, both repo checklists, the Go styleguide and the README. The guarantee now belongs to script/cibuild, which runs both container builds; a bare `docker build .` never lints at all. Verified in this repo, not only documented: two consecutive script/lint runs on a byte-identical tree both executed prettier (4.556s and 3.738s, lint layers DONE with a fresh epoch printed, dependency layers CACHED as intended); a planted violation failed the build naming the file, and reverting it went green; a bare `docker build -f Dockerfile.lint .` failed on the guard; make check, script/docker and script/cibuild all green with the check layers demonstrably executing; and the main image build completed without attempting a nested build.
42 lines
1.9 KiB
Docker
42 lines
1.9 KiB
Docker
# Lint-only image. `script/lint` builds this file and nothing else: the
|
|
# linter runs as a build step, so a successful build IS a clean lint.
|
|
# Building rather than bind-mounting is what makes it work where the
|
|
# docker daemon is remote and bind mounts are impossible.
|
|
#
|
|
# The linter is invoked directly below rather than through `make lint`.
|
|
# That is not a style choice: `script/lint` IS this build, so calling it
|
|
# from inside would recurse into a docker build with no daemon.
|
|
#
|
|
# This repo's linter is prettier over markdown. A Go repo's version of
|
|
# this file differs only in the base image and the two lint commands;
|
|
# see the containerised-lint rule in prompts/REPO_POLICIES.md.
|
|
#
|
|
# node 22-alpine, 2026-02-22
|
|
FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34
|
|
|
|
WORKDIR /app
|
|
|
|
# Dependency layer first, and deliberately above the ARG below, so it
|
|
# stays cached and only the lint steps re-run on every invocation.
|
|
# Without that ordering the cache-bust would reinstall dependencies on
|
|
# every lint and make linting network-dependent.
|
|
COPY script/ script/
|
|
COPY package.json yarn.lock ./
|
|
RUN script/bootstrap
|
|
|
|
COPY . .
|
|
|
|
# CHECK_EPOCH is a per-invocation nonce supplied by script/lint. Without
|
|
# it an unchanged tree serves the lint layer from cache and the build
|
|
# reports a lint it never ran — a green that proves nothing, which is
|
|
# the whole failure mode this file exists to avoid reintroducing. The
|
|
# guard makes a bare `docker build -f Dockerfile.lint .` fail loudly
|
|
# instead of silently reusing the empty (and therefore stable) cache
|
|
# key. The value is expanded into the lint command as well, so the cache
|
|
# miss does not depend on BuildKit's handling of an unreferenced ARG and
|
|
# the epoch is visible in the build log. Keep both references.
|
|
ARG CHECK_EPOCH
|
|
RUN [ -n "$CHECK_EPOCH" ] || exit 1
|
|
RUN echo "lint epoch: ${CHECK_EPOCH}" && \
|
|
yarn run prettier --check '**/*.md' --tab-width 4 --prose-wrap always
|