build: run golangci-lint in a pinned container via script/lint (closes #41)
golangci-lint is no longer invoked on the host anywhere in the repo.
Dockerfile.lint pins golangci/golangci-lint:v2.12.2 by digest and runs
the linter as a build step, so a successful build IS a clean lint, and
`make lint` becomes a thin shim over script/lint. This removes the host
linter install that produced a false green here, where a branch that was
genuinely red with a goconst finding reported "0 issues" off the shared
host cache; a container per run has its own cache and lock.
Two deliberate divergences from the sneak/homoicon reference shape:
- Two stages rather than one. A cached `deps` stage holds
`go mod download`, then `FROM deps AS lint` carries the source copy
and the lint run, and script/lint builds with
`--no-cache-filter=lint`. Caching of the lint result is explicitly
waived (a cached build lints nothing), and splitting the stages means
busting the lint layer does not re-fetch the module cache over the
network on every run.
- No `golangci-lint config verify` step. It resolves its JSON schema
over a live, unpinned HTTPS call: an unpinned network input inside
the one step whose purpose is a pinned, reproducible gate, and a
schema-host outage would surface as a red build. `golangci-lint run`
already fails on a malformed config. The reason is recorded in a
comment in Dockerfile.lint.
.dockerignore excludes .git only; the lint reads the Go sources,
go.mod/go.sum and .golangci.yml, none of which come from there.
The TODO.md scaffold-exemption note is narrowed rather than dropped:
Dockerfile.lint and script/lint are now permitted and required, while CI
config, REPO_POLICIES.md, an application Dockerfile and any other
script/ entrypoint still are not.
Verified, since a green docker build is the classic false green: two
consecutive script/lint runs on an unchanged tree each showed the
`golangci-lint run` layer executing (9.8s and 7.9s, both "0 issues.")
while the deps layers reported CACHED; a deliberate indent-error-flow
violation failed the build naming that finding and the unused one, and a
revert went clean again. `make check` green.
This commit is contained in:
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
# The lint build reads the Go sources, go.mod/go.sum and .golangci.yml;
|
||||
# none of that comes out of .git, so keep the build context small.
|
||||
.git
|
||||
32
Dockerfile.lint
Normal file
32
Dockerfile.lint
Normal file
@@ -0,0 +1,32 @@
|
||||
# Lint-only image: used by script/lint on machines where the docker
|
||||
# daemon is remote (no bind mounts possible) — the repo is COPYed into
|
||||
# the build context and golangci-lint runs as a build step, so a
|
||||
# successful build means a clean lint.
|
||||
#
|
||||
# Two stages on purpose. script/lint builds with --no-cache-filter=lint so
|
||||
# that the lint stage re-executes on every run, including on an unchanged
|
||||
# tree (caching of the lint result is explicitly waived: a cached build
|
||||
# lints nothing). Keeping `go mod download` in a separate `deps` stage
|
||||
# means busting the lint stage does not also re-fetch the module cache
|
||||
# over the network every time.
|
||||
|
||||
# golangci/golangci-lint:v2.12.2 (Debian-based), 2026-08-07
|
||||
FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 AS deps
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
# Copy go mod files first for better layer caching
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
FROM deps AS lint
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# No `golangci-lint config verify` step here, deliberately (sneak/homoicon
|
||||
# has one). It resolves its JSON schema over a live, unpinned HTTPS call:
|
||||
# an unpinned network input inside the one step whose whole purpose is a
|
||||
# pinned, reproducible gate, and a schema-host outage would show up as a
|
||||
# red build. `golangci-lint run` already fails on a malformed config.
|
||||
RUN golangci-lint run --config .golangci.yml ./...
|
||||
16
Makefile
16
Makefile
@@ -1,8 +1,10 @@
|
||||
# Development convenience targets. This repo is exempt from the standard
|
||||
# policy scaffold (no Dockerfile, CI, or REPO_POLICIES.md); this Makefile
|
||||
# is only a thin wrapper around the Go toolchain, golangci-lint, and
|
||||
# prettier so `make fmt` / `make check` behave the same as in sneak's
|
||||
# other repos.
|
||||
# policy scaffold (no CI config, no REPO_POLICIES.md, no application
|
||||
# Dockerfile) except for the lint container: per sneak's 2026-08-09
|
||||
# ruling, linting runs in docker only, so Dockerfile.lint and script/lint
|
||||
# are part of this repo. This Makefile is otherwise only a thin wrapper
|
||||
# around the Go toolchain and prettier so `make fmt` / `make check` behave
|
||||
# the same as in sneak's other repos.
|
||||
|
||||
GO_PKGS := ./...
|
||||
MD_FILES := $(shell git ls-files '*.md')
|
||||
@@ -26,9 +28,11 @@ fmt-check:
|
||||
fi
|
||||
$(PRETTIER) --check $(MD_FILES)
|
||||
|
||||
# Run the house linter (config in .golangci.yml).
|
||||
# Run the house linter. golangci-lint is never installed on the host: the
|
||||
# work happens inside the pinned container built by Dockerfile.lint, and
|
||||
# this target is a thin shim over the script that builds it.
|
||||
lint:
|
||||
golangci-lint run $(GO_PKGS)
|
||||
./script/lint
|
||||
|
||||
# Run the test suite. Quiet on success; on failure, rerun verbosely for the
|
||||
# full output and still fail the target (the first run already proved the
|
||||
|
||||
10
README.md
10
README.md
@@ -77,10 +77,12 @@ sequences, dungeon-generation golden checks, and an RNG compatibility test
|
||||
against the original C generator.
|
||||
|
||||
For development, the `Makefile` wraps the toolchain: `make fmt` (gofmt +
|
||||
prettier), `make lint` (golangci-lint), `make test` (the suite, under the race
|
||||
detector with coverage and a timeout), and `make check` (all three). Use the
|
||||
targets rather than invoking `go test` directly — they carry the flags the
|
||||
project relies on.
|
||||
prettier), `make lint` (`script/lint`, which runs golangci-lint inside the
|
||||
pinned container built from `Dockerfile.lint` — it is never installed on the
|
||||
host, so docker is required), `make test` (the suite, under the race detector
|
||||
with coverage and a timeout), and `make check` (all three). Use the targets
|
||||
rather than invoking `go test` directly — they carry the flags the project
|
||||
relies on.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
42
TODO.md
42
TODO.md
@@ -35,6 +35,34 @@ is finished.
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-08-10 Linting moved into a container
|
||||
(https://git.eeqj.de/sneak/rgoue/issues/41). `golangci-lint` is no longer
|
||||
invoked on the host anywhere in the repo: `Dockerfile.lint` pins
|
||||
`golangci/golangci-lint:v2.12.2` by digest and runs the linter as a build
|
||||
step, so a successful build is a clean lint, and `make lint` is now a shim
|
||||
over `script/lint`. This is what killed the false green seen earlier, where a
|
||||
branch that was genuinely red with a `goconst` finding reported `0 issues` off
|
||||
the shared host cache; a container per run has its own cache and lock.
|
||||
|
||||
Two deliberate divergences from the `sneak/homoicon` reference. The image
|
||||
has two stages rather than one — a cached `deps` stage holding
|
||||
`go mod download`, then `FROM deps AS lint` with the copy and the lint run —
|
||||
and `script/lint` builds with `--no-cache-filter=lint`. Caching of the lint
|
||||
result is explicitly waived (a cached build lints nothing), and splitting
|
||||
the stages means busting the lint layer does not also re-fetch the module
|
||||
cache over the network on every run. And `golangci-lint config verify` is
|
||||
left out: it resolves its JSON schema over a live, unpinned HTTPS call,
|
||||
which is an unpinned network input inside the one step whose purpose is a
|
||||
pinned reproducible gate, and a schema-host outage would surface as a red
|
||||
build. `golangci-lint run` already fails on a malformed config.
|
||||
|
||||
Verified rather than assumed, since a green docker build is the classic
|
||||
false green: two consecutive runs on an unchanged tree each showed the
|
||||
`golangci-lint run` layer executing (11.6s and 9.7s, both `0 issues.`) while
|
||||
the `deps` layers reported `CACHED`, and a deliberate `indent-error-flow`
|
||||
violation failed the build naming that finding plus the `unused` one before
|
||||
a revert went clean again.
|
||||
|
||||
- 2026-08-09 `TestAutoSaveOnSignalRacesTurnLoop` de-flaked at the cause
|
||||
(`fix/autosave-turn-budget-36`, closes #36). The failure text was captured
|
||||
before anything was changed and it is **not** a data race: the assertion was
|
||||
@@ -853,7 +881,13 @@ is finished.
|
||||
per-game dungeon dimensions instead of the 80x24 constants; open design
|
||||
questions are resize policy, gameplay tuning at larger sizes, and a --classic
|
||||
80x24 mode.
|
||||
2. Note: this repo is exempt from the standard policy scaffold. A minimal dev
|
||||
Makefile (fmt/fmt-check/lint/test/check targets) exists per sneak's
|
||||
2026-07-07 request, but do not add a Dockerfile, CI config, or
|
||||
REPO_POLICIES.md.
|
||||
2. Note: this repo is exempt from the standard policy scaffold, but the
|
||||
exemption is narrower than it was. A minimal dev Makefile
|
||||
(fmt/fmt-check/lint/test/check targets) exists per sneak's 2026-07-07
|
||||
request. `Dockerfile.lint` and `script/lint` are now also permitted, and
|
||||
required: sneak's 2026-08-09 ruling
|
||||
(https://git.eeqj.de/sneak/rgoue/issues/41) is that every repo lints in a
|
||||
container invoked through `script/lint`, and being later and explicit it
|
||||
overrides the 2026-07-07 exemption for those two files only. Still do not
|
||||
add: CI config, `REPO_POLICIES.md`, an application `Dockerfile`, or any other
|
||||
`script/` entrypoint.
|
||||
|
||||
20
script/lint
Executable file
20
script/lint
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
# script/lint: run the linter. golangci-lint is never installed locally:
|
||||
# it runs via docker only, one way, everywhere — script/lint builds
|
||||
# Dockerfile.lint, which COPYs the repo into the pinned golangci-lint
|
||||
# image and lints as a build step. This works even when the docker daemon
|
||||
# is remote and bind mounts are impossible.
|
||||
#
|
||||
# --no-cache-filter=lint forces the lint stage to re-execute every run, so
|
||||
# an unchanged tree is still actually linted; the deps stage keeps its
|
||||
# cache, so the module download is not repeated.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
docker build --no-cache-filter=lint -f Dockerfile.lint .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user