# Continuous integration

Oak runs CI itself — there's no external service to connect. Put a workflow file in the repo, and Oak runs it on hosted Linux runners when branches are pushed and merged, shows the result on every branch, and refuses to merge a branch whose CI is red.

## Your first workflow

Create `.oak/workflows/ci.yml`:

```yaml
on: [push, branch.created, merge]
jobs:
  test:
    steps:
      - name: build
        run: cargo build --locked
      - name: test
        run: cargo test --locked
```

Commit and push it:

```bash
oak commit && oak push
```

That push starts a run. Watch it from the CLI, or on the branch's page on oak.space, where a status chip links to live logs:

```bash
oak ci status          # the verdict for your branch head
oak ci runs            # recent runs in this repo
oak ci logs <run-id>   # a run's step-by-step output
```

> **List `branch.created` next to `push`.** The *first* push of a new branch fires `branch.created`, not `push`. A workflow with `on: [push]` alone silently skips every branch's opening push. Almost every workflow wants `on: [push, branch.created]`, plus `merge` to also run on `main` after merging.

## When workflows run

| Event | Fires when | Runs against |
|---|---|---|
| `branch.created` | A branch is pushed for the first time | The branch head |
| `push` | A branch that already exists on the server gets new commits; also a revert landing on a branch | The branch head |
| `merge` | A branch is squash-merged onto `main` | The new commit on `main` |
| `manual` | You trigger it — see [Running a workflow by hand](#running-a-workflow-by-hand) | The head of the branch you name |

Workflow files are read **from the commit being tested**, so a change to a workflow takes effect on the push that makes it. Every file in `.oak/workflows/` whose `on:` matches the event starts its own run.

The full file format — every key, and what's deliberately not supported — is in the [Workflow file reference](/docs/ci-workflows).

## What a run looks like

Each run gets a fresh Linux container with your repo checked out at the exact commit, in `/workspace`. Steps run in order; the first failing step fails the run and the rest are skipped. The container is destroyed afterwards — only paths you declare in `cache:` survive to the next run.

The runner is Linux x86_64 with 4 vCPU and 12 GB of RAM, running as root, with Python, Node, Bun, Rust, PostgreSQL and common build tools pre-installed and outbound internet access. [The CI runner environment](/docs/ci-runners) has the full list, and how to reach your own servers from a job.

## Secrets

Values that mustn't live in the repo — deploy keys, API tokens — go in **CI secrets**, set per repo under **Settings → CI** on oak.space (you need write access).

- Every secret is exported as an environment variable into **every step** of every workflow in the repo.
- Values are encrypted at rest and **redacted from logs** — including each line of a multi-line value such as an SSH key.
- Names are letters, digits, and `_`, can't start with a digit, and can't start with `OAK_CI_`. Values can be up to 64 KiB, and multi-line values are passed through exactly.
- Once saved, a secret's value can't be read back — only replaced or deleted.

There's no per-workflow scoping: a secret is available to every workflow in the repo, on every branch. Anyone who can push a branch can write a workflow that reads it, so treat repo write access as access to its secrets.

Secrets can also be managed over the API (`GET`/`PUT`/`DELETE /api/{owner}/{name}/ci/secrets`) — see [the API reference](/docs/api#ci).

## Caching

A job can declare paths to keep between runs:

```yaml
jobs:
  test:
    cache:
      key: cargo
      paths:
        - ~/.cargo/registry
        - target
    steps:
      - run: cargo test
```

The cache is restored before the first step and saved after the last — **only if every step succeeded**. Caches are scoped per branch: a run restores its own branch's cache, falling back to `main`'s, and only ever saves to its own branch's — so a feature branch can't poison `main`'s cache. The newest save wins. Each repo can keep up to 10 GiB of cache; the least recently used entries are evicted past that. Details in the [workflow reference](/docs/ci-workflows#cache).

## The merge gate

CI decides whether a branch can land. A merge onto `main` is **refused while the branch head's CI is failing or still running** — the latest run of every workflow for the head commit must have succeeded. A head with no runs isn't gated.

```bash
oak ci status          # exit 0 = passed, 1 = failed (or no runs), 3 = still running
oak merge --wait       # wait for CI to finish, then merge if it passed
oak merge --force      # override, deliberately, after reading the failure
```

The gate applies to every way of merging — CLI, web, batch merge, and API (where it's HTTP 412, overridden with `?force=1`). Forced merges are written to the organization's audit log. More in [Merging and conflicts](/docs/merging#the-ci-merge-gate).

## Watching and managing runs

**In the browser:** there's no separate CI page. Each branch row on the **Branches** page wears a status chip for its head, each branch and commit page shows a pill per workflow with its duration, and every chip opens the run's page — jobs, steps, and logs that stream live while it runs.

**From the CLI:**

```bash
oak ci runs --limit 50                    # recent runs: id, workflow, branch, commit, status, duration
oak ci status --json                      # gate verdict for the current head
oak ci status --run 1204                  # one specific run
oak ci wait --current --timeout 1800      # block until the current head's runs finish
oak ci wait 1204 1205 --progress          # …or specific runs, printing step transitions
oak ci logs 1204 --failed                 # only the failing steps' output
oak ci logs 1204 --summary                # step metadata only, no log output
oak ci rerun 1204                         # re-run at the same commit (for infrastructure flakes)
oak ci cancel 1204 --commit <full-hash>   # cancel a push or merge run
oak ci cancel --superseded --yes          # cancel this branch's in-flight runs for commits that are no longer its head
```

`oak ci rerun` is for failures that weren't the code's fault — a network blip, a flaky host. A code fix needs a new commit.

## Running a workflow by hand

```bash
oak ci trigger --workflow ci --expected-commit <full-hash> --idempotency-key retry-1
```

`oak ci trigger` starts a run at your branch's head — but only if the head is still exactly `--expected-commit`, so you never test something you didn't mean to. Re-sending the same `--idempotency-key` returns the original run instead of starting another.

Over the API:

```bash
curl -X POST https://oak.space/api/acme/web/ci/runs \
  -H "Authorization: Bearer $OAK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"workflow": "ci", "branch": "main"}'
```

> **Always name the workflow.** A manual trigger with no `workflow` runs **every** workflow at that head — including, say, a deploy workflow — whether or not it lists `manual` in `on:`.

## Limits

While Oak is in beta every organization gets **3,000 CI minutes a month**, **5 concurrent runs** (extra runs queue and start as slots free), and a **two-hour wall-clock limit per run**.

Minutes are wall-clock time rounded up to the minute, counted per calendar month (UTC) across the organization. Going over the monthly allowance **never blocks a run** — your organization's settings page shows usage against it. Runs beyond the concurrency limit wait in `queued` and start in order as slots free up. A run that hits the time limit ends as `timed_out`. See [Plans and limits](/docs/limits).

## Bring your own CI

To use an external CI system instead, have it listen to Oak's [webhooks](/docs/webhooks) (`push`, `branch.created`, `merge`), then fetch the code with `oak clone --branch <name>` using an [API key](/docs/api-keys). External systems can't yet report status back into Oak's merge gate.
