Run all linting in Docker via Dockerfile.lint (closes #46)
All checks were successful
check / check (push) Successful in 1m38s
All checks were successful
check / check (push) Successful in 1m38s
Per the owner ruling the linter runs in a container invoked through the script/ entrypoint, never installed on a host. 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, and works with a remote docker daemon, where bind mounts are impossible. script/bootstrap loses the `go install`, the pin constants, the version parser and verify_golangci_lint: with nothing linting on the host, the $GOPATH/bin versus PATH shadowing they diagnosed has no subject. It keeps the git/make/go presence checks and `go mod download`, and warns rather than fails when docker is absent. Traps for anyone changing this. A lint build on an unchanged tree exits 0 in under a second having run no linter -- #32 and #39 again. Caching is waived by ruling: Dockerfile.lint carries ARG CHECK_EPOCH referenced inside every gate RUN, because BuildKit hashes the expanded command and a declared but unreferenced ARG invalidates nothing. script/lint passes "$(date +%s)-$$"; the PID is there because two runs land in the same second easily and a bare epoch would cache the second. Nothing inside an image build may shell out to docker. The main Dockerfile's lint stage therefore invokes golangci-lint directly rather than `make lint`, and its build stage runs `make test` and `make fmt-check` rather than the `make check` aggregate, which reaches script/lint. Both stay `make` invocations rather than bare scripts because the Makefile's `export CGO_ENABLED = 0` only reaches what it invokes. COPY --from=lint /usr/bin/golangci-lint becomes 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 now, so ENV PATH=/home/builder/go/bin:$PATH goes with the `go install` that justified it. script/verify-linter-pin is retired with its README entry: it compared a linter binary against GOLANGCI_LINT_VERSION in script/bootstrap and neither subject still exists. The drift moved rather than went away -- the linter is pinned twice, as the FROM line of Dockerfile.lint and the FROM line of the Dockerfile lint stage, which is what #42 made a build failure. script/verify-lint-image-pin compares those two references to each other and restates neither pin; a hardcoded digest would be a third copy and the same drift one file further out. It runs as a gate in both files, and an unreadable reference is a hard failure rather than a vacuous pass. `golangci-lint config verify` is included per the ruling, and its unpinned live HTTPS schema fetch was measured rather than assumed: under --network none the pinned binary passes a valid config and rejects an invalid one with the jsonschema error, so it validates against a schema it embeds. That holds for the gate steps, none of which makes a network call, but not for the build around them -- Dockerfile.lint runs `go mod download` above the gates, so a cold cache needs the network and only a warm one lints offline, until go.mod or go.sum changes. 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 under distinct epochs with the COPY layer CACHED above them. A planted unused variable failed script/lint with that finding, and failed `make docker` at the lint stage with the build stage stopped before its COPY --from=lint; 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