1. CLAUDE.md and README.md described the superseded Python service. Both now describe server/ (Go), say plainly that the Python files at that level are the original the port was made from, and drop CLAUDE.md's claim that `make test-unit` is "the tier CI runs" — CI's only frontend check is the Dockerfile builder stage's gofmt + vet + go test. 2. static/units.js's F_REGIONS was guarded by nothing, despite three source comments claiming "a test asserts all three stay identical": the only check compared the Go set against the backend's Python. TestFCountriesMatchesUnitsJS now diffs the browser copy both directions. That backend cross-check also skips in CI — the image build context is frontend/, so backend/ is unreachable from the builder stage, which is the only place CI runs these tests. static/ IS in the context, so the Dockerfile copies units.js into the builder and the new assertion runs during the image build. Verified by mutating units.js and confirming the build fails. 3. Both docker-compose.test.yml files defaulted to the retired emi/thermograph-backend/app path, and the frontend harness pinned the split-era v0.0.2-split-ci tag. Path corrected in both. Rather than swap one hardcoded pin for another, backend-for-tests.sh now derives the tag from the checkout — sha-<12hex of `git log -1 -- backend/`>, the same domain-keyed rule build-push.yml and deploy.yml use — and compose requires the variable so a stale pin cannot creep back in. Verified: backend 429 passed/8 skipped; frontend go vet clean and all packages ok; frontend image builds; `make backend-up` pulls and serves on the derived tag; shellcheck zero findings across the tree. Unrelated pre-existing issue noted in the docs, not fixed here: `make test-integration` fails 7/16 with 503 against a cold throwaway backend (empty database, nothing warm). Reproduced identically on the old image, so it predates this change. Claude-Session: https://claude.ai/code/session_01AfXqHrxCJLs2D7hpQkiUiJ
104 lines
4.9 KiB
Docker
104 lines
4.9 KiB
Docker
# Thermograph frontend: server-rendered content pages, the interactive tool's
|
|
# SPA shells, and every static asset. Split from the monorepo (repo-split
|
|
# Stage 7), rewritten as a Go service (server/). No migrations, no DB, no
|
|
# pre-boot logic -- a plain exec-form CMD is enough (unlike backend, no
|
|
# separate entrypoint script needed).
|
|
#
|
|
# Multi-stage: the golang builder runs vet + the full Go test suite before
|
|
# building, so every published image provably passed the hermetic tier with
|
|
# the exact toolchain that compiled the shipping binary (this replaces the
|
|
# old in-image pytest step in .forgejo/workflows/build.yml -- the runtime
|
|
# image carries no toolchain to test with). The final stage is Alpine, not
|
|
# distroless: the Swarm stack (infra/deploy/stack/thermograph-stack.yml)
|
|
# bind-mounts a bash entrypoint shim (env-entrypoint.sh) over this image's
|
|
# entrypoint, so bash must exist inside the container; curl serves the
|
|
# HEALTHCHECK, same line as ever.
|
|
FROM golang:1.26 AS builder
|
|
|
|
WORKDIR /src
|
|
|
|
# Module graph first so the download layer caches across source-only changes.
|
|
COPY server/go.mod server/go.sum ./
|
|
RUN go mod download
|
|
|
|
COPY server/ ./
|
|
|
|
# internal/content's tests read two directories the same three-levels-up
|
|
# relative path away from the test file's own package dir (go test always
|
|
# runs with cwd set there): the committed golden fixtures (frontend/tests/
|
|
# fixtures/*.json — the same set the Python golden-diff comparison used) and
|
|
# the SSR copy (frontend/content/*.yaml, content_loader.go's LoadGlossary
|
|
# etc.). This stage only copies server/ into /src (so /src has no "frontend/"
|
|
# parent to climb to), which is why both land at container-root paths here
|
|
# instead — same three-levels-up relationship the tests' relative paths
|
|
# expect, just anchored differently.
|
|
COPY tests/fixtures /tests/fixtures
|
|
COPY content /content
|
|
|
|
# static/units.js is copied for ONE test: internal/format's
|
|
# TestFCountriesMatchesUnitsJS, which asserts the browser's F_REGIONS still
|
|
# matches the Go/backend Fahrenheit country set. The builder stage is the only
|
|
# place CI ever executes these tests, so without this the check would skip in CI
|
|
# and only ever run on a developer's checkout. Same three-levels-up relationship
|
|
# the test expects (/src/internal/format -> /static/units.js), anchored the same
|
|
# way the two directories above are.
|
|
#
|
|
# The sibling backend cross-check cannot be wired up this way: the build context
|
|
# is frontend/, so backend/ is structurally unreachable and that test stays a
|
|
# checkout-only guard.
|
|
COPY static/units.js /static/units.js
|
|
|
|
RUN test -z "$(gofmt -l .)" && go vet ./... && go test ./...
|
|
|
|
# Static binary: CGO off (no libc dependency on Alpine), -trimpath for
|
|
# reproducible paths, -s -w to strip debug info the container never uses.
|
|
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" \
|
|
-o /out/thermograph-frontend .
|
|
|
|
FROM alpine:3.22
|
|
|
|
# bash: required by the Swarm stack's env-entrypoint.sh shim (see above).
|
|
# curl: the HEALTHCHECK below (pulls in ca-certificates as a dependency).
|
|
RUN apk add --no-cache bash curl
|
|
|
|
# Same uid as the Python image: 10001 is the uid infra provisions readable
|
|
# secrets for (deploy-stack.sh installs /etc/thermograph/stack.env
|
|
# uid-10001-readable) -- do not change it. Explicit group (Alpine's `adduser
|
|
# -S` with no -G falls back to an existing system group, not a same-named
|
|
# one -- a bare `--chown=thermograph` below then has no "thermograph" group
|
|
# to resolve, which the classic (non-BuildKit) builder rejects outright).
|
|
RUN addgroup -S -g 10001 thermograph \
|
|
&& adduser -S -u 10001 -G thermograph -h /home/thermograph thermograph
|
|
|
|
COPY --from=builder /out/thermograph-frontend /usr/local/bin/thermograph-frontend
|
|
|
|
# The binary embeds its HTML templates (server/internal/render); static/ and
|
|
# content/ stay on disk, resolved relative to the working directory (see
|
|
# server/internal/config: StaticDir="static", ContentDir="content"), so /app
|
|
# mirrors the repo layout the config expects. Read-only at runtime -- the
|
|
# service is stateless and holds no data of its own.
|
|
#
|
|
# Numeric --chown, not the name: needs no /etc/passwd|group lookup at COPY
|
|
# time, so it works identically under BuildKit and the classic builder (the
|
|
# CI runner installs plain `docker.io`, no buildx plugin, so a build there
|
|
# silently uses the classic builder unless BuildKit is forced).
|
|
COPY --chown=10001:10001 static/ /app/static/
|
|
COPY --chown=10001:10001 content/ /app/content/
|
|
|
|
USER thermograph
|
|
WORKDIR /app
|
|
|
|
# No WORKERS knob anymore: uvicorn needed a process count, the Go server
|
|
# handles concurrency in one process. (The stack/compose files never set it
|
|
# for frontend, so nothing references it.)
|
|
ENV PORT=8080 \
|
|
THERMOGRAPH_BASE=/
|
|
|
|
EXPOSE 8080
|
|
|
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
|
|
CMD curl -fsS http://127.0.0.1:${PORT}/healthz || exit 1
|
|
|
|
# Exec form, no shell wrapper: the Swarm shim receives this CMD as $@ and
|
|
# execs the binary directly; PID 1 gets SIGTERM and shuts down gracefully.
|
|
CMD ["/usr/local/bin/thermograph-frontend"]
|