From 6a86840cd9aaa94fa35cc559d8f570e3764bb0d1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 3 Nov 2026 22:36:41 +0000 Subject: [PATCH] Add Docker-first build, test, and lint workflows Make Docker the preferred local path so the host only needs Docker: multi-stage Dockerfile (builder/test/lint/runtime), Makefile wrappers, compose helpers, .dockerignore, docs, and a PR CI workflow. Co-authored-by: me --- .dockerignore | 23 +++++++++++++ .github/workflows/ci.yml | 39 ++++++++++++++++++++++ AGENTS.md | 13 ++++++-- Dockerfile | 72 +++++++++++++++++++++++++++++++++++++--- Makefile | 61 ++++++++++++++++++++++++++++++---- README.md | 65 +++++++++++++++++++++++++----------- docker-compose.yml | 43 ++++++++++++++++++++++++ 7 files changed, 283 insertions(+), 33 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/ci.yml create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e8fc0ba --- /dev/null +++ b/.dockerignore @@ -0,0 +1,23 @@ +# Local secrets / env +.env +.env.* +!.env.sample + +# Toolchain + build outputs (downloaded/built inside the image) +.cosmocc/ +*.o +king +test_king +*.com.dbg +*.aarch64.elf +.aarch64/ +dist/ + +# Git metadata (submodules must already be checked out into vendor/) +.git/ +.gitignore +.gitmodules + +# CI / compose noise (not needed inside build context) +.github/ +docker-compose.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..987ad58 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + docker-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and test (Docker) + run: make docker-test + + - name: Build runtime image + run: make docker-image + + docker-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Lint (Docker / LLVM 21) + run: make docker-lint diff --git a/AGENTS.md b/AGENTS.md index 96071ce..c922b82 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,19 +4,28 @@ This file provides guidance to AI coding agents when working with code in this r ## Build +Prefer Docker so the host only needs Docker (no local cosmocc / LLVM): + ```bash -make +git submodule update --init # vendor/mbedtls required in the build context +make docker-test # compile + unit tests in Docker +make docker-king # extract ./king APE via Docker +make docker-lint # clang-format + clang-tidy (LLVM 21) in Docker +make docker-image # scratch runtime image tagged king:local ``` -The Makefile auto-downloads `cosmocc` (Cosmopolitan C compiler) on first run and builds mbedtls from the vendored submodule. The output is `king`, an Actually Portable Executable. +Native (downloads cosmocc onto the host): ```bash +make make test # build and run tests make clean # remove build artifacts make mbedclean # also clean mbedtls make distclean # also remove downloaded cosmocc ``` +The Makefile auto-downloads `cosmocc` for native builds and builds mbedtls from the vendored submodule. The output is `king`, an Actually Portable Executable. Dockerfile stages: `builder`, `test`, `lint`, `runtime`. + ## Run ```bash diff --git a/Dockerfile b/Dockerfile index 973f034..7dd6c6f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,78 @@ -FROM ubuntu:24.04 AS builder +# Multi-stage Cosmopolitan build for the king Discord bot. +# Host requirement: Docker only (no cosmocc / clang on the Mac). +# +# Targets: +# builder — compile king + test_king with cosmocc +# test — run unit tests (fails the build on test failure) +# lint — clang-format + clang-tidy (LLVM 21, matches CI) +# runtime — scratch image with assimilated APE (default) +# +# Prefer Makefile wrappers: `make docker-test`, `make docker-king`, etc. + +# ── shared base with build deps ────────────────────────────────── +FROM ubuntu:24.04 AS base RUN apt-get update && apt-get install -y --no-install-recommends \ - make curl unzip ca-certificates git \ + make curl unzip ca-certificates \ && rm -rf /var/lib/apt/lists/* WORKDIR /src + +# ── compile with cosmocc ──────────────────────────────────────── +FROM base AS builder + COPY . . -RUN git submodule update --init && make && .cosmocc/current/bin/assimilate /src/king +# vendor/mbedtls comes from `git submodule update --init` on the host +# (or `git clone --recursive`). Keep the image free of a .git checkout. +RUN if [ ! -f vendor/mbedtls/include/mbedtls/ssl.h ]; then \ + echo "error: vendor/mbedtls missing. Run: git submodule update --init" >&2; \ + exit 1; \ + fi + +# Cache cosmocc across builds when BuildKit is available. Binaries are +# copied to /artifacts because cache mounts are not part of the image layer. +RUN --mount=type=cache,target=/src/.cosmocc,sharing=locked \ + make \ + && make test_king \ + && mkdir -p /artifacts \ + && cp -a king test_king /artifacts/ \ + && cp -a king /artifacts/king.assimilated \ + && .cosmocc/current/bin/assimilate /artifacts/king.assimilated + +# ── unit tests ─────────────────────────────────────────────────── +FROM builder AS test + +RUN /artifacts/test_king + +# ── lint (same LLVM major as .github/workflows) ────────────────── +FROM ubuntu:24.04 AS lint + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl gnupg make \ + && curl -fsSL https://apt.llvm.org/llvm-snapshot.gpg.key \ + | gpg --dearmor -o /usr/share/keyrings/llvm.gpg \ + && echo "deb [signed-by=/usr/share/keyrings/llvm.gpg] https://apt.llvm.org/noble/ llvm-toolchain-noble-21 main" \ + > /etc/apt/sources.list.d/llvm.list \ + && apt-get update && apt-get install -y --no-install-recommends \ + clang-format-21 clang-tidy-21 \ + && ln -sf /usr/bin/clang-format-21 /usr/bin/clang-format \ + && ln -sf /usr/bin/clang-tidy-21 /usr/bin/clang-tidy \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /src +COPY . . + +RUN if [ ! -f vendor/mbedtls/include/mbedtls/ssl.h ]; then \ + echo "error: vendor/mbedtls missing. Run: git submodule update --init" >&2; \ + exit 1; \ + fi + +RUN make lint -FROM scratch +# ── runtime image (default) ────────────────────────────────────── +FROM scratch AS runtime -COPY --from=builder /src/king /king +COPY --from=builder /artifacts/king.assimilated /king ENTRYPOINT ["/king"] diff --git a/Makefile b/Makefile index 79490ae..cde3c1d 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,37 @@ # Cosmopolitan build for King Discord bot # Produces an Actually Portable Executable (APE) # -# Usage: -# make — builds the 'king' APE binary (auto-downloads cosmocc) +# Usage (Docker-first — preferred on macOS; only Docker needed on the host): +# make docker-test — build + run unit tests in Docker +# make docker-lint — clang-format + clang-tidy in Docker (LLVM 21) +# make docker-king — write ./king APE binary via Docker (no host cosmocc) +# make docker-image — build runtime scratch image tagged king:local +# make docker-run — run king:local with .env / env vars +# +# Native (downloads cosmocc onto the host): +# make — builds the 'king' APE binary # make test — builds and runs tests # make lint — runs clang-format and clang-tidy checks # make clean — removes build artifacts # make distclean — also removes downloaded cosmocc toolchain -# ── cosmocc toolchain (auto-downloaded) ────────────────────────── +# ── Docker-first wrappers (no host cosmocc / clang) ────────────── +DOCKER ?= docker +DOCKER_IMAGE ?= king +export DOCKER_BUILDKIT ?= 1 + +# Skip cosmocc download for Docker / lint / clean goals +DOCKER_GOALS = docker-test docker-lint docker-king docker-image docker-run docker-builder +SKIP_COSMOCC_GOALS = lint clean distclean mbedclean $(DOCKER_GOALS) + +# ── cosmocc toolchain (auto-downloaded for native builds) ──────── COSMOCC_VERSION = 3.9.2 COSMOCC_SHA256 = f4ff13af65fcd309f3f1cfd04275996fb7f72a4897726628a8c9cf732e850193 COSMOCC_DIR = .cosmocc/$(COSMOCC_VERSION) COSMOCC_BIN = $(COSMOCC_DIR)/bin -# Download cosmocc if not already present (skip for lint/clean targets) -ifneq ($(filter-out lint clean distclean mbedclean,$(MAKECMDGOALS)),) +# Download cosmocc if not already present (skip for lint/clean/docker targets) +ifneq ($(filter-out $(SKIP_COSMOCC_GOALS),$(MAKECMDGOALS)),) DOWNLOAD := $(shell scripts/download-cosmocc.sh $(COSMOCC_DIR) $(COSMOCC_VERSION) $(COSMOCC_SHA256)) endif ifeq ($(MAKECMDGOALS),) @@ -90,4 +106,37 @@ mbedclean: distclean: clean mbedclean rm -rf .cosmocc -.PHONY: all test lint clean mbedclean distclean +# ── Docker targets ─────────────────────────────────────────────── +# All compile/test/lint work happens inside the image. The host only +# needs Docker; cosmocc and LLVM are not installed locally. + +docker-builder: + $(DOCKER) build --target builder -t $(DOCKER_IMAGE):builder . + +docker-test: + $(DOCKER) build --target test -t $(DOCKER_IMAGE):test . + +docker-lint: + $(DOCKER) build --target lint -t $(DOCKER_IMAGE):lint . + +# Extract the portable APE (pre-assimilate) to ./king on the host. +docker-king: docker-builder + @cid=$$($(DOCKER) create $(DOCKER_IMAGE):builder); \ + $(DOCKER) cp $$cid:/artifacts/king ./king; \ + $(DOCKER) rm $$cid >/dev/null; \ + echo "wrote ./king" + +docker-image: + $(DOCKER) build --target runtime -t $(DOCKER_IMAGE):local . + +docker-run: docker-image + @if [ -f .env ]; then \ + $(DOCKER) run --rm --env-file .env $(DOCKER_IMAGE):local; \ + else \ + $(DOCKER) run --rm \ + -e DISCORD_TOKEN -e DISCORD_REPLY -e DISCORD_REACTION -e LOG_LEVEL \ + $(DOCKER_IMAGE):local; \ + fi + +.PHONY: all test lint clean mbedclean distclean \ + docker-builder docker-test docker-lint docker-king docker-image docker-run diff --git a/README.md b/README.md index e6841e8..eaf89f0 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,40 @@ A tiny discord bot written in C. say "king" in a channel and it reacts with 👑. -Built with [Cosmopolitan Libc](https://justine.lol/cosmopolitan/), so `make` spits out a single binary that runs on Linux, macOS, Windows, FreeBSD, OpenBSD, and NetBSD. No Docker, no runtime deps. +Built with [Cosmopolitan Libc](https://justine.lol/cosmopolitan/), so the build produces a single Actually Portable Executable that runs on Linux, macOS, Windows, FreeBSD, OpenBSD, and NetBSD. -## Build +## Build (Docker-first) + +Only **Docker** is required on the host — no cosmocc, clang, or other toolchains. ```bash git clone --recursive https://github.com/SteveParson/king.git +cd king +# if you cloned without --recursive: +# git submodule update --init + +make docker-test # compile + run unit tests in Docker +make docker-king # write ./king to the host via Docker +make docker-image # build scratch runtime image (king:local) +``` + +| Target | What it does | +|--------|----------------| +| `make docker-test` | Build and run unit tests inside Docker | +| `make docker-lint` | `clang-format` + `clang-tidy` (LLVM 21) inside Docker | +| `make docker-king` | Produce `./king` APE on the host (no local cosmocc) | +| `make docker-image` | Build `king:local` scratch image | +| `make docker-run` | Run `king:local` with `.env` or exported env vars | + +`docker compose build test|lint|king` works the same way if you prefer Compose. + +### Native build (optional) + +If you want cosmocc on the machine itself: + +```bash make +make test ``` ## Run @@ -18,7 +45,18 @@ export DISCORD_TOKEN='' ./king ``` -Or copy `.env.sample` to `.env`, fill in your token, and source it before running. +Or copy `.env.sample` to `.env`, fill in your token, and source it before running — or use Docker: + +```bash +make docker-run +# equivalent: docker run --rm --env-file .env king:local +``` + +A pre-built image is also published to the GitHub Container Registry: + +```bash +docker run -e DISCORD_TOKEN='' ghcr.io/steveparson/king:latest +``` You need **Message Content Intent** enabled in the [Discord Developer Portal](https://discord.com/developers/applications). @@ -29,28 +67,15 @@ You need **Message Content Intent** enabled in the [Discord Developer Portal](ht | `DISCORD_REACTION` | `👑` | Reaction emoji | | `LOG_LEVEL` | `info` | `error`, `info`, or `debug` | -## Docker - -A pre-built image is published to the GitHub Container Registry: - -```bash -docker run -e DISCORD_TOKEN='' ghcr.io/steveparson/king:latest -``` - -Or using a `.env` file: +## Test / lint ```bash -cp .env.sample .env -# edit .env with your token -docker run --env-file .env ghcr.io/steveparson/king:latest +make docker-test +make docker-lint ``` -## Test - -Run `make test` to run the tests. - ## How it works -~1000 lines of C, Connects to the Discord Gateway over WebSocket/TLS (mbedtls, vendored), listens for messages, checks for the word "king" (case-insensitive, whole-word match), and fires off a REST reply + reaction. Heartbeats, reconnects, the usual Gateway lifecycle stuff. +~1000 lines of C. Connects to the Discord Gateway over WebSocket/TLS (mbedtls, vendored), listens for messages, checks for the word "king" (case-insensitive, whole-word match), and fires off a REST reply + reaction. Heartbeats, reconnects, the usual Gateway lifecycle stuff. Things we cook in this repo might not be very baked, but from our bit of testing, it 'works'. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c9db111 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +# Docker-first workflows for king. Requires Docker only on the host. +# Submodules must be initialized: `git submodule update --init` +# +# docker compose build test # compile + run unit tests +# docker compose build lint # clang-format + clang-tidy +# docker compose build king # runtime scratch image +# docker compose run --rm king # run bot (needs .env) + +services: + test: + build: + context: . + target: test + image: king:test + profiles: ["build"] + + lint: + build: + context: . + target: lint + image: king:lint + profiles: ["build"] + + builder: + build: + context: . + target: builder + image: king:builder + profiles: ["build"] + + king: + build: + context: . + target: runtime + image: king:local + env_file: + - path: .env + required: false + environment: + DISCORD_TOKEN: ${DISCORD_TOKEN:-} + DISCORD_REPLY: ${DISCORD_REPLY:-} + DISCORD_REACTION: ${DISCORD_REACTION:-} + LOG_LEVEL: ${LOG_LEVEL:-info}