# Phase A production runner image for the hobby-ci-runner fly.io app.
#
# This is Dockerfile.spike + act_runner + entrypoint. The two files
# share the toolchain layer; updates here MUST stay in sync with
# Dockerfile.spike (or vice versa) — there is no FROM relationship
# because both layers need to live in the same image.
#
# Atlas is intentionally NOT baked: `make it` runs atlas as a docker
# container via rustainers (image arigaio/atlas:1.2.0-community-alpine),
# and deploy migrations run on the orchestrator's pre-installed atlas.
# The runner image only needs dockerd to host the atlas container.
#
# Tokens, secrets, and any *.env files are NEVER baked into image
# layers. They're injected at machine boot via Fly secrets (R2 cache
# credentials, app-level) or via the spawn payload (one-shot Forgejo
# runner registration token, per-machine). See ops/fly/README.md
# § "Security model" for the full credential flow.
#
# Version constants must stay in sync with ops/bootstrap-host.sh.

FROM debian:bookworm-slim

ARG MOLD_VERSION=v2.41.0
ARG MOLD_SHA256=a3696680d99e692970590a178bc3a33d78d60d1c6dc9db7a11b557b02b751f5d
ARG SCCACHE_VERSION=v0.8.2
# act_runner version. Bump alongside any Forgejo server upgrade so
# the runner stays API-compatible. See Forgejo release notes for the
# matching pair: https://code.forgejo.org/forgejo/runner/releases
ARG ACT_RUNNER_VERSION=v12.10.1
ARG ACT_RUNNER_SHA256=9e0378b4a22b95da98b350cfcb1b3844c1929a354a961f1ca7b49122b42abcc1

ENV RUSTUP_HOME=/usr/local/rustup \
    CARGO_HOME=/usr/local/cargo \
    PATH=/usr/local/cargo/bin:$PATH

# ─── OS deps + Docker engine + compose plugin ────────────────────
# docker-compose-plugin lives in Docker's apt repo (not Debian's);
# we add Docker's repo and install docker-ce + plugin from there.
#
# pkg-config + libssl-dev + libpq-dev: required by Rust -sys crates
# in our dep tree (openssl-sys, libpq-sys etc). Discovered missing
# in Phase 0 spike — `bootstrap-host.sh` inherits them transitively
# from Debian's base build-essential, but the slim variant doesn't.
RUN apt-get update && apt-get install -y --no-install-recommends \
        ca-certificates \
        curl \
        jq \
        make \
        clang \
        git \
        git-lfs \
        gnupg \
        procps \
        fuse-overlayfs \
        fuse3 \
        pkg-config \
        libssl-dev \
        libpq-dev \
        nodejs \
        npm \
    && install -m 0755 -d /etc/apt/keyrings \
    && curl -fsSL https://download.docker.com/linux/debian/gpg \
        -o /etc/apt/keyrings/docker.asc \
    && chmod a+r /etc/apt/keyrings/docker.asc \
    && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable" \
        > /etc/apt/sources.list.d/docker.list \
    && apt-get update && apt-get install -y --no-install-recommends \
        docker-ce \
        docker-ce-cli \
        containerd.io \
        docker-compose-plugin \
    && rm -rf /var/lib/apt/lists/*

# ─── Docker daemon config: fuse-overlayfs storage driver ─────────
# fly.io's firecracker VM blocks BOTH the new containerd-snapshotter
# overlayfs AND the legacy overlay2 driver with
# "failed to mount overlay: invalid argument" — overlay-on-overlay
# isn't supported in the VM kernel. fuse-overlayfs runs in userspace
# (via FUSE) and works regardless. Small perf penalty vs native
# overlay but the only thing that works inside a fly.io machine.
# Also pin data-root to /work/docker so docker uses the per-machine
# fly volume (the default 7.8 GB rootfs is too small).
# Discovered during Phase 0 spike — see ops/fly/spike-notes.md.
RUN mkdir -p /etc/docker \
    && echo '{"storage-driver":"fuse-overlayfs","data-root":"/work/docker"}' \
        > /etc/docker/daemon.json

# ─── Install rustup minimal (no default toolchain) ──────────────
# We do NOT use rust:1.83-bookworm as base because it bakes ~533 MB
# of stable Rust we never use. The project's nightly toolchain is
# installed by `rustup show` from rust-toolchain.toml below.
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
    | sh -s -- -y --no-modify-path --profile minimal --default-toolchain none

# ─── mold (fast linker, referenced by .cargo/config.toml) ───────
RUN curl -fsSL -o /tmp/mold.tar.gz \
        "https://github.com/rui314/mold/releases/download/${MOLD_VERSION}/mold-${MOLD_VERSION#v}-x86_64-linux.tar.gz" \
    && echo "${MOLD_SHA256}  /tmp/mold.tar.gz" | sha256sum -c - \
    && tar -xzf /tmp/mold.tar.gz -C /tmp \
    && cp "/tmp/mold-${MOLD_VERSION#v}-x86_64-linux/bin/mold" /usr/bin/mold \
    && ln -sf /usr/bin/mold /usr/bin/ld.mold \
    && rm -rf /tmp/mold*

# ─── sccache (rustc compile cache; reads/writes R2 via S3 API) ──
# SHA verification deliberately omitted — pin the SHA in a follow-up
# once we've settled on a sccache version that consistently works
# with our R2 setup.
RUN curl -fsSL -o /tmp/sccache.tar.gz \
        "https://github.com/mozilla/sccache/releases/download/${SCCACHE_VERSION}/sccache-${SCCACHE_VERSION}-x86_64-unknown-linux-musl.tar.gz" \
    && tar -xzf /tmp/sccache.tar.gz -C /tmp \
    && install -m 0755 \
        "/tmp/sccache-${SCCACHE_VERSION}-x86_64-unknown-linux-musl/sccache" \
        /usr/local/bin/sccache \
    && rm -rf /tmp/sccache*

# ─── Bake the nightly toolchain from rust-toolchain.toml ────────
# Keeping rust-toolchain.toml in /opt/toolchain serves two purposes:
#   1. `rustup show` reads it to install the right channel.
#   2. Subsequent `cargo install` invocations run in this dir so
#      rustup picks up the override (stable can't compile
#      `edition = "2024"` which cargo-leptos uses).
# Then promote the toolchain to global default so cargo works
# outside /opt/toolchain too (e.g., the cloned repo at /work/hobby).
RUN mkdir -p /opt/toolchain
COPY rust-toolchain.toml /opt/toolchain/rust-toolchain.toml
WORKDIR /opt/toolchain
RUN rustup show \
    && rustup default "$(rustup show active-toolchain | awk '{print $1}')"

# ─── cargo-leptos + cargo-nextest (built with nightly toolchain) ─
# Clean the cargo registry cache / git checkouts / build sources in
# the SAME RUN — they're ~1.4 GB of one-time-use data we don't need
# at runtime. The installed binaries live in $CARGO_HOME/bin (~50 MB).
RUN cargo install --locked cargo-leptos cargo-nextest \
    && rm -rf \
        ${CARGO_HOME}/registry/cache \
        ${CARGO_HOME}/registry/src \
        ${CARGO_HOME}/git
WORKDIR /

# ─── act_runner binary (Phase A addition over Dockerfile.spike) ──
# act_runner registers this container with Forgejo as a runner and
# picks up one job. See ops/fly/runner-entrypoint.sh for invocation.
# SHA pinned from Forgejo's published .sha256 sidecar — bumping
# ACT_RUNNER_VERSION requires also fetching and updating the SHA.
RUN curl -fsSL -o /tmp/act_runner \
        "https://code.forgejo.org/forgejo/runner/releases/download/${ACT_RUNNER_VERSION}/forgejo-runner-${ACT_RUNNER_VERSION#v}-linux-amd64" \
    && echo "${ACT_RUNNER_SHA256}  /tmp/act_runner" | sha256sum -c - \
    && install -m 0755 /tmp/act_runner /usr/local/bin/act_runner \
    && rm /tmp/act_runner

# ─── Entrypoint ──────────────────────────────────────────────────
# Starts dockerd (DiD), registers with Forgejo using the env-injected
# one-shot token, runs ONE job, then exits. Fly's auto_destroy=true
# (set on spawn) destroys the machine after exit.
COPY ops/fly/runner-entrypoint.sh /usr/local/bin/runner-entrypoint.sh
RUN chmod +x /usr/local/bin/runner-entrypoint.sh

# ─── Build-time verification ─────────────────────────────────────
# Fail the image build immediately if any tool is missing or
# unversioned. Cheap insurance against silent regressions.
RUN cargo --version \
    && rustc --version \
    && sccache --version \
    && cargo leptos --version \
    && cargo nextest --version \
    && mold --version | head -n1 \
    && docker --version \
    && act_runner --version

ENTRYPOINT ["/usr/local/bin/runner-entrypoint.sh"]
