From 6d77ed3c485ee4a32ba4e8711448e605ffedac3a Mon Sep 17 00:00:00 2001 From: zk Date: Wed, 20 May 2026 01:20:32 +0100 Subject: [PATCH] feat: significantly better ci images (#1516) --- .github/workflows/bot.yml | 2 +- .github/workflows/client.yml | 4 +- .github/workflows/common.yml | 2 +- .github/workflows/database-seeds.yml | 2 +- .github/workflows/dev-image.yml | 29 ++++++- .github/workflows/github-bot.yml | 2 +- .github/workflows/homepage.yml | 2 +- .github/workflows/seeds-webui.yml | 2 +- .github/workflows/server.yml | 2 +- Dockerfile.dev | 111 +++++++++++++++++++-------- bun.lock | 2 +- docker/Dockerfile.server | 6 +- 12 files changed, 119 insertions(+), 47 deletions(-) diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml index ac140428a..0af83f749 100644 --- a/.github/workflows/bot.yml +++ b/.github/workflows/bot.yml @@ -38,7 +38,7 @@ jobs: test: runs-on: ubuntu-latest container: - image: ghcr.io/zkldi/tachi-dev:main + image: ghcr.io/zkldi/tachi-ci:main options: --user root env: NODE_ENV: "test" diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index f4a1da077..bf2cdbbbb 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -25,7 +25,7 @@ jobs: test: runs-on: ubuntu-latest container: - image: ghcr.io/zkldi/tachi-dev:main + image: ghcr.io/zkldi/tachi-ci:main options: --user root steps: - name: Checkout @@ -45,7 +45,7 @@ jobs: build: runs-on: ubuntu-latest container: - image: ghcr.io/zkldi/tachi-dev:main + image: ghcr.io/zkldi/tachi-ci:main options: --user root needs: test if: ${{ github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }} diff --git a/.github/workflows/common.yml b/.github/workflows/common.yml index 3acc305bb..f3140231e 100644 --- a/.github/workflows/common.yml +++ b/.github/workflows/common.yml @@ -22,7 +22,7 @@ jobs: test: runs-on: ubuntu-latest container: - image: ghcr.io/zkldi/tachi-dev:main + image: ghcr.io/zkldi/tachi-ci:main options: --user root steps: - name: Checkout diff --git a/.github/workflows/database-seeds.yml b/.github/workflows/database-seeds.yml index 2771dd9e0..ee97e5182 100644 --- a/.github/workflows/database-seeds.yml +++ b/.github/workflows/database-seeds.yml @@ -26,7 +26,7 @@ jobs: test: runs-on: ubuntu-latest container: - image: ghcr.io/zkldi/tachi-dev:main + image: ghcr.io/zkldi/tachi-ci:main options: --user root steps: - name: Checkout diff --git a/.github/workflows/dev-image.yml b/.github/workflows/dev-image.yml index 3e5d1e6b0..2a21f753a 100644 --- a/.github/workflows/dev-image.yml +++ b/.github/workflows/dev-image.yml @@ -53,16 +53,39 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - name: Build and push + # `tachi-ci` = the minimal `base` stage. Used as the `container:` image + # for every workspace CI job. Kept small so the per-job image pull is + # cheap. + - name: Build and push tachi-ci (base stage) uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . file: Dockerfile.dev + target: base + push: ${{ github.ref == 'refs/heads/main' && github.event_name != 'pull_request' }} + tags: | + ghcr.io/zkldi/tachi-ci:main + ghcr.io/zkldi/tachi-ci:latest + ghcr.io/zkldi/tachi-ci:${{ steps.revision.outputs.image_tag }} + ghcr.io/zkldi/tachi-ci:${{ github.sha }} + cache-from: type=gha,scope=dev-image-base + cache-to: type=gha,mode=max,scope=dev-image-base + + # `tachi-dev` = full devcontainer image (`dev` stage on top of `base`). + # Pulled by devs once and rebuilt rarely; CI never touches it. + - name: Build and push tachi-dev (dev stage) + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + with: + context: . + file: Dockerfile.dev + target: dev push: ${{ github.ref == 'refs/heads/main' && github.event_name != 'pull_request' }} tags: | ghcr.io/zkldi/tachi-dev:main ghcr.io/zkldi/tachi-dev:latest ghcr.io/zkldi/tachi-dev:${{ steps.revision.outputs.image_tag }} ghcr.io/zkldi/tachi-dev:${{ github.sha }} - cache-from: type=gha,scope=dev-image - cache-to: type=gha,mode=max,scope=dev-image + cache-from: | + type=gha,scope=dev-image-dev + type=gha,scope=dev-image-base + cache-to: type=gha,mode=max,scope=dev-image-dev diff --git a/.github/workflows/github-bot.yml b/.github/workflows/github-bot.yml index 0d91b85ae..ed6b5f4a8 100644 --- a/.github/workflows/github-bot.yml +++ b/.github/workflows/github-bot.yml @@ -28,7 +28,7 @@ jobs: test: runs-on: ubuntu-latest container: - image: ghcr.io/zkldi/tachi-dev:main + image: ghcr.io/zkldi/tachi-ci:main options: --user root steps: - name: Checkout diff --git a/.github/workflows/homepage.yml b/.github/workflows/homepage.yml index 2c4bf76fe..82ae22a46 100644 --- a/.github/workflows/homepage.yml +++ b/.github/workflows/homepage.yml @@ -16,7 +16,7 @@ jobs: deploy: runs-on: ubuntu-latest container: - image: ghcr.io/zkldi/tachi-dev:main + image: ghcr.io/zkldi/tachi-ci:main options: --user root if: ${{ github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }} steps: diff --git a/.github/workflows/seeds-webui.yml b/.github/workflows/seeds-webui.yml index 9b92a61d3..6ab29e85b 100644 --- a/.github/workflows/seeds-webui.yml +++ b/.github/workflows/seeds-webui.yml @@ -26,7 +26,7 @@ jobs: build: runs-on: ubuntu-latest container: - image: ghcr.io/zkldi/tachi-dev:main + image: ghcr.io/zkldi/tachi-ci:main options: --user root steps: - name: Checkout diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml index b02b15183..e5657edeb 100644 --- a/.github/workflows/server.yml +++ b/.github/workflows/server.yml @@ -36,7 +36,7 @@ jobs: test: runs-on: ubuntu-latest container: - image: ghcr.io/zkldi/tachi-dev:main + image: ghcr.io/zkldi/tachi-ci:main options: --user root env: NODE_ENV: "test" diff --git a/Dockerfile.dev b/Dockerfile.dev index 7ec677817..490cd132b 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,56 +1,103 @@ -# For use via `devcontainer.json`. The docker-compose-dev file sets some other -# important variables. +# syntax=docker/dockerfile:1.7 +# +# Two stages live here: +# +# - `base`: minimal image used as the `container:` for every CI job. Just +# enough to run `bun install`, `bun run …`, `just`, `psql`, `rsync`, ssh. +# Published as `ghcr.io/zkldi/tachi-ci`. +# - `dev`: superset of `base` with all the devcontainer QoL (fish, neovim, +# gh, ripgrep, fd, bat, fzf, mkdocs, minio-client, locales, sudo, the +# `tachi` user, ...). Published as `ghcr.io/zkldi/tachi-dev`. +# +# Always use `--no-install-recommends`. Debian's recommends-by-default pulls +# the full LLVM/Mesa/Vulkan/GCC chain in via various transitive deps and adds +# ~750MB of crap that nothing here actually needs. -FROM debian:13 +# --------------------------------------------------------------------------- +# bun-src: just here so we can `COPY --from=` the binary into our stages. +# +# We deliberately track the same floating tag prod uses (see +# `docker/Dockerfile.server`, `FROM oven/bun:alpine`). The `debian` variant +# is used here because our base is `debian:13-slim` (glibc), not Alpine +# (musl). If/when prod pins a specific bun version, pin this to match. +# --------------------------------------------------------------------------- +FROM oven/bun:debian AS bun-src + +# --------------------------------------------------------------------------- +# base: CI image +# --------------------------------------------------------------------------- +FROM debian:13-slim AS base WORKDIR /tachi # Add the official PostgreSQL APT repository so we can install client tools # that match the server version (v18). The Debian-packaged postgresql-client -# only provides v17 on Debian 13 (trixie). +# only provides v17 on Debian 13 (trixie). `gnupg` is only needed to dearmor +# the keyring and is purged afterwards. RUN apt-get update \ - && apt-get install -y --no-install-recommends gnupg curl ca-certificates \ + && apt-get install -y --no-install-recommends \ + ca-certificates curl gnupg \ && curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \ | gpg --dearmor -o /etc/apt/trusted.gpg.d/pgdg.gpg \ && echo "deb https://apt.postgresql.org/pub/repos/apt trixie-pgdg main" \ - > /etc/apt/sources.list.d/pgdg.list - -RUN DEBIAN_FRONTEND=noninteractive apt-get update \ - && apt-get upgrade -y \ - && apt-get install -y \ - # essentials - git npm locales sudo unzip jq \ - # docs pythonisms - python-is-python3 python3-setuptools pip mkdocs mkdocs-material \ - # postgres client tools (psql, pg_dump, pg_restore, pg_isready, etc.) + > /etc/apt/sources.list.d/pgdg.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + # essentials for running the workspace in CI + git jq unzip just \ + # vitest spawns workers via node; bun runs vitest itself but the + # worker subprocesses are node. With --no-install-recommends this + # pulls libnode115 but skips npm + nodejs-doc. + nodejs \ + # postgres client tools (psql, pg_dump, pg_restore, pg_isready, ...) postgresql-client-18 \ - # nice to haves - gh fish just fzf curl wget parallel neovim fd-find bat ripgrep rsync \ - # MinIO CLI (binary is `minio-client`, not Midnight Commander's `mc`) - minio-client \ - # uninstall lynx so people don't get a CLI browser that can't load anything when they do `gh auth login` - && apt-get purge -y lynx + # rsync + ssh client for the homepage deploy job + rsync openssh-client \ + && apt-get purge -y gnupg \ + && apt-get autoremove -y \ + && rm -rf /var/lib/apt/lists/* -# so apt doesn't complain about the lack of a dialog-like program -ENV DEBIAN_FRONTEND=readline - -# `fd` is called `fdfind` on debian. Awesome. -RUN ln -s $(which fdfind) /usr/bin/fd +# bun — copied from the official oven/bun image (see BUN_VERSION arg at top). +# `bunx` is a symlink to `bun` in the upstream image; we recreate it here. +COPY --from=bun-src /usr/local/bin/bun /usr/local/bin/bun +RUN ln -sf /usr/local/bin/bun /usr/local/bin/bunx # expose the db CLI globally RUN ln -sf /tachi/typescript/db-cli/src/index.ts /usr/local/bin/tachidb +# --------------------------------------------------------------------------- +# dev: devcontainer image +# --------------------------------------------------------------------------- +FROM base AS dev + +# so apt doesn't complain about the lack of a dialog-like program +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + # shell + editor + admin + fish neovim sudo locales \ + # search / files / network / parallel + ripgrep fd-find bat fzf parallel wget \ + # python stack for docs work (mkdocs is run via Dockerfile.docs in CI, + # but devs build/preview docs locally from here) + python-is-python3 python3-pip mkdocs mkdocs-material \ + # github CLI for `gh auth login`-style flows + gh \ + # MinIO CLI (binary is `minio-client`, not Midnight Commander's `mc`) + minio-client \ + && rm -rf /var/lib/apt/lists/* + +ENV DEBIAN_FRONTEND=readline + +# `fd` is called `fdfind` on debian. Awesome. +RUN ln -s "$(which fdfind)" /usr/bin/fd + # setup locales # https://stackoverflow.com/questions/28405902/how-to-set-the-locale-inside-a-debian-ubuntu-docker-container RUN echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen && locale-gen ENV LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8 -# bun -# TODO(zk): This will just pull a random version off -# the internet, unpinned. There's got to be something -# better... -RUN curl -fsSL https://bun.sh/install | BUN_INSTALL=/usr/local bash - # https://github.com/python-babel/babel/issues/990 # it wouldn't be python without needing absurd global state manipulation to fix # an incoherent error message diff --git a/bun.lock b/bun.lock index d5a9fdf3d..774f528d1 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,6 @@ { "lockfileVersion": 1, - "configVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "tachi", diff --git a/docker/Dockerfile.server b/docker/Dockerfile.server index fea5a4d44..e2ee4bc73 100644 --- a/docker/Dockerfile.server +++ b/docker/Dockerfile.server @@ -17,12 +17,14 @@ # NOTE: bun build --compile is NOT used here because it's # FUCKING BROKEN -FROM oven/bun:alpine AS deps +# Bun version is pinned and kept in sync with Dockerfile.dev (which uses the +# `-debian` variant of the same tag). Bump both together. +FROM oven/bun:1.3.14-alpine AS deps WORKDIR /app COPY . . RUN bun install --frozen-lockfile -FROM oven/bun:alpine AS prod +FROM oven/bun:1.3.14-alpine AS prod ARG VERSION ARG VERSION_DETAIL