Run all linting in Docker via Dockerfile.lint (closes #109)
All checks were successful
check / check (push) Successful in 2m45s
All checks were successful
check / check (push) Successful in 2m45s
golangci-lint no longer runs on the host. script/lint builds Dockerfile.lint, which copies the repo into the digest-pinned linter image, so the container holds only this repo and the cross-worktree cache contamination of #106 becomes structurally impossible rather than filtered after the fact. Five workers hit that contamination in one evening, in both directions. Three properties are load-bearing. --no-cache-filter=lint forces the lint stage to re-execute while deps keeps its cache, because a cached build lints nothing in 0.27s and exits 0. script/lint does not trust that flag, since Docker silently ignores an unmatched stage name: it asserts golangci-lint's own summary line appears, so no summary means no lint whatever the exit code says. And both lint steps run --network=none, which enforces rather than assumes that config verify does not fetch its schema — verify is kept, because golangci-lint run silently ignores unrecognized config keys and it is the only thing that catches a typo that disables a setting. Independently reviewed four times. Three passed the behaviour; the remaining rounds were a README merge against #151, whose premise was that the file contains no false statements. Six statements this change falsified were found and corrected across those rounds — the last reviewer re-derived every countable claim against the tree rather than reading for plausibility, and found no seventh. Supersedes #106.
This commit was merged in pull request #165.
This commit is contained in:
78
README.md
78
README.md
@@ -12,14 +12,16 @@ with retry support, logging, and observability. Category: infrastructure
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.26.1+ (the version in `go.mod`)
|
||||
- golangci-lint v2.12.2 (the version pinned in `script/bootstrap` and
|
||||
in the `Dockerfile`'s lint stage; `make bootstrap` installs it)
|
||||
- Docker (for containerized deployment, and for the lint and test
|
||||
stages of the CI gate)
|
||||
- Docker (for linting, for the test stage of the CI gate, and for
|
||||
containerized deployment)
|
||||
- `curl`, used by `script/fetch-assets` to download the third-party
|
||||
browser assets, which are not committed (`make bootstrap` installs
|
||||
it if missing)
|
||||
|
||||
golangci-lint is not a prerequisite and must not be installed on the
|
||||
host: `script/bootstrap` does not install it, and `make lint` runs the
|
||||
digest-pinned linter image via `Dockerfile.lint`.
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
@@ -27,9 +29,9 @@ with retry support, logging, and observability. Category: infrastructure
|
||||
git clone https://git.eeqj.de/sneak/webhooker.git
|
||||
cd webhooker
|
||||
|
||||
# Install Go dependencies, the pinned linter, and the third-party
|
||||
# browser assets. `make deps` alone is not enough: it only runs
|
||||
# go mod download/tidy, and the checks below need the fetched assets.
|
||||
# Install Go dependencies and the third-party browser assets.
|
||||
# `make deps` alone is not enough: it only runs go mod download/tidy,
|
||||
# and the checks below need the fetched assets.
|
||||
make bootstrap
|
||||
|
||||
# Run all checks (test, lint, format check)
|
||||
@@ -52,7 +54,7 @@ make setup # Bootstrap + install git pre-commit hook
|
||||
make assets # Fetch + verify third-party browser assets
|
||||
make fmt # Format code (gofmt + goimports)
|
||||
make fmt-check # Fail if gofmt would change anything (writes nothing)
|
||||
make lint # Run golangci-lint
|
||||
make lint # Run golangci-lint in Docker (Dockerfile.lint)
|
||||
make test # Run tests with race detection
|
||||
make check # test + lint + fmt-check (CI gate)
|
||||
make build # Build binary to bin/webhooker
|
||||
@@ -275,7 +277,7 @@ are inline commands with no script behind them. We provide:
|
||||
- `script/fetch-assets` — download the third-party browser assets into
|
||||
`static/`, verifying each against its pinned sha256
|
||||
- `script/test` — run the test suite
|
||||
- `script/lint` — run golangci-lint
|
||||
- `script/lint` — run golangci-lint in Docker (see Linting below)
|
||||
- `script/fmt` — format all code (writes)
|
||||
- `script/fmt-check` — check formatting (read-only)
|
||||
- `script/check` — run test, lint, and fmt-check
|
||||
@@ -1247,6 +1249,7 @@ webhooker/
|
||||
├── templates/ # Go HTML templates (base, login, sources, etc.)
|
||||
├── script/ # Scripts to Rule Them All entrypoints
|
||||
├── Dockerfile # Three stages: lint, test+build, Alpine runtime
|
||||
├── Dockerfile.lint # Lint-only image built by script/lint
|
||||
├── Makefile # 10 of 16 targets shim script/; 6 are inline
|
||||
├── go.mod / go.sum
|
||||
└── .golangci.yml # Linter configuration
|
||||
@@ -1452,6 +1455,37 @@ Two operational consequences follow from bounding the sequence:
|
||||
no shutdown diagnostics at all. Keep the deployment's grace above
|
||||
the stop timeout.
|
||||
|
||||
### Linting
|
||||
|
||||
golangci-lint never runs on the host. `script/lint` builds
|
||||
`Dockerfile.lint`, which copies the repo into the digest-pinned
|
||||
golangci-lint image and lints as a build step, so a successful build is
|
||||
a clean lint. A host binary would share one cache and one lock with
|
||||
every other checkout on the machine, which has produced both invented
|
||||
findings attributed to other worktrees and unearned passes.
|
||||
|
||||
Three properties are load-bearing:
|
||||
|
||||
- `script/lint` passes `--no-cache-filter=lint`. Without it an unchanged
|
||||
tree replays the lint layer from cache and the build exits 0 in under
|
||||
a second having linted nothing. The `deps` stage stays cacheable, so
|
||||
module downloads are not repeated. Invalidation is scoped to the one
|
||||
stage; never prune the shared build cache.
|
||||
- `script/lint` does not trust that flag. Docker silently ignores
|
||||
`--no-cache-filter` for a stage name that does not match, so a stage
|
||||
rename or a one-character typo would restore the cached false green
|
||||
with no warning and a fast exit 0. The script therefore tees the
|
||||
build output and treats a run as a pass only if golangci-lint's own
|
||||
summary line (`N issues.` / `N issues:`) appears in it: no summary,
|
||||
no lint, whatever the exit code says.
|
||||
- Both lint steps use `RUN --network=none`. `golangci-lint config
|
||||
verify` is documented as fetching its JSON schema over HTTPS, which
|
||||
would be an unpinned remote dependency; the pinned image resolves the
|
||||
schema without network access, and `--network=none` enforces that
|
||||
instead of trusting it. Verify is worth keeping because
|
||||
`golangci-lint run` silently ignores config keys it does not
|
||||
recognize, so a typo would disable a setting with no warning.
|
||||
|
||||
### Docker
|
||||
|
||||
The Dockerfile uses a three-stage build. Each stage is pinned by
|
||||
@@ -1460,7 +1494,8 @@ version is fixed independently of the compiler's:
|
||||
|
||||
1. **Lint stage** (`golangci/golangci-lint:v2.12.2`, Debian-based) —
|
||||
installs `make`, downloads dependencies, copies the source, and runs
|
||||
`make fmt-check` then `make lint`.
|
||||
`make fmt-check`, then `golangci-lint config verify` and
|
||||
`golangci-lint run`, both with `--network=none`.
|
||||
2. **Builder stage** (`golang:1.26.1-bookworm`) — depends on the lint
|
||||
stage passing (it copies a file from it), runs `script/fetch-assets`
|
||||
to download and verify the third-party browser assets, then runs
|
||||
@@ -1471,20 +1506,21 @@ version is fixed independently of the compiler's:
|
||||
runs as the non-root `webhooker` user (UID 1000), exposes port 8080,
|
||||
and includes a health check against `/.well-known/healthcheck`.
|
||||
|
||||
The lint stage invokes `golangci-lint` directly rather than `make lint`:
|
||||
it is already the pinned linter image, and `make lint` builds
|
||||
`Dockerfile.lint`, which would need a docker daemon inside this build.
|
||||
|
||||
Both check stages use Debian rather than Alpine because
|
||||
`gorm.io/driver/sqlite` pulls in `mattn/go-sqlite3`, which needs CGO
|
||||
and does not compile against musl. Only the final binary is statically
|
||||
linked, which is what lets it run on the Alpine runtime image.
|
||||
|
||||
`script/cibuild` — `docker build .` — is the CI gate: the four check
|
||||
targets run inside the image, so a build that succeeds is a repo that
|
||||
is formatted, linted, tested and compiled. Only `script/cibuild` and
|
||||
`script/docker` involve Docker. `script/lint`, and therefore
|
||||
`make lint` and `make check`, run whatever `golangci-lint` is on the
|
||||
host, which can be a different version from the pinned one — so the
|
||||
container is the authoritative lint result
|
||||
([issue #109](https://git.eeqj.de/sneak/webhooker/issues/109) tracks
|
||||
routing local linting through it as well).
|
||||
`script/cibuild` — `docker build .` — is the CI gate: the checks run
|
||||
inside the image, so a build that succeeds is a repo that is formatted,
|
||||
linted, tested and compiled. `script/lint` also uses Docker
|
||||
(`Dockerfile.lint`, see Linting above), so `make lint` and `make check`
|
||||
run the same pinned linter version the gate does; only `script/test`
|
||||
and `script/fmt-check` run on the host.
|
||||
|
||||
#### CI gate honesty
|
||||
|
||||
@@ -1497,8 +1533,8 @@ the hash of the last commit that touched the build context, so:
|
||||
- Any commit that changes code (including a squash merge whose tree
|
||||
matches an already-built branch) gets a new fingerprint, invalidates
|
||||
the `COPY . .` layer of both check stages, and really runs
|
||||
`make fmt-check`, `make lint`, `make test`, and `make build`. A run
|
||||
that reports success ran them.
|
||||
`make fmt-check`, `golangci-lint`, `make test`, and `make build`. A
|
||||
run that reports success ran them.
|
||||
- A docs-only commit leaves the fingerprint unchanged — `.dockerignore`
|
||||
excludes `*.md`, `LICENSE` and `.editorconfig` from the context
|
||||
anyway — so the image replays from cache and costs seconds.
|
||||
|
||||
Reference in New Issue
Block a user