Run all linting in Docker via Dockerfile.lint (closes #109)
All checks were successful
check / check (push) Successful in 3m9s
All checks were successful
check / check (push) Successful in 3m9s
golangci-lint no longer 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. The host binary shared one cache and one lock with every other checkout on the machine, which produced findings attributed to unrelated worktrees as well as unearned passes. Two properties the wrapper has to get right: - --no-cache-filter=lint forces the lint stage to re-execute. Without it an unchanged tree replays the layer and the build exits 0 in under a second having linted nothing. The deps stage stays cacheable. - Both lint steps use RUN --network=none. golangci-lint config verify is documented as fetching its JSON schema over HTTPS; the pinned image resolves it with no network, and --network=none enforces that rather than trusting it. Verify is kept because golangci-lint run silently ignores config keys it does not recognize. The main Dockerfile's lint stage now invokes golangci-lint directly instead of `make lint`, which would otherwise need a docker daemon inside the build. golangci-lint installation is removed from script/bootstrap.
This commit is contained in:
50
README.md
50
README.md
@@ -12,8 +12,10 @@ with retry support, logging, and observability. Category: infrastructure
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.26+
|
||||
- golangci-lint v2.11+
|
||||
- Docker (for containerized deployment)
|
||||
- Docker (for linting and for containerized deployment)
|
||||
|
||||
golangci-lint is not a prerequisite and must not be installed on the host:
|
||||
`make lint` runs the pinned linter image via `Dockerfile.lint`.
|
||||
|
||||
### Quick Start
|
||||
|
||||
@@ -41,7 +43,7 @@ make docker
|
||||
make bootstrap # Install all dependencies (idempotent)
|
||||
make setup # Bootstrap + install git pre-commit hook
|
||||
make fmt # Format code (gofmt + goimports)
|
||||
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
|
||||
@@ -248,7 +250,7 @@ them. We provide:
|
||||
(bootstrap, then install-precommit)
|
||||
- `script/projectname` — output the project name ("webhooker")
|
||||
- `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
|
||||
@@ -1058,6 +1060,7 @@ webhooker/
|
||||
│ └── js/app.js # Client-side JavaScript (minimal bootstrap)
|
||||
├── templates/ # Go HTML templates (base, index, login, etc.)
|
||||
├── Dockerfile # Multi-stage: lint, build+test, then Alpine runtime
|
||||
├── Dockerfile.lint # Lint-only image built by script/lint
|
||||
├── Makefile # fmt, lint, test, check, build, docker targets
|
||||
├── go.mod / go.sum
|
||||
└── .golangci.yml # Linter configuration
|
||||
@@ -1165,17 +1168,48 @@ downstream at form-parse time.
|
||||
- Container runs as non-root user (UID 1000)
|
||||
- GORM soft deletes on all entities (data preserved for audit)
|
||||
|
||||
### 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.
|
||||
|
||||
Two 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.
|
||||
- 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 multi-stage build:
|
||||
|
||||
1. **Builder stage** (Debian-based `golang:1.24`) — installs
|
||||
golangci-lint, downloads dependencies, copies source, runs `make
|
||||
check` (format verification, linting, tests, compilation).
|
||||
2. **Runtime stage** (`alpine:3.21`) — copies the binary, creates the
|
||||
1. **Lint stage** (`golangci/golangci-lint`) — copies source, runs
|
||||
`make fmt-check`, `golangci-lint config verify`, and
|
||||
`golangci-lint run`.
|
||||
2. **Builder stage** (Debian-based `golang`) — downloads dependencies,
|
||||
copies source, runs `make test` and `make build`, then relinks the
|
||||
binary statically.
|
||||
3. **Runtime stage** (`alpine:3.21`) — copies the binary, creates the
|
||||
`/var/lib/webhooker` directory for all SQLite databases, runs as
|
||||
non-root user, exposes port 8080, includes a health check.
|
||||
|
||||
The lint stage invokes `golangci-lint` directly rather than `make lint`:
|
||||
it is already the pinned linter image, and `make lint` would shell out
|
||||
to another docker build from inside this one.
|
||||
|
||||
The builder uses Debian rather than Alpine because GORM's SQLite
|
||||
dialect pulls in CGO-dependent headers at compile time. The runtime
|
||||
binary is statically linked and runs on Alpine.
|
||||
|
||||
Reference in New Issue
Block a user