Command Palette

Search for a command to run…

ZarynZaryn
CI, evals and releases·10 min read·

Get started with CI

A guided path from "what is CI?" to signed, deployed container images: how Zaryn builds every push and pull request in an isolated container, how to write a .zev-ci.json, and the gates that keep main working. Pictures first, every step explained.

Tutorial⏱ About 30 minutes◔ From zero to advanced☰ 15 chapters
You’ll learn
  • What continuous integration is, and what it saves you from
  • How a push becomes a build in a locked-down container on Zaryn
  • Writing a .zev-ci.json: steps, images, network, memory and time
  • Gates that stop a bad merge: tests, evals and advisories
  • Building signed container images and deploying them
You’ll need
  • A repository on Zaryn (see the Quick start if you don't have one)
  • The zev command, signed in
  • Tests you can run with one command (any language)
This tutorial goes from the idea of CI to shipping signed images, one chapter at a time. Already know what CI is? Jump to [Turn it on](#4-turn-it-on) from the roadmap above. ## 1. What CI is ```diagram type: flow title: Every push, checked by a machine loop: on every push and every pull request --- You push | `zev push`: your new commits reach Zaryn | | ⇡ ~ automatically Build and test | In a clean, isolated container, the same way every time | accent | ⚙ ~ seconds to minutes later A verdict | ✓ or ✗ on the commit and the pull request, with the logs | good | ✓ ``` **Continuous integration** (CI) means a machine builds and tests your project every time someone pushes, in a clean environment, and puts the result next to the commit. Nobody has to remember to run the tests, and "it works on my machine" stops being an argument: the build either passes in a clean container or it doesn't. ## 2. What it saves you from ```diagram type: cards title: Without CI, and with it --- 🖥 | "Works on my machine" | A missing file or a local setting hides a broken build | bad 💥 | A broken main | Someone merges, everyone pulls, nothing compiles | bad 🐢 | Slow reviews | Reviewers run the tests by hand, or don't | warn ✅ | A verdict on every change | Each pull request shows whether it builds and passes | good 🛡 | Gates | A change that fails tests, makes a metric worse or uses an unsafe dependency can't merge | good 📦 | Releases from the build | The artifact that passed is the one you ship, signed | good ``` ## 3. How Zaryn runs it ```diagram type: graph title: From a push to a verdict alt: zev push reaches zev-api, which sends a signed webhook to zev-ci; zev-ci clones the repository as the CI bot and runs each step in its own locked-down Docker container, then reports the status, artifacts and SBOM back to zev-api legend: path=The push; path2=The verdict height: 46 --- node you | zev push | 10 22 | laptop | | Your commits, or a pull request node api | Zaryn | 38 22 | cloud | accent | Sends a signed webhook to CI for repositories with CI on node ci | zev-ci | 66 22 | ci | | Clones as the CI bot, then runs each step box Each step | 78 4 20 36 | good node box | a container | 88 22 | lock | good | No network unless asked, read-only root, no capabilities, memory, CPU and time limits link you api | | path link api ci | signed webhook | path link ci box | each step | path link ci api | | hidden step The verdict comes back | Status, logs, artifacts and the SBOM go back to Zaryn, where the repository, the pull request and `zev ci status` show them. link api ci | dim | link ci api | path2 | status ``` Each step runs in a **fresh, locked-down container**: no network unless the step asks for it, a read-only root filesystem, every Linux capability dropped, and memory, CPU, process and time limits. A step can't read the server's credentials or another repository's code, so CI is safe even for code you didn't write. ## 4. Turn it on ```diagram type: flow title: One command --- zev ci enable | In the repository's folder, or Enable CI on its dashboard | accent | ⏻ ~ push First run | zev push: Zaryn starts a build | | ▶ ~ no config yet? Zero-config | zev-ci recognises the project and runs its usual build and tests | good | ✨ ``` ```bash zev ci enable zev push ``` No `.zev-ci.json` yet? CI looks at the repository and runs the usual commands: ```diagram type: cards --- ⚡ | Zig | `build.zig`: installs the latest stable Zig, runs `zig build test` | 🦀 | Rust | `Cargo.toml`: `cargo build --release`, then `cargo test` | 🐹 | Go | `go.mod`: `go build ./...`, then `go test ./...` | 🟩 | Node | `package.json`: `npm ci` (or install), then `npm test` if there's a test script | 🐍 | Python | `requirements.txt` or `pyproject.toml`: installs them and pytest, runs pytest | 🐳 | Dockerfile | Builds a signed container image, even with no other configuration | accent ``` ## 5. Your first pipeline When the defaults aren't what you want, put a `.zev-ci.json` in the repository's root: ```json { "image": "python:3.12-alpine", "timeout_seconds": 300, "steps": [ { "name": "copy-source", "run": "cp -r /mnt/source/. /workspace/" }, { "name": "install", "run": "pip install --target /workspace/.deps -r requirements.txt", "allow_network": true }, { "name": "test", "run": "PYTHONPATH=/workspace/.deps python -m pytest -q" }, { "name": "package", "run": "mkdir -p dist && tar czf dist/app.tar.gz src" } ], "artifacts": ["dist/app.tar.gz"] } ``` ```diagram type: flow title: What that file does --- copy-source | The code, from the read-only `/mnt/source` into `/workspace` | | 📁 ~ then install | Dependencies; this step alone may use the internet | warn | 🌐 ~ then test | pytest: a failure stops the run here | accent | 🧪 ~ then package | Builds `dist/app.tar.gz`, kept as an artifact | good | 📦 ``` Steps run in order and **stop at the first failure**. Commit the file and push: the next run uses it. ## 6. See the result ```bash zev ci status # recent runs, and the error of the latest failure ``` ```diagram type: cards --- 🧾 | The repository | Each commit shows its run; open it for every step's output | accent 🔀 | Pull requests | The check sits next to the review: green, or red with the failing step | ⌨️ | The terminal | `zev ci status` lists recent runs and why the last one failed | 🤖 | AI agents | `zev mcp` gives agents the CI status as a tool | ``` ## 7. Steps, images and the workspace ```diagram type: graph title: One workspace, shared by every step of a run alt: The repository is mounted read-only at /mnt/source; the first step copies it into /workspace, which every later step sees; artifacts are taken from /workspace at the end height: 38 --- node src | /mnt/source | 10 18 | folder | | Your repository, read-only node ws | /workspace | 46 18 | storage | accent | Shared by the steps of one run, removed when it ends node s1 | step 1 | 46 6 | ci | | node s2 | step 2 | 70 6 | ci | | Sees what step 1 left node art | artifacts | 86 30 | file | good | Paths in /workspace kept after a passing run link src ws | copy | path link s1 ws | | dim link s2 ws | | dim link ws art | | path2 ``` - `image` is the default container image; a step can choose its own with `"image": "…"` (a Node step and a Go step in one pipeline). - `run` is a shell command (`sh -c`). Anything a step writes in `/workspace` is there for the next one. - Only `/workspace` and a small `/tmp` are writable; everything else is read-only. ## 8. Network, memory and time ```diagram type: cards title: What a step can ask for --- 🌐 | allow_network | Internet access for this step only, for installing dependencies | warn 🎯 | allowed_hosts | Internet, but only to these hosts | good 🧠 | memory | `"2g"`: more memory (default 512m, no swap) | ⏱ | timeout_seconds | Per step or for the pipeline, 1 to 21600 (default 120) | ``` Your plan sets the ceiling, whatever the file asks for: ```diagram type: stats --- 400 | CI minutes a month on Free | 2 CPUs, 4 GB, 30 min a step | 4 | 10,000 | CI minutes a month on Premium | 4 CPUs, 8 GB, 2 h a step | 100 | accent ``` A run past the month's minutes stops at once and says so. ## 9. Keep what you built `"artifacts": ["dist/app.tar.gz"]` keeps those files after a **passing** run: they're pinned to IPFS, so each has a content address anyone can download and check, tied to the commit that produced it. A failing run keeps nothing. ```diagram type: flow title: An artifact's way out --- Built | `dist/app.tar.gz` in /workspace | | 📦 ~ the run passed Pinned | On IPFS: a CID made from its bytes | accent | 📌 ~ shown with the run Downloadable | From the run, by anyone who can read the repository | good | ⇣ ``` ## 10. Skip work already done ```diagram type: flow title: Same inputs, same result, no work or: otherwise --- Inputs | The source tree's hash, the image and the exact command | | 🧩 ~ succeeded before? Skip | The stored result is used: 0.53 s became 0.01 s | good | ⏭ ? Run it | And keep the result only if it passes ``` When the CI server has memoization on, a step whose source, image and command already succeeded is served from the cache. Failures are never cached, so a flaky test can't hide. Because a skipped step leaves nothing in the workspace, it suits pipelines whose steps don't depend on each other. [How it works](/docs/en/memoized-ci). ## 11. Stop a change that makes things worse Tests pass or fail; numbers drift. **Evals** keep each commit's numbers (build time, binary size, benchmarks, coverage, model accuracy) and block a pull request that makes one worse than its tolerance: ```bash zev eval init # once: zev finds what to run and measure zev eval run # compares this commit with main; exits 1 on a regression ``` ```diagram type: cards --- = | build_time: held | +1.1%, inside its 10% tolerance | ✓ | tests.passed: better | 212 → 214 | good ✗ | binary: worse | +12.2%, past 1%: the pull request is blocked | bad ``` Run `zev eval run` as a step, or from any CI. Everything about evals: [Evals and lineage](/docs/en/evals-and-lineage). ## 12. Don't build on something unsafe ```json { "advisories": { "fail_on": "high" }, "steps": [ … ] } ``` ```diagram type: flow title: Checked before the first step or: otherwise --- Ask Zaryn | Does an unresolved advisory at or above "high" reach this repository through its lineage? | | 🔎 ~ none Run | The steps start as usual | good | ▶ ? One does | The run fails before any step, so nothing built from it is published | bad ``` If Zaryn can't answer, the run fails rather than passing unchecked. [Advisories](/docs/en/advisories). ## 13. Build signed container images ```json { "steps": [{ "name": "test", "run": "cp -r /mnt/source/. . && make test" }], "images": [{}] } ``` ```diagram type: flow title: From a passing run to an image anyone can verify --- Build | Rootless BuildKit, in a throwaway container with no credentials | | 🏗 ~ then SBOM | Every package inside, listed by syft, offline | | 📋 ~ then Push | To Zaryn's registry, as the CI bot | accent | ⇡ ~ then Attest | Zaryn signs SLSA provenance and adds the build to the transparency log | good | ✍ ``` `{}` builds the repository's `Dockerfile` as `<owner>/<repo>`. Add a `name`, `context`, `tags` (`{branch}`, `{sha}`, `{pr}`) or several `platforms` (Premium) when you need them. See [Containers](/docs/en/packages-containers). ## 14. Deploy it ```diagram type: graph title: main deploys, pull requests preview alt: A passing run on main deploys the signed image to the prod target; each pull request gets its own preview, removed when it merges height: 36 --- node ci | a passing run | 12 18 | ci | accent | Only digests CI built and signed are deployed node prod | prod | 60 8 | server | good | `"deploy": {"target": "prod", "app": "web", "branches": ["main"]}` node prev | pr-42.preview… | 60 30 | server | | `"preview"`: each pull request as its own app, removed on merge link ci prod | main | path link ci prev | pull request | path2 ``` An image can deploy itself to your own servers running Zaryn's agent: `main` to production, each pull request to a preview address. Only images CI built and signed are deployed, and only to targets that accept CI deploys. ## 15. Limits, and when it doesn't start ```diagram type: cards title: Quick fixes --- 🔌 | A push starts nothing | CI isn't on: `zev ci enable`, or Enable CI on the dashboard | warn 🌐 | Installs fail | The step has no network: add `"allow_network": true` (or `allowed_hosts`) | warn ⏱ | Killed after 2 minutes | The default step timeout: raise `timeout_seconds` | 🧠 | Killed while building | Out of memory: raise `memory`, within your plan | 📉 | "minutes used up" | The month's CI minutes are spent: next month, or Premium | ``` You've gone from what CI is to signed, deployed images. Next: [Memoized CI](/docs/en/memoized-ci) in depth, [Evals and lineage](/docs/en/evals-and-lineage) for every kind of metric, and [Advisories](/docs/en/advisories).
You finished the tutorial

Where to go next