# This repo is a library: there is no cmd/ and no binary to ship, so the # build ends at the stage that compiles and tests the packages. There is # no runtime stage because there is nothing to run. # Lint stage — fast feedback on formatting and lint issues. Tools are # invoked directly (not via make/script): the docker build is its own # single path. # This stage must stay the one that runs golangci-lint, and its name must # match $lint_stage in script/cibuild, which cache-busts it by name. # golangci/golangci-lint:v2.12.2 (Debian-based), 2026-08-30 FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 AS lint WORKDIR /src # Copy go mod files first for better layer caching COPY go.mod go.sum ./ RUN go mod download # Copy source code COPY . . # Inventory of the sources that actually arrived here. script/cibuild # reads these lines out of the build log and compares them against the # git index, so a .dockerignore entry or a narrowed COPY that hides a # package fails the run instead of yielding a clean report over a tree # the linter never saw. Keep it immediately after `COPY . .`, and keep # `echo context-manifest-begin` as its first command: # script/assert-context-complete matches the step by that prefix. RUN echo context-manifest-begin; \ { find . -type f -name '*.go'; \ for f in go.mod go.sum .golangci.yml .golangci.yaml; do \ if [ -f "$f" ]; then echo "./$f"; fi; \ done; } \ | sed 's|^\./||' | LC_ALL=C sort | sed 's|^|context-file: |'; \ echo context-manifest-end # Formatting check, config check, linter RUN test -z "$(gofmt -s -l .)" || { echo "gofmt needed on:"; gofmt -s -l .; exit 1; } RUN golangci-lint config verify --config .golangci.yml RUN golangci-lint run --config .golangci.yml ./... # Build stage, and the last one: script/cibuild passes no --target, so # BuildKit builds whichever stage is last and appending one drops lint, # builder and their checks out of the run. Must stay the stage that runs # go test, and its name must match $test_stage in script/cibuild, which # cache-busts it by name. # golang:1.25.7-bookworm (Debian-based: the race detector used by the # test run requires glibc), 2026-08-30 FROM golang:1.25.7-bookworm@sha256:564e366a28ad1d70f460a2b97d1d299a562f08707eb0ecb24b659e5bd6c108e1 AS builder # Depend on lint stage passing (forces BuildKit ordering) COPY --from=lint /src/go.sum /dev/null WORKDIR /build # Copy go mod files first for better layer caching COPY go.mod go.sum ./ RUN go mod download # Copy source code COPY . . # Same inventory as the lint stage, and separately checked: this stage # has its own COPY, so an intact context over there is no evidence about # the tree `go test ./...` is about to walk here. A package that did not # arrive is a package the tests never run, and the run still ends `ok`. RUN echo context-manifest-begin; \ { find . -type f -name '*.go'; \ for f in go.mod go.sum .golangci.yml .golangci.yaml; do \ if [ -f "$f" ]; then echo "./$f"; fi; \ done; } \ | sed 's|^\./||' | LC_ALL=C sort | sed 's|^|context-file: |'; \ echo context-manifest-end # Run tests: quiet first, verbose rerun on failure (and still fail). # -count=1 disables the test result cache, matching script/test. RUN go test -count=1 -timeout 90s -race -cover ./... || \ { echo "--- Rerunning with -v for details ---"; \ go test -count=1 -timeout 90s -race -v ./...; exit 1; } # No binary to emit, so the compile check is the artifact: it proves every # package builds for the target the library claims to support. RUN CGO_ENABLED=0 go build -trimpath ./...