check / check (push) Waiting to run
Each Dockerfile stage that runs gates now checks, right after its ARG CHECK_EPOCH, that the value is not empty, and stops with a message naming script/cibuild and script/docker. A plain `docker build .` can no longer report a green from cached gate layers. script/cibuild and script/docker now append the process id to the epoch, the form script/lint already uses, so two runs started in the same second still get different values. README says both. TODO.md corrects the steady-state CACHED count recorded for issue 32 from twelve to thirteen. Model: opus-5-5
198 lines
8.9 KiB
Docker
198 lines
8.9 KiB
Docker
# Lint stage — fast feedback on formatting and lint issues
|
|
# golangci/golangci-lint:v2.12.2, 2026-08-07
|
|
FROM golangci/golangci-lint@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 AS lint
|
|
WORKDIR /src
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
COPY . .
|
|
|
|
# Cache-buster for the gate layers, and only for them. Docker
|
|
# invalidates COPY only when the copied content changes, so on an
|
|
# unchanged tree the gates below would be served from cache and the
|
|
# build would exit 0 having run nothing. script/cibuild and
|
|
# script/docker pass a fresh CHECK_EPOCH on every invocation. A build
|
|
# that passes none, such as a bare `docker build .`, fails at the check
|
|
# right after the ARG instead of quietly serving the gates from cache.
|
|
#
|
|
# Two properties this depends on. ARG is per-stage, so the markdown and
|
|
# build stages below declare it again; one declaration here would leave
|
|
# their gates cacheable. And each gate RUN must reference the value,
|
|
# because BuildKit hashes the expanded command: a declared but
|
|
# unreferenced ARG invalidates nothing.
|
|
#
|
|
# It sits below the dependency layers deliberately. Everything above it
|
|
# (the pinned base image, go mod download) keeps its cache; only the
|
|
# gates go cold.
|
|
ARG CHECK_EPOCH
|
|
RUN if [ -z "${CHECK_EPOCH}" ]; then \
|
|
echo "CHECK_EPOCH is unset; build via script/cibuild or script/docker" >&2; \
|
|
exit 1; \
|
|
fi
|
|
|
|
# The linter is invoked directly here, not through `make lint`. That
|
|
# target now runs `docker build -f Dockerfile.lint`, and a docker build
|
|
# cannot run a docker build: routing the gate through make would mean
|
|
# nesting docker inside this image. Same reason `make check` is gone
|
|
# from the build stage below, and `make fmt-check` from both stages: it
|
|
# runs prettier through docker too. Its gofmt half is the step below,
|
|
# its Markdown half the markdown stage further down. gofmt's output is
|
|
# assigned to a variable first so that its own exit status, as when it
|
|
# cannot parse a file, still fails the step.
|
|
RUN echo "gate gofmt, epoch ${CHECK_EPOCH}" && \
|
|
files="$(gofmt -s -l .)" && \
|
|
if [ -n "$files" ]; then \
|
|
echo "gofmt: files not formatted:" >&2; echo "$files" >&2; exit 1; \
|
|
fi
|
|
|
|
# The FROM above and the one in Dockerfile.lint pin the same linter
|
|
# twice, and nothing else keeps them in sync; this fails the build when
|
|
# they disagree. See the script for why it restates neither pin.
|
|
RUN echo "gate lint-image-pin, epoch ${CHECK_EPOCH}" && \
|
|
script/verify-lint-image-pin
|
|
|
|
# Same config-schema check Dockerfile.lint runs, kept here so this build
|
|
# gates on exactly what script/lint gates on. It validates against a
|
|
# schema the pinned binary embeds, so it needs no network.
|
|
RUN echo "gate config verify, epoch ${CHECK_EPOCH}" && \
|
|
golangci-lint config verify --config .golangci.yml
|
|
|
|
RUN echo "gate lint, epoch ${CHECK_EPOCH}" && \
|
|
golangci-lint run --config .golangci.yml ./...
|
|
|
|
# Prettier stage: the prettier that formats this repository's Markdown,
|
|
# never installed on a host. script/fmt and script/fmt-check build this
|
|
# stage alone and run it with the repository mounted on /src. prettier
|
|
# is installed in /tools so that the repository, mounted or copied onto
|
|
# /src, cannot hide it.
|
|
# node:22-alpine, 2026-02-22
|
|
FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 AS prettier
|
|
WORKDIR /tools
|
|
# yarn.lock pins prettier by hash, and --frozen-lockfile fails rather
|
|
# than install anything yarn.lock does not name.
|
|
COPY package.json yarn.lock ./
|
|
RUN yarn install --frozen-lockfile
|
|
ENV PATH=/tools/node_modules/.bin:$PATH
|
|
WORKDIR /src
|
|
|
|
# Markdown stage: the Markdown half of `make fmt-check`, as a gate.
|
|
FROM prettier AS markdown
|
|
COPY . .
|
|
# Second per-stage declaration of the gate cache-buster and its check;
|
|
# see the lint stage above.
|
|
ARG CHECK_EPOCH
|
|
RUN if [ -z "${CHECK_EPOCH}" ]; then \
|
|
echo "CHECK_EPOCH is unset; build via script/cibuild or script/docker" >&2; \
|
|
exit 1; \
|
|
fi
|
|
RUN echo "gate prettier, epoch ${CHECK_EPOCH}" && \
|
|
prettier --check '**/*.md' --tab-width 4 --prose-wrap always
|
|
|
|
# Build stage
|
|
# golang:1.25-alpine, 2026-07-23
|
|
FROM golang@sha256:56961d79ea8129efddcc0b8643fd8a5416b4e6228cfd477e3fd61deb2672c587 AS builder
|
|
|
|
# We never build or run as root. Create an unprivileged user and point
|
|
# HOME and the build cache at its home so go build and go test can write
|
|
# it when we drop to it below. $GOPATH/bin is deliberately not on PATH:
|
|
# script/bootstrap no longer `go install`s anything (the linter runs
|
|
# from a pinned image, never from a host install), so nothing lands
|
|
# there and adding it would only widen what this image resolves.
|
|
#
|
|
# The module cache is kept outside that home, at the base image's
|
|
# default /go/pkg/mod, and belongs to root: script/bootstrap fills it as
|
|
# root. Do not move it into the home and hand it over with `chown -R`:
|
|
# that walks every file in it, which took from about 80 s to over ten
|
|
# minutes on a shared host, depending on load.
|
|
RUN adduser -D -u 1000 builder
|
|
ENV HOME=/home/builder
|
|
ENV GOPATH=/home/builder/go
|
|
ENV GOMODCACHE=/go/pkg/mod
|
|
ENV GOCACHE=/home/builder/.cache/go-build
|
|
|
|
WORKDIR /src
|
|
|
|
# No-op file copies whose only purpose is the build-graph edge: they are
|
|
# what make this stage depend on the lint and markdown stages, and so
|
|
# what forces BuildKit to finish gofmt, the pin guard, lint and prettier
|
|
# before compilation and tests start. Remove one and the fail-fast
|
|
# design dies silently — the build stops gating on that stage and still
|
|
# exits 0. The first replaces a copy of the linter binary itself, which
|
|
# is no longer wanted here: nothing in this stage runs the linter,
|
|
# because `make lint` is now a docker build and a docker build cannot
|
|
# run inside one.
|
|
COPY --from=lint /src/go.sum /dev/null
|
|
COPY --from=markdown /src/go.sum /dev/null
|
|
|
|
# Install development prerequisites the same way a developer does,
|
|
# rather than duplicating the installs inline. Only script/ and the
|
|
# dependency manifests are copied first, nothing else, 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 script/bootstrap
|
|
|
|
# Hand builder only what it writes to, without walking the module cache.
|
|
# This layer stays cached with bootstrap.
|
|
# - /src itself: make build writes the binary into it, and git refuses
|
|
# a repository whose top directory belongs to another user.
|
|
# - the module cache's cache/download directory itself, not what is in
|
|
# it: Go only reads the downloaded modules, but make build saves its
|
|
# lookup of this module's own version from git there, in a new
|
|
# directory named after the module path.
|
|
# - builder's home: the go commands bootstrap ran as root left Go's
|
|
# telemetry files there, a few small files.
|
|
RUN chown builder:builder /src /go/pkg/mod/cache/download && \
|
|
chown -R builder:builder /home/builder
|
|
|
|
# The sources are handed to builder as they are copied, so no layer has
|
|
# to walk them. Then drop root before running any checks or builds.
|
|
COPY --chown=builder:builder . .
|
|
USER builder
|
|
|
|
# Fail the build unless the branch is green. Runs as non-root so the
|
|
# permission-denied test paths are exercised legitimately (root would
|
|
# bypass the chmod(0) the tests rely on).
|
|
#
|
|
# The gate is `make test`, not `make check`: that aggregate runs
|
|
# `script/lint` and `script/fmt-check`, which both run docker, and
|
|
# nothing inside an image build may shell out to docker. Lint and the
|
|
# format checks are not skipped by this — they ran in the lint and
|
|
# markdown stages above, which this stage's COPY --from lines make
|
|
# prerequisites. `make`, not the script directly, because the Makefile's
|
|
# `export CGO_ENABLED = 0` applies only to what it invokes.
|
|
#
|
|
# Third per-stage declaration of the gate cache-buster and its check;
|
|
# see the lint stage above for why one is not enough. It is placed after
|
|
# USER so the drop to the unprivileged user still happens before the
|
|
# checks run.
|
|
ARG CHECK_EPOCH
|
|
RUN if [ -z "${CHECK_EPOCH}" ]; then \
|
|
echo "CHECK_EPOCH is unset; build via script/cibuild or script/docker" >&2; \
|
|
exit 1; \
|
|
fi
|
|
RUN echo "gate test, epoch ${CHECK_EPOCH}" && make test
|
|
|
|
# The version stamped into the binary: the VERSION build argument when
|
|
# one is given, otherwise `git describe --tags --always` of the .git in
|
|
# the build context (git is installed by script/bootstrap above). A
|
|
# context that carries .git and still yields no version fails the build;
|
|
# with neither, as from a source tarball, it is "dev".
|
|
ARG VERSION
|
|
RUN version="${VERSION:-$(git describe --tags --always || echo dev)}"; \
|
|
if [ -e .git ] && { [ -z "$version" ] || [ "$version" = dev ] || \
|
|
[ "$version" = unknown ]; }; then \
|
|
echo "no version could be derived although the build context carries .git" >&2; \
|
|
exit 1; \
|
|
fi; \
|
|
make build VERSION="$version"
|
|
|
|
# Runtime stage
|
|
# alpine:3.22, 2026-07-23
|
|
FROM alpine@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce
|
|
|
|
COPY --from=builder /src/sfdupes /usr/local/bin/sfdupes
|
|
|
|
ENTRYPOINT ["sfdupes"]
|