Files
ci/Dockerfile
T
zeekayandHanzo Dev c8e4db8770 build: refuse a Go builder image older than the module it compiles
The official golang images set GOTOOLCHAIN=local, so a go.mod requiring a
newer Go than the base image does not degrade to a download — it dies
mid-build:

  go: go.mod requires go >= 1.26.5 (running go 1.26.4; GOTOOLCHAIN=local)

visor v1.108.16 shipped that failure. The reason it reached a release build
is that nothing local can see it: a dev box runs GOTOOLCHAIN=auto and simply
fetches what the module asks for, so `go build` is green right up until the
image build. The break is introduced by editing go.mod, a file with nothing
to do with Docker.

It is not one repo's problem. Sweeping every Dockerfile across the orgs —
all variants, all subdirectories, not just repo roots — found 54 builder
stages already below their own go.mod in 23 repos, and only 7 of 223 Go
stages setting GOTOOLCHAIN=auto. Fixing those 54 fixes today; this makes the
55th impossible, which is the part worth having.

bin/gover is that check, run by the build lane before any build work so a
mismatch costs seconds instead of a binfmt install and a layer cache. It
refuses only what cannot build: an image BELOW the module floor. Pinning
ahead of go.mod is valid and stays silent, because a newer toolchain
compiling an older directive always works. A floating tag against a
patch-pinned go.mod warns and passes — it builds today, and a gate that
fails what builds is a gate people learn to skip.

The error names the remediation rather than just saying no: pin the base to
the version go.mod asks for, and add ENV GOTOOLCHAIN=auto so the next bump
downloads its toolchain instead of failing.

This repo's own module and image move to go1.26.5 and adopt that same
GOTOOLCHAIN=auto line, so the pipeline holds itself to the rule it enforces.

bin/gover_test.sh pins both directions — the refusals and the allowances
(newer-than-floor, alpine suffix vs Go patch, ARG defaults, and a
multi-module repo judged by its nearest go.mod). Offline, no registry.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 16:11:53 -07:00

42 lines
2.1 KiB
Docker

# syntax=docker/dockerfile:1
#
# ci — the ci.hanzo.ai dashboard. Pure-Go, no cgo, no node: the page is
# server-rendered from a template compiled into the binary, and the design
# tokens it spends are @hanzo/brand's published stylesheet, go:embed-ed beside
# it. So the image is still the binary and a CA bundle — nothing served from
# disk, nothing to go stale against the code, and no JS toolchain on the path
# that ships the board you read when the builds are broken.
FROM golang:1.26.5-alpine AS builder
WORKDIR /build
# Resolve through the module proxy: proxy.golang.org and sum.golang.org agree
# and neither can change under us, which a direct fetch against a moved tag
# cannot promise.
ENV GOPROXY=https://proxy.golang.org,direct
# The base image above is pinned to exactly the Go go.mod asks for, so nothing
# is downloaded here — the pin is what makes this build hermetic. GOTOOLCHAIN
# is set to auto anyway, because the golang images default it to `local` and
# that turns the NEXT go.mod bump from "fetches the toolchain it needs" into
# "dies mid-build with go.mod requires go >= X". The pin is the fast path; this
# is the one that keeps a version bump from being a build break. bin/gover
# gates the same rule for every repo this pipeline builds.
ENV GOTOOLCHAIN=auto
COPY go.mod ./
RUN --mount=type=cache,id=ci-gomod,target=/go/pkg/mod go mod download
COPY . .
RUN --mount=type=cache,id=ci-gomod,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /build/ci .
FROM alpine:3.21
RUN apk add --no-cache ca-certificates tzdata \
&& addgroup -S hanzo && adduser -S hanzo -G hanzo
COPY --from=builder /build/ci /app/ci
USER hanzo
EXPOSE 8080
# Liveness only. Readiness deliberately does not gate on having a snapshot — see
# the /healthz comment in main.go: a Hanzo Git outage must render as a dashboard
# saying so, not as this pod leaving the load balancer as well.
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD wget -qO- http://127.0.0.1:8080/healthz || exit 1
ENTRYPOINT ["/app/ci"]