build: run all linting in Docker via Dockerfile.lint (closes #134)
All checks were successful
check / check (push) Successful in 1m17s
All checks were successful
check / check (push) Successful in 1m17s
golangci-lint is no longer installed or run on the host. script/lint is now a thin wrapper that builds the new root Dockerfile.lint, which COPYs the repo into the digest-pinned golangci/golangci-lint:v2.12.2 image and lints as a build step, so a successful build is a clean lint. This works even where the docker daemon is remote and bind mounts are impossible. Dockerfile.lint is split into a deps stage (base image, go mod download) and a lint stage (source copy, linter run). script/lint passes --no-cache-filter=lint so the lint stage executes on every invocation: caching is explicitly waived for linting, and a cached build lints nothing. The deps stage stays cached and no global cache invalidation is performed. --progress=plain keeps the linter's own output visible. golangci-lint config verify is deliberately omitted: it fetches its JSON schema over a live, unpinned HTTPS call, which would make linting network-dependent and defeat hash-pinning. script/bootstrap no longer installs golangci-lint and warns instead when docker is absent. The goimports install stays, since script/fmt and script/fmt-check still run it on the host. The root Dockerfile ran make check in its builder stage, which would now recurse into script/lint and shell out to docker build with no daemon available. It gains its own lint stage on the same pinned image, invoked directly, with the builder depending on it via COPY --from=lint and running make fmt-check, make test and make build.
This commit is contained in:
38
Dockerfile
38
Dockerfile
@@ -1,13 +1,9 @@
|
|||||||
# Build stage
|
# Lint stage - fast feedback on lint issues, before the build starts.
|
||||||
# golang 1.25-alpine, 2026-02-28
|
# The linter is invoked directly rather than through `make lint`: that
|
||||||
FROM golang@sha256:f6751d823c26342f9506c03797d2527668d095b0a15f1862cddb4d927a7a4ced AS builder
|
# target shells out to `docker build -f Dockerfile.lint`, and there is
|
||||||
|
# no docker daemon inside a docker build.
|
||||||
RUN apk add --no-cache git make gcc musl-dev binutils-gold
|
# golangci/golangci-lint:v2.12.2 (Debian-based), 2026-08-10
|
||||||
|
FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 AS lint
|
||||||
# golangci-lint v2.12.2, 2026-08-07
|
|
||||||
RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@c0d3ddc9cf3faa61a4e378e879ece580256d76e5
|
|
||||||
# goimports v0.42.0
|
|
||||||
RUN go install golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0
|
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
@@ -15,8 +11,26 @@ RUN go mod download
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Run all checks - build fails if any check fails
|
RUN make fmt-check
|
||||||
RUN make check
|
RUN golangci-lint run --config .golangci.yml ./...
|
||||||
|
|
||||||
|
# Build stage
|
||||||
|
# golang 1.25-alpine, 2026-02-28
|
||||||
|
FROM golang@sha256:f6751d823c26342f9506c03797d2527668d095b0a15f1862cddb4d927a7a4ced AS builder
|
||||||
|
|
||||||
|
RUN apk add --no-cache git make gcc musl-dev binutils-gold
|
||||||
|
|
||||||
|
# Force BuildKit to run the lint stage before proceeding
|
||||||
|
COPY --from=lint /src/go.sum /dev/null
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Run the tests - build fails if any test fails
|
||||||
|
RUN make test
|
||||||
|
|
||||||
# Build the binary
|
# Build the binary
|
||||||
RUN make build
|
RUN make build
|
||||||
|
|||||||
27
Dockerfile.lint
Normal file
27
Dockerfile.lint
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Lint-only image: used by script/lint. golangci-lint is never run on
|
||||||
|
# the host — the repo is COPYed into the build context and the linter
|
||||||
|
# runs as a build step, so a successful build IS a clean lint. This
|
||||||
|
# also works where the docker daemon is remote and bind mounts are
|
||||||
|
# impossible.
|
||||||
|
#
|
||||||
|
# `golangci-lint config verify` is deliberately NOT run here: it
|
||||||
|
# fetches its JSON schema over a live, unpinned HTTPS call, which would
|
||||||
|
# make linting network-dependent and defeat hash-pinning.
|
||||||
|
#
|
||||||
|
# golangci/golangci-lint:v2.12.2 (Debian-based), 2026-08-10
|
||||||
|
FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 AS deps
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Dependencies first, so this stage stays cached across lint runs.
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Everything below is invalidated on every run by the
|
||||||
|
# --no-cache-filter=lint that script/lint passes: caching is explicitly
|
||||||
|
# waived for linting, and a cached build lints nothing.
|
||||||
|
FROM deps AS lint
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN golangci-lint run --config .golangci.yml ./...
|
||||||
15
README.md
15
README.md
@@ -380,14 +380,21 @@ standard: normalized scripts in `script/` are the entrypoints for the
|
|||||||
development workflow, and the Makefile targets are thin shims that call
|
development workflow, and the Makefile targets are thin shims that call
|
||||||
them. We provide:
|
them. We provide:
|
||||||
|
|
||||||
- `script/bootstrap` — install all dependencies (go, pinned
|
- `script/bootstrap` — install all dependencies (go, pinned goimports,
|
||||||
golangci-lint and goimports, `go mod download`)
|
`go mod download`). It does not install golangci-lint: see
|
||||||
|
`script/lint` below.
|
||||||
- `script/setup` — make a fresh clone ready for development: bootstrap
|
- `script/setup` — make a fresh clone ready for development: bootstrap
|
||||||
plus the git pre-commit hook
|
plus the git pre-commit hook
|
||||||
- `script/projectname` — print the project name (used for the Docker
|
- `script/projectname` — print the project name (used for the Docker
|
||||||
image tag)
|
image tag)
|
||||||
- `script/test` — run the test suite (race detector, coverage)
|
- `script/test` — run the test suite (race detector, coverage)
|
||||||
- `script/lint` — run golangci-lint
|
- `script/lint` — run golangci-lint, always inside Docker: it builds
|
||||||
|
`Dockerfile.lint`, which COPYs the repo into the digest-pinned
|
||||||
|
`golangci-lint` image and lints as a build step, so a successful
|
||||||
|
build is a clean lint. The linter is never installed or run on the
|
||||||
|
host, and Docker is the only prerequisite. Caching is waived for
|
||||||
|
linting: the lint stage is forced to execute on every run with
|
||||||
|
`--no-cache-filter`, because a cached build lints nothing.
|
||||||
- `script/fmt` — format all code (gofmt -s, goimports)
|
- `script/fmt` — format all code (gofmt -s, goimports)
|
||||||
- `script/fmt-check` — check formatting (read-only)
|
- `script/fmt-check` — check formatting (read-only)
|
||||||
- `script/check` — run test, lint, and fmt-check
|
- `script/check` — run test, lint, and fmt-check
|
||||||
@@ -403,7 +410,7 @@ them. We provide:
|
|||||||
```sh
|
```sh
|
||||||
make build # Build binary to bin/dnswatcher
|
make build # Build binary to bin/dnswatcher
|
||||||
make test # Run tests with race detector
|
make test # Run tests with race detector
|
||||||
make lint # Run golangci-lint
|
make lint # Run golangci-lint in Docker (requires docker)
|
||||||
make fmt # Format code
|
make fmt # Format code
|
||||||
make check # Run all checks (test, lint, fmt-check)
|
make check # Run all checks (test, lint, fmt-check)
|
||||||
make clean # Remove build artifacts
|
make clean # Remove build artifacts
|
||||||
|
|||||||
10
TODO.md
10
TODO.md
@@ -25,6 +25,16 @@ confirm make check still passes.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-08-10: all linting moved into Docker: new root `Dockerfile.lint`
|
||||||
|
on the digest-pinned `golangci/golangci-lint:v2.12.2` image,
|
||||||
|
`script/lint` reduced to a thin wrapper that builds it with
|
||||||
|
`--no-cache-filter=lint` so the linter actually executes every run,
|
||||||
|
golangci-lint install dropped from `script/bootstrap` (goimports
|
||||||
|
stays, `script/fmt` needs it on the host), and the root `Dockerfile`
|
||||||
|
given its own lint stage so its build no longer recurses through
|
||||||
|
`make check` into `script/lint`. `golangci-lint config verify` is
|
||||||
|
deliberately omitted: it fetches its schema over an unpinned live
|
||||||
|
HTTPS call
|
||||||
- 2026-08-07: golangci-lint bumped to v2.12.2 (commit-pinned installs
|
- 2026-08-07: golangci-lint bumped to v2.12.2 (commit-pinned installs
|
||||||
in `Dockerfile` and `script/bootstrap`); `.golangci.yml` set to the
|
in `Dockerfile` and `script/bootstrap`); `.golangci.yml` set to the
|
||||||
org-standard v2-schema config used across the org's repos
|
org-standard v2-schema config used across the org's repos
|
||||||
|
|||||||
@@ -3,15 +3,16 @@
|
|||||||
# this repo. Idempotent: every install is guarded by a check so already
|
# this repo. Idempotent: every install is guarded by a check so already
|
||||||
# installed tools are skipped. Base tooling comes from nix, apt, brew,
|
# installed tools are skipped. Base tooling comes from nix, apt, brew,
|
||||||
# or apk (detected in that order); assumes nothing is present.
|
# or apk (detected in that order); assumes nothing is present.
|
||||||
# golangci-lint and goimports are installed via `go install` at the same
|
# goimports is installed via `go install` at a pinned commit (never
|
||||||
# pinned commits the Dockerfile uses (never "latest").
|
# "latest") because script/fmt and script/fmt-check run it on the host.
|
||||||
|
# The linter is NOT installed here: golangci-lint runs via docker only
|
||||||
|
# (script/lint), pinned by image digest, so its only prerequisite is a
|
||||||
|
# working docker.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
# Pinned versions, 2026-08-07 (same pins as the Dockerfile)
|
# Pinned version, 2026-08-07 (same pin as the Dockerfile)
|
||||||
# golangci-lint v2.12.2
|
|
||||||
GOLANGCI_LINT_REF="github.com/golangci/golangci-lint/v2/cmd/golangci-lint@c0d3ddc9cf3faa61a4e378e879ece580256d76e5"
|
|
||||||
# goimports v0.42.0
|
# goimports v0.42.0
|
||||||
GOIMPORTS_REF="golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0"
|
GOIMPORTS_REF="golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0"
|
||||||
|
|
||||||
@@ -69,11 +70,18 @@ main() {
|
|||||||
if missing make; then pkg_install gnumake make make make; fi
|
if missing make; then pkg_install gnumake make make make; fi
|
||||||
if missing go; then pkg_install go golang go go; fi
|
if missing go; then pkg_install go golang go go; fi
|
||||||
|
|
||||||
# Lint/format tools, pinned via go install (installs into
|
# Format tools, pinned via go install (installs into
|
||||||
# "$(go env GOPATH)/bin"; ensure that is on your PATH).
|
# "$(go env GOPATH)/bin"; ensure that is on your PATH).
|
||||||
if missing golangci-lint; then go install "$GOLANGCI_LINT_REF"; fi
|
|
||||||
if missing goimports; then go install "$GOIMPORTS_REF"; fi
|
if missing goimports; then go install "$GOIMPORTS_REF"; fi
|
||||||
|
|
||||||
|
# Linting runs via docker only (script/lint). Warn, don't fail:
|
||||||
|
# everything except `make lint` works without it.
|
||||||
|
if missing docker; then
|
||||||
|
echo "bootstrap: WARNING: docker not found; make lint and" >&2
|
||||||
|
echo "bootstrap: make docker require it. Install docker to" >&2
|
||||||
|
echo "bootstrap: run the linter." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
go mod download
|
go mod download
|
||||||
|
|
||||||
echo "bootstrap complete"
|
echo "bootstrap complete"
|
||||||
|
|||||||
20
script/lint
20
script/lint
@@ -1,12 +1,28 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# script/lint: run the linter.
|
# script/lint: run the linter. golangci-lint is never installed or run
|
||||||
|
# on the host: it runs via docker only, one way, everywhere. This
|
||||||
|
# builds Dockerfile.lint, which COPYs the repo into the digest-pinned
|
||||||
|
# golangci-lint image and lints as a build step, so a successful build
|
||||||
|
# means a clean lint.
|
||||||
|
#
|
||||||
|
# --no-cache-filter=lint forces the lint stage (source copy + linter
|
||||||
|
# run) to execute on every invocation. Without it an unchanged tree
|
||||||
|
# returns success in well under a second having linted nothing. The
|
||||||
|
# deps stage (base image + go mod download) stays cached, and no global
|
||||||
|
# cache invalidation is performed. --progress=plain keeps the linter's
|
||||||
|
# own output visible.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
golangci-lint run --config .golangci.yml ./...
|
docker build \
|
||||||
|
--progress=plain \
|
||||||
|
--no-cache-filter=lint \
|
||||||
|
--target lint \
|
||||||
|
-f Dockerfile.lint \
|
||||||
|
.
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user