# Lint stage — fast feedback on formatting and lint issues
# golangci/golangci-lint:v2.12.2 (Debian-based), 2026-08-07
FROM golangci/golangci-lint:v2.12.2@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 build stage
# below declares it again; one declaration here would leave that
# stage's gate 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 echo "gate fmt-check, epoch ${CHECK_EPOCH}" && make fmt-check
RUN echo "gate lint, epoch ${CHECK_EPOCH}" && make lint

# 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 Go caches at its home so go build/test and golangci-lint
# 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: a
# tool bootstrap installs must be runnable afterwards, and bootstrap
# verifies its own installs against what PATH resolves, so leaving that
# directory unsearched would make any install it performs both unusable
# and self-reported as shadowed. Nothing in this image is shadowed by
# it: the directory does not exist until bootstrap runs.
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. 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.
#   - Together with the check below it is what keeps the two stages on
#     one toolchain: `make check` here runs the very binary the lint
#     stage ran, not a second one that happens to agree. 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

# Fail the build, naming both versions, unless the binary that just
# arrived from the lint stage is the version script/bootstrap pins.
#
# Nothing else enforces that. The linter version is pinned in two
# independent places — the lint stage's image digest above and
# GOLANGCI_LINT_VERSION in script/bootstrap — and bumping one alone is
# an easy mistake. Without this check that mistake is invisible:
# bootstrap below would see a version that is not its pin, quietly
# rebuild the pinned one from source into a directory that is on PATH,
# verify that, and exit 0. The build would go green with the lint stage
# having linted at one version and `make check` at another, which is
# precisely the divergence the copy above exists to prevent.
#
# It runs here, before bootstrap, so that a reinstall cannot satisfy it,
# and it needs no CHECK_EPOCH: its only inputs are the copied binary and
# script/, so Docker invalidates this layer exactly when a cached result
# would stop being true.
COPY script/ script/
RUN script/verify-linter-pin /usr/local/bin/golangci-lint

# Install development prerequisites the same way a developer does,
# rather than duplicating the installs inline. Only script/ (copied
# above) 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 go.mod go.sum ./
RUN script/bootstrap

COPY . .

# Hand the sources and caches to the unprivileged user, then drop root
# before running any checks or builds.
RUN chown -R builder:builder /src /home/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).
#
# Second 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 check, epoch ${CHECK_EPOCH}" && make check

RUN make build

# Runtime stage
# alpine:3.22, 2026-07-23
FROM alpine@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce

COPY --from=builder /src/sfdupes /usr/local/bin/sfdupes

ENTRYPOINT ["sfdupes"]
