Run script/bootstrap in the Docker build stage (closes #42)
All checks were successful
check / check (push) Successful in 1m16s

Canonical REPO_POLICIES.md:97 requires Dockerfiles to install
development prerequisites by running script/bootstrap rather than
duplicating installs inline. The build stage did the opposite: an
inline `apk add --no-cache make` and its own `go mod download`, so it
maintained a second, independent notion of the toolchain — the
local-versus-CI divergence #24 exists to close, reintroduced one layer
down.

The stage now copies script/ plus go.mod/go.sum and runs
script/bootstrap, which ends in `go mod download`.

COPY --from=lint /usr/bin/golangci-lint is kept and moved above the
bootstrap layer. It is the only edge making this stage depend on the
lint stage, so removing it as redundant would silently stop the build
gating on lint. Copying it first also puts it on PATH before bootstrap
runs, so bootstrap's version check compares the lint stage's linter
against the pin on every build: the two stages are now provably one
toolchain rather than two that happen to agree, and bootstrap does not
pay for a from-source build of its own linter.

$GOPATH/bin joins PATH so that if the copied binary ever stops matching
the pin, bootstrap's reinstall lands somewhere PATH resolves instead of
failing its own verification.

All of it sits above ARG CHECK_EPOCH, and the chown and USER builder
still precede make check.
This commit is contained in:
2026-08-09 14:43:04 +00:00
parent 47fd4e8def
commit 3a183aa64b
2 changed files with 71 additions and 6 deletions

View File

@@ -29,24 +29,48 @@ RUN echo "gate lint, epoch ${CHECK_EPOCH}" && make lint
# golang:1.25-alpine, 2026-07-23
FROM golang@sha256:56961d79ea8129efddcc0b8643fd8a5416b4e6228cfd477e3fd61deb2672c587 AS builder
RUN apk add --no-cache make
# We never build or run as root. Create an unprivileged user and point
# HOME and the Go caches at its home so go build/test and golangci-lint
# can write their caches when we drop to it below.
# can write their caches when we drop to it below. $GOPATH/bin is on
# PATH because that is where script/bootstrap's `go install` lands: if
# the linter copied in below ever stops matching bootstrap's pin,
# bootstrap reinstalls it and then verifies the pin against what PATH
# resolves, which can only succeed if that directory is searched.
RUN adduser -D -u 1000 builder
ENV HOME=/home/builder
ENV GOPATH=/home/builder/go
ENV GOCACHE=/home/builder/.cache/go-build
ENV PATH=/home/builder/go/bin:$PATH
WORKDIR /src
# Reuse the linter binary from the lint stage; the copy also forces
# BuildKit to complete linting before this stage proceeds.
# Reuse the linter binary from the lint stage. This copy is load-bearing
# twice over and must not be deleted as redundant now that bootstrap
# below can install a linter of its own:
#
# - It is the only thing making this stage depend on the lint stage,
# so it is what forces BuildKit to finish fmt-check and lint before
# compilation and tests start. Remove it and the fail-fast design
# dies silently: the build stops gating on lint and still exits 0.
# - It is what keeps the two stages on one toolchain. script/bootstrap
# version-checks whatever PATH resolves against its pin, so copying
# the lint stage's binary in first means every build now compares
# the lint stage's linter to that pin and fails loudly if they ever
# drift apart. Bootstrap installing its own linter here instead
# would restore exactly the two-independent-toolchains problem the
# copy prevents (and cost a from-source build of the linter).
COPY --from=lint /usr/bin/golangci-lint /usr/local/bin/golangci-lint
# Install development prerequisites the same way a developer does,
# rather than duplicating the installs inline. script/ and the
# dependency manifests are copied first, and nothing else is, so this
# layer stays cached until the scripts or the dependencies change —
# bootstrap ends in `go mod download`, which is why there is no separate
# invocation of it here.
COPY script/ script/
COPY go.mod go.sum ./
RUN go mod download
RUN script/bootstrap
COPY . .
# Hand the sources and caches to the unprivileged user, then drop root