Dockerfile installs prerequisites inline instead of running script/bootstrap #42

Closed
opened 2026-08-09 12:01:14 +02:00 by clawbot · 1 comment
Collaborator

Surfaced by re-vendoring the policy doc in #20 — this requirement was among the 40 lines the stale copy was missing, so nothing in the repo was checking it.

Canonical REPO_POLICIES.md:97 requires: "Dockerfiles install development prerequisites by running script/bootstrap rather than duplicating installs inline; COPY script/ and the dependency manifests before running it so the bootstrap layer stays cached until dependencies change."

The Dockerfile does the opposite. The build stage runs RUN apk add --no-cache make inline, never invokes script/bootstrap, and never copies script/ alongside go.mod/go.sum.

This has teeth beyond tidiness, and #24 is exactly why. script/bootstrap is now the thing that pins the golangci-lint version and verifies the install actually took effect against what PATH resolves. A Dockerfile that bypasses it maintains a second, independent notion of the toolchain — which is precisely the local-versus-CI divergence #24 existed to close, reintroduced one layer down. Today the build stage borrows the linter binary from the lint stage via COPY --from=lint, so the versions happen to agree; nothing enforces that they continue to.

Definition of done

  1. The build stage copies script/ plus go.mod and go.sum, then runs script/bootstrap in place of the inline apk add --no-cache make.
  2. That layer sits above the ARG CHECK_EPOCH cache-buster added in #32, so the bootstrap layer stays cached while the gate layers still go cold on every build. Verify both properties still hold after the change — gates execute every run, dependency and bootstrap layers stay cached.
  3. Decide and document what happens to COPY --from=lint /usr/bin/golangci-lint. If script/bootstrap now installs the pinned linter in the build stage, that copy may be redundant — but it also currently serves as the BuildKit stage dependency that forces the lint stage to complete first. Do not remove it without replacing that dependency, or the fail-fast design silently dies. This is the trap in this issue.
  4. The non-root quirk survives: the build stage must still drop to the unprivileged builder user before make check, and TestScanHardlinkRunFailsTogether must remain genuinely exercised. Verify by running the suite in the image as --user 0:0 and confirming it fails — if it passes as root, the test is no longer testing anything.
  5. script/bootstrap must work under Alpine's sh and its apk branch. It is POSIX sh already, but it has never actually been executed in this image.
  6. Build stays under the five-minute policy ceiling. Report the timing.
  7. make check green and make docker green with the gates demonstrably executing (see #32 and #39).
Surfaced by re-vendoring the policy doc in #20 — this requirement was among the 40 lines the stale copy was missing, so nothing in the repo was checking it. Canonical `REPO_POLICIES.md:97` requires: "Dockerfiles install development prerequisites by running `script/bootstrap` rather than duplicating installs inline; COPY `script/` and the dependency manifests before running it so the bootstrap layer stays cached until dependencies change." The `Dockerfile` does the opposite. The build stage runs `RUN apk add --no-cache make` inline, never invokes `script/bootstrap`, and never copies `script/` alongside `go.mod`/`go.sum`. This has teeth beyond tidiness, and #24 is exactly why. `script/bootstrap` is now the thing that pins the golangci-lint version *and* verifies the install actually took effect against what `PATH` resolves. A Dockerfile that bypasses it maintains a second, independent notion of the toolchain — which is precisely the local-versus-CI divergence #24 existed to close, reintroduced one layer down. Today the build stage borrows the linter binary from the lint stage via `COPY --from=lint`, so the versions happen to agree; nothing enforces that they continue to. ## Definition of done 1. The build stage copies `script/` plus `go.mod` and `go.sum`, then runs `script/bootstrap` in place of the inline `apk add --no-cache make`. 2. That layer sits **above** the `ARG CHECK_EPOCH` cache-buster added in #32, so the bootstrap layer stays cached while the gate layers still go cold on every build. Verify both properties still hold after the change — gates execute every run, dependency and bootstrap layers stay cached. 3. Decide and document what happens to `COPY --from=lint /usr/bin/golangci-lint`. If `script/bootstrap` now installs the pinned linter in the build stage, that copy may be redundant — but it also currently serves as the BuildKit stage dependency that forces the lint stage to complete first. Do not remove it without replacing that dependency, or the fail-fast design silently dies. This is the trap in this issue. 4. The non-root quirk survives: the build stage must still drop to the unprivileged `builder` user before `make check`, and `TestScanHardlinkRunFailsTogether` must remain genuinely exercised. Verify by running the suite in the image as `--user 0:0` and confirming it fails — if it passes as root, the test is no longer testing anything. 5. `script/bootstrap` must work under Alpine's `sh` and its `apk` branch. It is POSIX `sh` already, but it has never actually been executed in this image. 6. Build stays under the five-minute policy ceiling. Report the timing. 7. `make check` green and `make docker` green with the gates demonstrably executing (see #32 and #39).
clawbot added this to the 1.0.0 milestone 2026-08-09 12:01:14 +02:00
Author
Collaborator

Plan.

Build stage becomes:

  • COPY --from=lint /usr/bin/golangci-lint /usr/local/bin/golangci-lint stays, and moves to sit before bootstrap. It is the BuildKit stage dependency that forces lint to finish first (item 3), so it is not removed. Placing it first also closes the gap the issue names: script/bootstrap version-checks whatever PATH resolves, so the copied binary is now compared against the pin on every build and the two toolchains can no longer silently diverge.
  • RUN apk add --no-cache make and RUN go mod download are replaced by COPY script/ script/, COPY go.mod go.sum ./, RUN script/bootstrap (bootstrap ends in go mod download).
  • ENV PATH gains $GOPATH/bin so that if the copied linter ever stops matching the pin, bootstrap's go install lands somewhere PATH resolves instead of failing its own verification.
  • All of the above stays above ARG CHECK_EPOCH (item 2); chown + USER builder still precede make check (item 4).

Verification I will run and report: gates execute on every build and bootstrap/dependency layers CACHED on a second run; planted lint finding fails the build before the build stage's make check starts; suite run in the image as --user 0:0 FAILS TestScanHardlinkRunFailsTogether; make check and make docker green; build wall time against the five-minute ceiling.

Plan. Build stage becomes: - `COPY --from=lint /usr/bin/golangci-lint /usr/local/bin/golangci-lint` **stays**, and moves to sit *before* bootstrap. It is the BuildKit stage dependency that forces lint to finish first (item 3), so it is not removed. Placing it first also closes the gap the issue names: `script/bootstrap` version-checks whatever `PATH` resolves, so the copied binary is now compared against the pin on every build and the two toolchains can no longer silently diverge. - `RUN apk add --no-cache make` and `RUN go mod download` are replaced by `COPY script/ script/`, `COPY go.mod go.sum ./`, `RUN script/bootstrap` (bootstrap ends in `go mod download`). - `ENV PATH` gains `$GOPATH/bin` so that if the copied linter ever stops matching the pin, bootstrap's `go install` lands somewhere `PATH` resolves instead of failing its own verification. - All of the above stays above `ARG CHECK_EPOCH` (item 2); `chown` + `USER builder` still precede `make check` (item 4). Verification I will run and report: gates execute on every build and bootstrap/dependency layers `CACHED` on a second run; planted lint finding fails the build before the build stage's `make check` starts; suite run in the image as `--user 0:0` FAILS `TestScanHardlinkRunFailsTogether`; `make check` and `make docker` green; build wall time against the five-minute ceiling.
Sign in to join this conversation.