Run script/bootstrap in the Docker build stage (closes #42) #44
36
Dockerfile
36
Dockerfile
@@ -29,24 +29,48 @@ RUN echo "gate lint, epoch ${CHECK_EPOCH}" && make lint
|
|||||||
# golang:1.25-alpine, 2026-07-23
|
# golang:1.25-alpine, 2026-07-23
|
||||||
FROM golang@sha256:56961d79ea8129efddcc0b8643fd8a5416b4e6228cfd477e3fd61deb2672c587 AS builder
|
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
|
# 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
|
# 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
|
RUN adduser -D -u 1000 builder
|
||||||
ENV HOME=/home/builder
|
ENV HOME=/home/builder
|
||||||
ENV GOPATH=/home/builder/go
|
ENV GOPATH=/home/builder/go
|
||||||
ENV GOCACHE=/home/builder/.cache/go-build
|
ENV GOCACHE=/home/builder/.cache/go-build
|
||||||
|
ENV PATH=/home/builder/go/bin:$PATH
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
# Reuse the linter binary from the lint stage; the copy also forces
|
# Reuse the linter binary from the lint stage. This copy is load-bearing
|
||||||
# BuildKit to complete linting before this stage proceeds.
|
# 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
|
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 ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN script/bootstrap
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Hand the sources and caches to the unprivileged user, then drop root
|
# Hand the sources and caches to the unprivileged user, then drop root
|
||||||
|
|||||||
41
TODO.md
41
TODO.md
@@ -29,6 +29,47 @@
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- install the Docker build stage's prerequisites by running
|
||||||
|
`script/bootstrap` instead of `apk add --no-cache make` inline
|
||||||
|
(2026-08-09, branch `dockerfile-bootstrap`, closes #42): canonical
|
||||||
|
`REPO_POLICIES.md:97` requires it, and the inline install left the
|
||||||
|
build stage maintaining its own notion of the toolchain — exactly
|
||||||
|
the divergence #24 exists to close, one layer down. The stage now
|
||||||
|
copies `script/` plus `go.mod`/`go.sum` and runs `script/bootstrap`,
|
||||||
|
which ends in `go mod download`, so the separate invocation of that
|
||||||
|
is gone. `COPY --from=lint /usr/bin/golangci-lint` stays, and moves
|
||||||
|
above the bootstrap layer. It is the only edge making this stage
|
||||||
|
depend on the lint stage, so deleting it as redundant would end
|
||||||
|
fail-fast linting silently; putting it first also means bootstrap's
|
||||||
|
version check now compares the lint stage's linter against the pin
|
||||||
|
on every build, which is what makes the two stages provably one
|
||||||
|
toolchain instead of two that happen to agree. Letting bootstrap
|
||||||
|
install its own linter here would have reintroduced the second
|
||||||
|
toolchain and paid for a from-source build of it. `$GOPATH/bin`
|
||||||
|
joins `PATH` so that if the copied binary ever stops matching the
|
||||||
|
pin, bootstrap's reinstall lands somewhere `PATH` resolves rather
|
||||||
|
than failing its own verification. Everything added sits above
|
||||||
|
`ARG CHECK_EPOCH`, and the `chown` and `USER builder` still precede
|
||||||
|
`make check`. Verified: bootstrap runs clean under Alpine's `sh` and
|
||||||
|
its `apk` branch, installing `git` and `make` and finding the copied
|
||||||
|
linter already at the pin; a second build served the bootstrap and
|
||||||
|
dependency layers `CACHED` while both gates ran with a fresh epoch;
|
||||||
|
a planted `unused` finding failed the build at the lint gate in
|
||||||
|
48.9s with the build stage's `make check` never starting; and the
|
||||||
|
suite run in the image as `--user 0:0` fails
|
||||||
|
`TestScanHardlinkRunFailsTogether`, so the drop to the unprivileged
|
||||||
|
user is still load-bearing. That last check needs the Go test cache
|
||||||
|
disabled — the first attempt reported `ok ... (cached)` as root,
|
||||||
|
reusing the result the build-time run had left in the shared cache,
|
||||||
|
which would have read as a pass. Build wall time, on a shared host
|
||||||
|
running many concurrent builds and so noisy: 2m13s on an unchanged
|
||||||
|
tree, 2m17s and 4m29s for two builds after a source change, 5m14s
|
||||||
|
cold. Only the cold one breaches the policy ceiling, and not because
|
||||||
|
of this change — `chown -R builder:builder /src /home/builder` walks
|
||||||
|
the module cache and re-runs on every source change, and it alone
|
||||||
|
varied between 77s and 210s across those four builds, which is also
|
||||||
|
the whole spread in the totals. The same cold measurement against
|
||||||
|
`main` is 5m03s with a 209s `chown`. Filed as #43
|
||||||
- bust the Docker layer cache for the gate steps, so `script/cibuild`
|
- bust the Docker layer cache for the gate steps, so `script/cibuild`
|
||||||
and `script/docker` cannot report a green they did not earn
|
and `script/docker` cannot report a green they did not earn
|
||||||
(2026-08-09, branch `cibuild-cache-bust`, closes #32): both scripts
|
(2026-08-09, branch `cibuild-cache-bust`, closes #32): both scripts
|
||||||
|
|||||||
Reference in New Issue
Block a user