Run the linter only in Docker (closes #20)
check / check (push) Successful in 48s

script/lint now builds only the lint stage of the Dockerfile, without
the build cache, so every run executes the linter; the image is tagged
simplelog-lint. The lint stage calls golangci-lint directly, since make
lint is itself a docker build of that stage. script/cibuild and
script/docker also build without the cache, so their check steps always
run. script/fmt no longer runs golangci-lint --fix, and script/bootstrap
no longer installs it. golangci-lint config verify is left out, on the
owner's ruling. The README, TODO.md and the script/cibuild comment say
what now runs.

Model: opus-5-5
This commit was merged in pull request #37.
This commit is contained in:
2026-10-06 09:41:32 +02:00
parent 151dd42b4b
commit 8e53530434
8 changed files with 53 additions and 28 deletions
+2 -1
View File
@@ -6,7 +6,8 @@ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
COPY . . COPY . .
RUN make fmt-check RUN make fmt-check
RUN make lint # Called directly: make lint is itself a docker build of this stage.
RUN golangci-lint run --config .golangci.yml ./...
# Test stage: run full test suite # Test stage: run full test suite
# golang 1.22.12 (2025-02-04) # golang 1.22.12 (2025-02-04)
+14 -9
View File
@@ -138,24 +138,29 @@ development workflow, and the Makefile targets are thin shims that call them.
The scripts are POSIX sh (not bash) so they run in minimal containers such as The scripts are POSIX sh (not bash) so they run in minimal containers such as
alpine. We provide: alpine. We provide:
- `script/bootstrap` — install all dependencies (go and golangci-lint if - `script/bootstrap` — install all dependencies (go if missing, then
missing, then `go mod download`) `go mod download`); golangci-lint is not installed, since it runs only in
Docker
- `script/setup` — set up the repo for development after a fresh clone: runs - `script/setup` — set up the repo for development after a fresh clone: runs
`script/bootstrap`, then `script/install-precommit` `script/bootstrap`, then `script/install-precommit`
- `script/projectname` — output the project name (our own extension); used by - `script/projectname` — output the project name (our own extension); used by
`script/docker` for the image tag `script/docker` for the image tag
- `script/test` — run the test suite (`go test -v ./...`) - `script/test` — run the test suite (`go test -v ./...`)
- `script/lint` — run golangci-lint - `script/lint` — run golangci-lint in Docker by building only the `lint`
- `script/fmt` — format all files (goimports plus `golangci-lint run --fix`; stage of the `Dockerfile` (which also runs the format check), without the
writes) build cache, so every run lints; the image is tagged `simplelog-lint`
- `script/fmt` — format all files with goimports (writes)
- `script/fmt-check` — check formatting (read-only); fails if `gofmt -l` - `script/fmt-check` — check formatting (read-only); fails if `gofmt -l`
reports files reports files
- `script/check` — run all checks: `test`, `lint`, `fmt-check` (our own - `script/check` — run all checks: `test`, `lint`, `fmt-check` (our own
extension) extension)
- `script/docker` — build the Docker image, tagged via `script/projectname` - `script/docker` — build the Docker image without the build cache, tagged
(byte-identical across repos) via `script/projectname` (byte-identical across repos); it also passes the
- `script/cibuild` — cd to the repo root and `docker build .` (what CI runs; output of `git describe` as the `VERSION` build argument, which is ignored
the image build runs the checks) because this `Dockerfile` does not declare it
- `script/cibuild` — cd to the repo root and build the Docker image without the
build cache, tagged via `script/projectname` (what CI runs; the image build
runs the checks)
- `script/precommit` — run by the git pre-commit hook (our own extension); - `script/precommit` — run by the git pre-commit hook (our own extension);
runs a `go mod tidy` guard, then `script/check` runs a `go mod tidy` guard, then `script/check`
- `script/install-precommit` — installs the git pre-commit hook (our own - `script/install-precommit` — installs the git pre-commit hook (our own
+4 -3
View File
@@ -24,6 +24,10 @@ files it depends on: .golangci.yml, REPO_POLICIES.md, .editorconfig,
# Completed Steps # Completed Steps
* 2026-10-06: the linter runs only in Docker: `script/lint` builds the
`lint` stage of the `Dockerfile`, every `docker build` in `script/`
runs without the build cache, and `script/bootstrap` no longer
installs golangci-lint
* 2026-10-06: every handler now returns a failed delivery from `Handle` * 2026-10-06: every handler now returns a failed delivery from `Handle`
instead of discarding it: console and JSON return the stdout write instead of discarding it: console and JSON return the stdout write
error, the webhook also fails on a non-2xx answer, and the multiplex error, the webhook also fails on a non-2xx answer, and the multiplex
@@ -51,9 +55,6 @@ files it depends on: .golangci.yml, REPO_POLICIES.md, .editorconfig,
# Future Steps # Future Steps
* Rewrite the Dockerfile to run make check with sha256-pinned base
images (currently golangci/golangci-lint:latest and golang:1.22,
unpinned, duplicating Makefile logic instead of calling it)
* Restructure README.md into the standard sections: Description, * Restructure README.md into the standard sections: Description,
Getting Started, Rationale, Design, TODO, License, Author Getting Started, Rationale, Design, TODO, License, Author
* Delete the old plain TODO file once this TODO.md lands * Delete the old plain TODO file once this TODO.md lands
+1 -6
View File
@@ -54,12 +54,7 @@ main() {
if missing git; then pkg_install git git git git; fi if missing git; then pkg_install git git git git; fi
if missing go; then pkg_install go golang go go; fi if missing go; then pkg_install go golang go go; fi
# golangci-lint is packaged in nix, brew, and apk; there is no apt # golangci-lint is not installed: it runs only in docker (script/lint).
# package (on apt hosts, install it from a hash-verified GitHub
# release archive manually, never curl | sh).
if missing golangci-lint; then
pkg_install golangci-lint golangci-lint golangci-lint golangci-lint
fi
go mod download go mod download
+6 -4
View File
@@ -1,13 +1,15 @@
#!/bin/sh #!/bin/sh
# script/cibuild: run the CI build. The Dockerfile runs script/check, so # script/cibuild: run the CI build. The Dockerfile runs the format check,
# a successful build implies all checks pass. # the linter and the tests, so a successful build means they all pass.
# --no-cache because a cached check step is a check that did not run.
set -eu set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
docker build . docker build --no-cache -t "$("$SCRIPT_DIR/projectname")" .
} }
main "$@" main "$@"
+12 -1
View File
@@ -1,6 +1,8 @@
#!/bin/sh #!/bin/sh
# script/docker: build the Docker image tagged with the project name. # script/docker: build the Docker image tagged with the project name.
# Identical in all repos; the tag comes from script/projectname. # Identical in all repos; the tag comes from script/projectname.
# --no-cache because the gate phases the final stage depends on are RUN
# steps, and a cached one is a check that did not run.
set -eu set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
@@ -8,7 +10,16 @@ ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
docker build -t "$("$SCRIPT_DIR/projectname")" . # Own line: a failing command substitution inside an argument does
# not trip `set -e`, so the inline form degrades silently to an
# empty constant. VERSION is computed here because .dockerignore
# excludes .git, so `git describe` in a build stage yields an empty
# version without failing.
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
[ -n "$version" ] || version="unknown"
docker build --no-cache \
--build-arg VERSION="$version" \
-t "$("$SCRIPT_DIR/projectname")" .
} }
main "$@" main "$@"
-1
View File
@@ -7,7 +7,6 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
goimports -l -w . goimports -l -w .
golangci-lint run --fix
} }
main "$@" main "$@"
+14 -3
View File
@@ -1,12 +1,23 @@
#!/bin/sh #!/bin/sh
# script/lint: run the linter. # script/lint: run the linter. Linting is a phase of the Dockerfile and
# this builds that phase alone; the linter is never installed or run on
# a developer host, where a shared result cache and a host-global lock
# make its answer untrustworthy.
#
# The phase is not the last stage in the file, so it is built only when
# --target names it. --no-cache because a cached lint layer is a lint
# that did not run. The tag makes each build replace the previous image
# instead of leaving a dangling one behind.
set -eu set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
golangci-lint run docker build --no-cache \
--target lint \
-t "$("$SCRIPT_DIR/projectname")-lint" .
} }
main "$@" main "$@"