# Dogfood — multi-stage build.
#
# One builder stage produces every binary; each runtime target copies just the
# one it needs, so the three images share all their build layers (spec §10).

# ─── editor bundle ───────────────────────────────────────────────────────────
# CodeMirror is ESM and has to be bundled. Node lives in this stage only — the
# runtime images below copy a single .js file and never see a package manager,
# which is the objection spec §1 raises against a Node toolchain, not to
# bundling as such.
FROM node:22-alpine AS editor
WORKDIR /editor
COPY crates/df-web/assets/editor/package.json ./
RUN npm install --no-audit --no-fund --silent
COPY crates/df-web/assets/editor/editor.js ./
RUN npx esbuild editor.js --bundle --minify --format=iife --target=es2020 \
        --outfile=/editor/editor.min.js \
 && ls -la /editor/editor.min.js

# ─── planner: cache dependency compilation ───────────────────────────────────
FROM rust:1-slim-bookworm AS chef
WORKDIR /build
# `build-essential` is needed for the tree-sitter grammars, which are C
# libraries compiled into df-render (spec §8).
RUN apt-get update \
 && apt-get install -y --no-install-recommends pkg-config libssl-dev build-essential \
 && rm -rf /var/lib/apt/lists/*
RUN cargo install cargo-chef --locked

FROM chef AS planner
COPY Cargo.toml Cargo.lock* ./
COPY crates ./crates
COPY migrations ./migrations
RUN cargo chef prepare --recipe-path recipe.json

# ─── builder ─────────────────────────────────────────────────────────────────
FROM chef AS builder
COPY --from=planner /build/recipe.json recipe.json
# Dependencies are built once and cached; only workspace source changes below
# this line invalidate the layer.
RUN cargo chef cook --release --recipe-path recipe.json

COPY Cargo.toml Cargo.lock* ./
COPY crates ./crates
# `sqlx::migrate!` embeds these at compile time, so they must be present.
COPY migrations ./migrations
# …and so is the editor bundle, by `include_str!`. It has to land before the
# build, not after.
COPY --from=editor /editor/editor.min.js ./crates/df-web/assets/editor.min.js
RUN cargo build --release --bin dogfood-web --bin dogfood-worker --bin dogfood-admin --bin dogfood-hook --bin dogfood-ssh

# ─── runtime base ────────────────────────────────────────────────────────────
FROM debian:bookworm-slim AS runtime-base
RUN apt-get update \
 && apt-get install -y --no-install-recommends ca-certificates git wget \
 && rm -rf /var/lib/apt/lists/* \
 && useradd --system --create-home --uid 10001 dogfood \
 && mkdir -p /srv/repos \
 && chown dogfood:dogfood /srv/repos
USER dogfood
WORKDIR /app

# ─── web ─────────────────────────────────────────────────────────────────────
FROM runtime-base AS web
COPY --from=builder /build/target/release/dogfood-web /usr/local/bin/dogfood-web
# Installed into each repository's hooks/ directory at creation.
COPY --from=builder /build/target/release/dogfood-hook /usr/local/bin/dogfood-hook
EXPOSE 8080
# /readyz, not /healthz: /healthz is an unconditional 200, so it reported this
# container healthy through the two hours of 2026-08-03 in which every request
# 500ed on an exhausted connection pool. Docker does not act on this status by
# itself — compose restarts on exit, not on unhealthy — so this buys visibility,
# not recovery. The 5s timeout is deliberately below the pool's 10s
# acquire_timeout, so a pool that can no longer hand out connections fails the
# check instead of slowly answering it.
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
    CMD wget -qO- http://127.0.0.1:8080/readyz >/dev/null || exit 1
ENTRYPOINT ["/usr/local/bin/dogfood-web"]

# ─── worker ──────────────────────────────────────────────────────────────────
FROM runtime-base AS worker
COPY --from=builder /build/target/release/dogfood-worker /usr/local/bin/dogfood-worker
# The admin CLI ships in the worker image so operational commands run where the
# repository volume is mounted.
COPY --from=builder /build/target/release/dogfood-admin /usr/local/bin/dogfood-admin
COPY --from=builder /build/target/release/dogfood-hook /usr/local/bin/dogfood-hook
ENTRYPOINT ["/usr/local/bin/dogfood-worker"]

# ─── ssh ─────────────────────────────────────────────────────────────────────
# Git over SSH (spec §6, §9). Public-key auth only, and `dogfood-ssh` dispatches
# nothing but `git-upload-pack` and `git-receive-pack` — no shell, no PTY, no
# forwarding — so the image needs `git` and nothing else.
FROM runtime-base AS ssh
# The host-key directory must exist in the *image*, owned by the runtime user.
# Docker seeds a named volume from the image path it is mounted at, including
# ownership — an empty volume on a path that does not exist comes up owned by
# root, and the process (uid 10001) then cannot write its host key.
USER root
RUN mkdir -p /etc/dogfood && chown dogfood:dogfood /etc/dogfood
USER dogfood
COPY --from=builder /build/target/release/dogfood-ssh /usr/local/bin/dogfood-ssh
# Installed into each repository's hooks/ directory; the SSH path receives
# pushes and must validate them the same way HTTPS does.
COPY --from=builder /build/target/release/dogfood-hook /usr/local/bin/dogfood-hook
# The host sshd owns :22 on this deployment, so the container listens on 2222
# and is published there too — clone URLs carry the explicit port.
EXPOSE 2222
ENTRYPOINT ["/usr/local/bin/dogfood-ssh"]
