# CI > rrlm docs: [README](../README.md) | [Design](DESIGN.md) | CI (this page) | [Benchmarks and findings](FINDINGS.md) | [Local serving](LOCAL_SERVING.md) | [Pi integration](../pi/README.md) rrlm's canonical CI gate is a [Dagger](https://dagger.io) module. The pipeline lives in the repo (`dagger.json` plus `dagger/`) and runs the same offline test suite a developer runs locally; any provider (or any laptop) runs it with a single command: `dagger call ci`. GitHub Actions (`.github/workflows/ci.yml`) runs the same gate *contract* natively - `uv sync --frozen`, `make lint`, `make cov` - instead of invoking Dagger, because a fresh runner's cold Dagger cache re-downloads the entire dependency tree inside the engine on every run, while setup-uv's wheel cache keeps the native path to a few minutes. Both paths execute the make-documented commands below, so they cannot drift in what they check. ## What it runs The module mirrors the make contract exactly, so CI and local runs cannot drift: | Dagger function | Equivalent make target | What it does | | --------------- | ---------------------- | ---------------------------------------------------- | | `lint` | `make lint` | `ruff check src/ tests/ examples/` | | `test` | `make test` | `pytest -m "not real"` (offline unit + integration + e2e) | | `cov` | `make cov` | the same suite with `--cov-fail-under=80` | | `ci` | (the gate) | `lint`, then `cov` | The `real` marker (live-model tests) stays excluded. Tests run fully offline: the integration and e2e tests start a local OpenAI-compatible stub server as a subprocess (no network, no API keys, no GPU). The project is installed via `uv sync` so the e2e tests can invoke the `rrlm-solve` console script. ## Why repeat runs are fast (and first runs are not) The container environment is built in two layers. The dependency layer sees only `pyproject.toml`, `uv.lock`, and `.python-version` and runs `uv sync --frozen --no-install-project`; Dagger content-caches it, so editing source or tests never re-resolves or re-downloads dependencies - only a lockfile change does. The project layer then mounts the full source and finishes the sync (installing rrlm itself, which is cheap). Underneath both, the uv cache volume means even a lockfile change re-downloads only genuinely new wheels. Two operational consequences: * **Keep the engine warm.** The layer cache and the uv cache volume live in the Dagger engine container (`docker ps` shows `dagger-engine-*`). The first call after a fresh engine pays the full download once; every later call reuses it. Do not `docker rm` the engine between runs. * **Ephemeral runners always pay the cold cost**, which is why the GitHub Actions workflow runs the make contract natively with setup-uv's wheel cache instead of `dagger call ci`. If a Dagger-run CI is ever wanted on ephemeral runners, wire up a persistent remote cache (Dagger Cloud or a self-hosted engine) rather than accepting the per-run download. ## Prerequisites - A container runtime. Docker works; Dagger uses it to run its engine. - The Dagger CLI. Install once: ```bash curl -fsSL https://dl.dagger.io/dagger/install.sh | sh # into ./bin or ~/.local/bin # or brew install dagger/tap/dagger ``` Full docs and other install methods: https://docs.dagger.io. ## Run it ```bash make ci # wraps `dagger call ci` # or directly: dagger call ci dagger call lint dagger call test dagger call cov dagger functions # list the available functions ``` A green run ends with the coverage table and: ``` Required test coverage of 80% reached. Total coverage: 96.37% ``` ## How any CI provider uses it There is nothing provider-specific to maintain. A provider job needs only a container runtime and the Dagger CLI, then: ```bash dagger call ci ``` That is the entire integration. The same command runs identically on a laptop, in GitLab CI, CircleCI, Jenkins, Buildkite, or a fresh VM. That portability is the reason CI lives here instead of in a `.github/workflows/` file. ## Image, cache, and network notes - Base image: `ghcr.io/astral-sh/uv:python3.13-bookworm-slim` (uv + Python 3.13, matching `.python-version`). The first run pulls it; later runs reuse it. - The uv cache is kept on a Dagger cache volume (`rrlm-uv-cache`), so repeat runs skip re-downloading dependencies. - Network is used only on a cold cache, for `uv sync`. The test suite itself runs offline. Expect the first run to be slower (image pull + dependency download + Dagger engine warmup); subsequent runs are fast. - Generated artifacts (`dagger/sdk/`, `dagger/.venv/`) are git-ignored and regenerated by `dagger develop`; you do not commit them.