check / check (push) Waiting to run
script/fmt and script/fmt-check run prettier over every Markdown file again, next to gofmt. prettier is pinned by hash through package.json and yarn.lock, copied from the prompts repo with .prettierrc and .prettierignore, and is never installed on a host: a new prettier stage of the Dockerfile installs it into a digest-pinned node image, and both scripts build that stage and run it with the repository mounted. CI checks the Markdown in a markdown stage that the build stage waits on. Because make fmt-check now runs docker, the Dockerfile runs gofmt directly in its lint stage instead. All Markdown is reformatted. Model: opus-5-5
179 lines
8.2 KiB
Docker
179 lines
8.2 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.
|
|
#
|
|
# 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
|
|
|
|
# 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.
|
|
RUN echo "gate gofmt, epoch ${CHECK_EPOCH}" && \
|
|
test -z "$(gofmt -s -l .)" || \
|
|
{ echo "gofmt: files not formatted:" >&2; gofmt -s -l . >&2; exit 1; }
|
|
|
|
# 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; see the lint
|
|
# stage above.
|
|
ARG CHECK_EPOCH
|
|
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; 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 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"]
|