diff --git a/Dockerfile b/Dockerfile index fa861de..ad8e072 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,6 +29,12 @@ RUN go mod download COPY . . +# go.mod and go.sum must be as `go mod tidy` writes them, which is what +# `make tidy` does. Checked before the tests, which a missing go.sum line +# fails with a message that does not name `make tidy`. +RUN go mod tidy -diff || \ + { echo "go.mod or go.sum is not tidy: run make tidy" >&2; exit 1; } + # Go's build cache is kept on a tmpfs, out of the image: nothing uses it # after this step, and writing it into the image takes seconds. RUN --mount=type=tmpfs,target=/root/.cache/go-build \ @@ -36,7 +42,25 @@ RUN --mount=type=tmpfs,target=/root/.cache/go-build \ { echo "--- Rerunning with -v for details ---"; \ go test -timeout 90s -race -v ./...; exit 1; } -# Build stage. Nothing is wanted from the two phases above; the copies +# Tidy stage: `go mod tidy` in the test phase's Go, so that the files it +# writes pass the test phase's check. Nothing else depends on it, so only +# script/tidy, which names the stage after it, builds it. +# +# golang 1.27.1-trixie, 2026-09-19 +FROM golang@sha256:3b77fc618ec235a1ab412de7737f120dd507c57e8d87de4cbb7994fb94275ed5 AS tidy + +WORKDIR /src + +COPY . . + +RUN go mod tidy + +# go.mod and go.sum alone, which script/tidy writes into the working tree. +FROM scratch AS tidy-files + +COPY --from=tidy /src/go.mod /src/go.sum / + +# Build stage. Nothing is wanted from the lint and test phases; the copies # are what make BuildKit build them first, so the image, which needs this # stage, cannot be produced unless lint and test passed. # diff --git a/Makefile b/Makefile index 36cf289..6f3d484 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,10 @@ -.PHONY: bootstrap setup test lint fmt fmt-check check docker hooks build run \ - example-app +.PHONY: bootstrap setup test lint fmt fmt-check tidy check docker hooks build \ + run example-app # Makefile targets are thin shims; the implementations live in script/ # per the scripts-to-rule-them-all pattern (see the Entrypoints section -# of README.md). build and run are for working on the code by hand; +# of README.md). tidy writes go.mod and go.sum as `go mod tidy` does, +# which test checks. build and run are for working on the code by hand; # example-app checks the image with an app built on it. bootstrap: @@ -24,6 +25,9 @@ fmt: fmt-check: @script/fmt-check +tidy: + @script/tidy + check: @script/check diff --git a/README.md b/README.md index 318eba6..4efd855 100644 --- a/README.md +++ b/README.md @@ -1401,10 +1401,10 @@ addresses are never sent to GeoJS. - `internal/alerts`: takes the alerts the other parts raise, holds back repeats and those past the hourly limit, and sends the others to the webhook, Slack and ntfy, each from a queue of its own. -- `Dockerfile`: the lint and test phases, then the image, whose last stage - installs Ubuntu's packages, nixpkgs, `runsvinit` and `smallwebwaf`, with - `share/smallwebwaf.run` as runit's `run` script for `smallwebwaf` and - `share/rules.d/00-default.rules` as its default rule file. +- `Dockerfile`: the lint and test phases, the stages `script/tidy` builds, then + the image, whose last stage installs Ubuntu's packages, nixpkgs, `runsvinit` + and `smallwebwaf`, with `share/smallwebwaf.run` as runit's `run` script for + `smallwebwaf` and `share/rules.d/00-default.rules` as its default rule file. - `deploy/example-app`: an app built on the image, which `script/example-app` checks. @@ -1433,7 +1433,11 @@ so that they run in minimal containers. `script/install-precommit`. - `script/projectname`: prints the project's name, `smallwebwaf`, which `script/docker` and the others tag their images with. -- `script/test`: runs the tests, as the `test` phase of the `Dockerfile`. +- `script/test`: checks that `go.mod` and `go.sum` are as `go mod tidy` writes + them, then runs the tests, as the `test` phase of the `Dockerfile`. +- `script/tidy`: writes `go.mod` and `go.sum` as `go mod tidy` writes them, with + the Go of the `test` phase, in the `tidy` stage of the `Dockerfile`; + `make tidy` runs it. - `script/lint`: runs golangci-lint, as the `lint` phase of the `Dockerfile`. - `script/fmt`: formats the Go code with `gofmt` and the Markdown with prettier. - `script/fmt-check`: checks the formatting, and changes nothing. diff --git a/script/tidy b/script/tidy new file mode 100755 index 0000000..2be17e4 --- /dev/null +++ b/script/tidy @@ -0,0 +1,19 @@ +#!/bin/sh +# script/tidy: write go.mod and go.sum as `go mod tidy` writes them, which +# the test phase of the Dockerfile checks. This builds the Dockerfile's +# tidy-files stage, which holds the two files alone, and --output writes +# them into the working tree. The build makes no image, so it has no tag. +# --no-cache because `go mod tidy` asks the module proxy, whose answers a +# cached layer would repeat. +set -eu + +ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" + +main() { + cd "$ROOT" + docker build --no-cache \ + --target tidy-files \ + --output "type=local,dest=$ROOT" . +} + +main "$@"