# 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.

Source: https://zaryn.xyz/docs/en/get-started-ci

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).
