Run all linting in Docker via Dockerfile.lint (closes #46)
All checks were successful
check / check (push) Successful in 1m23s
All checks were successful
check / check (push) Successful in 1m23s
Per the owner ruling, the linter runs inside a container invoked through the script/ entrypoint and is never installed on a host. A new root Dockerfile.lint COPYs the repo into the digest-pinned golangci/golangci-lint:v2.12.2 image and runs `golangci-lint config verify` and `golangci-lint run` as build steps, so a successful build IS a clean lint. script/lint is reduced to building it, which also works when the docker daemon is remote and bind mounts are impossible. script/bootstrap loses the `go install`, the pin constants, the version parser and verify_golangci_lint outright rather than being hardened: with nothing linting on the host, the $GOPATH/bin versus PATH shadowing problem those existed to diagnose has no subject. It keeps the git/make/go presence checks and `go mod download`, and warns rather than fails when docker is absent. Two traps. A lint build on an unchanged tree returns success in well under a second having run no linter, which is #32 and #39 over again. Caching is waived by ruling, so Dockerfile.lint carries ARG CHECK_EPOCH referenced inside every gate RUN -- BuildKit hashes the expanded command, not the declaration, so a declared but unreferenced ARG invalidates nothing -- and script/lint passes "$(date +%s)-$$". The PID is in that value because two lint runs land inside the same second easily and a bare epoch would cache the second one. Nothing inside an image build may shell out to docker. The main Dockerfile's lint stage therefore invokes golangci-lint directly instead of `make lint`, and its build stage runs `make test` and `make fmt-check` instead of the `make check` aggregate, which reaches script/lint. Those two remain `make` invocations rather than the bare scripts because the Makefile's `export CGO_ENABLED = 0` only applies to what it invokes, and today's `make check` gets it. COPY --from=lint /usr/bin/golangci-lint is replaced by COPY --from=lint /src/go.sum /dev/null. The copied binary was the only edge forcing BuildKit to finish linting before the build stage starts; dropping it without replacing the edge would have ended fail-fast linting silently under a still-green build. That no-op copy is the ordering edge canonical REPO_POLICIES.md prescribes. Nothing in the build stage runs the linter any more, so the binary itself is not wanted there, and ENV PATH=/home/builder/go/bin:$PATH goes with the `go install` that justified it. script/verify-linter-pin is retired -- deleted along with its README entry -- because both of its subjects ceased to exist in this same change: it compared a linter binary against GOLANGCI_LINT_VERSION in script/bootstrap, and there is now neither a binary crossing between stages nor a version pin in bootstrap. The drift it guarded has not gone away, it has moved. The linter is still pinned twice, now as the FROM line of Dockerfile.lint and the FROM line of the Dockerfile lint stage, with nothing syncing them, which is exactly what #42 made a build failure. Its replacement is one new script/verify-lint-image-pin that compares those two references to each other and deliberately restates neither pin: a hardcoded expected digest would be a third copy and the same drift one file further out. It runs as a gate in both files, so `make lint`, `make check` and `make docker` all catch drift, and an unreadable reference is a hard failure rather than a vacuous pass. `golangci-lint config verify` is included per the ruling. The concern about its unpinned live HTTPS schema fetch was measured rather than assumed: under --network none the pinned binary both passes a valid config and rejects an invalid one with the jsonschema error, so it validates against a schema it embeds and linting needs no network beyond pulling the image. The README states that rather than a requirement that does not exist. Verified. `make lint` green with every PATH directory containing a golangci-lint removed and `command -v golangci-lint` empty. Two consecutive script/lint runs on an untouched tree both executed the linter, 27.7s and 28.7s in the lint step under distinct epochs with the COPY layer CACHED above them. A planted unused variable failed script/lint with that exact finding and failed `make docker` at the lint stage with the build stage stopped before its COPY --from=lint, then reverted clean. The drift guard fails on tag-only, digest-only and unreadable-reference cases, naming both sides. `make check` green; `make docker` green in 5m35s with all six gates executing and the test gate reporting real coverage rather than a cached ok. In the builder image with the Go test cache off, --user 0:0 still fails TestScanHardlinkRunFailsTogether where the unprivileged user passes, so the non-root quirk is intact.
This commit is contained in:
97
README.md
97
README.md
@@ -454,16 +454,12 @@ and may be invoked directly. The provided entrypoints are:
|
||||
develop this repository, idempotently, assuming nothing is
|
||||
present. `git`, `make`, and `go` come from the first of nix, apt,
|
||||
brew, or apk found on the host, and are presence-checked only.
|
||||
`golangci-lint` is treated differently: it is checked against the
|
||||
version pinned in the script (the version the `Dockerfile` lint
|
||||
stage runs) and reinstalled with `go install` whenever the
|
||||
installed version differs — older or newer, not merely absent —
|
||||
because a host on any other version lints against different rules
|
||||
than CI. After installing, the script verifies the pin against the
|
||||
`golangci-lint` that `PATH` actually resolves; if a different copy
|
||||
shadows the install, bootstrap fails, naming both the install
|
||||
directory and the shadowing binary, rather than reporting a
|
||||
success the gate would not honour. Ends with `go mod download`.
|
||||
`golangci-lint` is deliberately **not** installed: it runs from a
|
||||
digest-pinned image via `script/lint` and never from a host
|
||||
install, so there is no host copy to drift from the pin. A missing
|
||||
`docker` is warned about rather than installed or treated as
|
||||
fatal — everything except linting works without it. Ends with
|
||||
`go mod download`.
|
||||
- `script/setup` — make a fresh clone ready for development: runs
|
||||
`script/bootstrap`, then `script/install-precommit`.
|
||||
- `script/projectname` — print this project's name (`sfdupes`).
|
||||
@@ -472,14 +468,35 @@ and may be invoked directly. The provided entrypoints are:
|
||||
- `script/test` — run the test suite with a 30-second timeout and
|
||||
coverage enabled, rerunning verbosely on failure so the logs show
|
||||
which test failed.
|
||||
- `script/lint` — run `golangci-lint` over the module with the
|
||||
repository's `.golangci.yml`.
|
||||
- `script/lint` — run the linter. It builds `Dockerfile.lint`, which
|
||||
copies the repository into the digest-pinned
|
||||
`golangci/golangci-lint` image and runs
|
||||
`golangci-lint config verify` and `golangci-lint run` as build
|
||||
steps, so a successful build is a clean lint. The linter is never
|
||||
run on the host, which makes a working `docker` the one
|
||||
prerequisite for linting — and therefore for `make check` and the
|
||||
pre-commit hook. Offline machines: the gate steps themselves make
|
||||
no network calls. `golangci-lint run` does not, and neither does
|
||||
`golangci-lint config verify` — it validates against a schema the
|
||||
pinned binary embeds, measured under `--network none` to both
|
||||
pass a valid config and reject an invalid one. The build around
|
||||
them does. `Dockerfile.lint` runs `go mod download` before the
|
||||
gates and this module has external dependencies, so a first lint
|
||||
on a machine with a cold BuildKit cache reaches the network there
|
||||
(as well as pulling the pinned image); under `--network none` it
|
||||
fails at that step, before any gate. That layer sits above the
|
||||
gates and stays cached, so once it is warm `script/lint` — and
|
||||
with it `make check` — runs entirely offline, until `go.mod` or
|
||||
`go.sum` changes and the download layer goes cold again. Because
|
||||
the daemon only ever sees a build context, this works when the
|
||||
docker daemon is remote and bind mounts are impossible.
|
||||
- `script/fmt` — format the Go sources in place (`gofmt -s -w`).
|
||||
Markdown is not formatted.
|
||||
- `script/fmt-check` — the read-only counterpart of `script/fmt`:
|
||||
prints any unformatted file and exits non-zero instead of writing.
|
||||
- `script/check` — run `script/test`, `script/lint`, and
|
||||
`script/fmt-check`, in that order. Modifies nothing.
|
||||
`script/fmt-check`, in that order. Modifies nothing. Needs
|
||||
`docker`, because `script/lint` does.
|
||||
- `script/docker` — build the Docker image, tagged with the name
|
||||
from `script/projectname`. The `Dockerfile` runs the gates as
|
||||
build steps, so this is also the check a developer or reviewer
|
||||
@@ -493,24 +510,36 @@ and may be invoked directly. The provided entrypoints are:
|
||||
- `script/install-precommit` — install the git pre-commit hook that
|
||||
runs `script/precommit`. The hook is written to the common git
|
||||
directory, so the main checkout and every worktree share it.
|
||||
- `script/verify-linter-pin` — fail unless a `golangci-lint` binary
|
||||
(given as its argument, default whatever `PATH` resolves) is
|
||||
exactly the version `script/bootstrap` pins, naming both versions
|
||||
if not. The `Dockerfile` build stage runs it on the linter it
|
||||
copies out of the lint stage: the version is pinned independently
|
||||
in the lint stage's image digest and in `script/bootstrap`, and
|
||||
bumping one alone would otherwise be absorbed silently by
|
||||
bootstrap rebuilding its pin from source, leaving the two stages
|
||||
on different linters under a green build. The pin is read from
|
||||
`script/bootstrap`, which stays its single source of truth.
|
||||
- `script/verify-lint-image-pin` — fail unless the
|
||||
`golangci/golangci-lint` reference in `Dockerfile.lint` and the
|
||||
one in the `Dockerfile` lint stage are the same image at the same
|
||||
digest, naming both if not. The linter is pinned in those two
|
||||
files and nothing else keeps them in sync, so a bump applied to
|
||||
one alone would leave `make lint` and the `Dockerfile`'s
|
||||
fail-fast lint stage checking the same tree against different
|
||||
rulesets, both green. The guard restates neither pin — a third
|
||||
copy would be the same drift one file further out — and runs as a
|
||||
gate in both files, so `make lint`, `make check` and `make docker`
|
||||
all catch it.
|
||||
|
||||
`script/docker` and `script/cibuild` both pass a freshly computed
|
||||
`CHECK_EPOCH` build argument, and the `Dockerfile`'s gate steps
|
||||
reference it. Without that, an unchanged tree lets Docker serve the
|
||||
gate layers from cache and the build exits 0 having executed no tests
|
||||
and no lint — a green it never earned. `CHECK_EPOCH` invalidates the
|
||||
gate layers on every run while leaving the pinned base images and the
|
||||
dependency layers cached.
|
||||
`script/verify-linter-pin` used to live here. It compared a linter
|
||||
binary against a version pin in `script/bootstrap`, and both of its
|
||||
subjects are gone: no linter binary is copied between build stages any
|
||||
more, and bootstrap pins no version because it installs no linter. The
|
||||
drift it existed to catch has moved from binary-versus-pin to
|
||||
pin-versus-pin, which is what `script/verify-lint-image-pin` above
|
||||
checks.
|
||||
|
||||
`script/lint`, `script/docker` and `script/cibuild` all pass a freshly
|
||||
computed `CHECK_EPOCH` build argument, and the gate steps in
|
||||
`Dockerfile.lint` and `Dockerfile` reference it. Without that, an
|
||||
unchanged tree lets Docker serve the gate layers from cache and the
|
||||
build exits 0 having executed no tests and no lint — a green it never
|
||||
earned, and one this repository has produced twice. `CHECK_EPOCH`
|
||||
invalidates the gate layers on every run while leaving the pinned base
|
||||
images and the dependency layers cached. `script/lint`'s value carries
|
||||
the process id as well as the epoch, because two lint runs land inside
|
||||
the same second easily and a bare epoch would cache the second one.
|
||||
|
||||
## Build
|
||||
|
||||
@@ -526,12 +555,14 @@ carries the compile recipe:
|
||||
pre-commit hook.
|
||||
- `make test` — run the test suite (30-second timeout; reruns with
|
||||
`-v` on failure).
|
||||
- `make lint` — run `golangci-lint` with the repo config.
|
||||
- `make lint` — run `golangci-lint` with the repo config, in Docker
|
||||
(see `script/lint`); requires `docker`.
|
||||
- `make fmt` / `make fmt-check` — format Go sources / verify
|
||||
formatting without writing.
|
||||
- `make check` — `test`, `lint`, and `fmt-check`; modifies nothing.
|
||||
- `make docker` — build the Docker image, which runs `make check` as
|
||||
a build stage.
|
||||
Requires `docker`, via `lint`.
|
||||
- `make docker` — build the Docker image, which runs the gates as
|
||||
build stages.
|
||||
- `make hooks` — install the pre-commit hook.
|
||||
- `make clean` — remove the binary.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user