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:
on: [push, branch.created, merge]
jobs:
test:
steps:
- name: build
run: cargo build --locked
- name: test
run: cargo test --locked
Commit and push it:
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:
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.creatednext topush. The first push of a new branch firesbranch.created, notpush. A workflow withon: [push]alone silently skips every branch's opening push. Almost every workflow wantson: [push, branch.created], plusmergeto also run onmainafter 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 | 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.
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 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 withOAK_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.
Caching#
A job can declare paths to keep between runs:
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.
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.
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.
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:
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#
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:
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
workflowruns every workflow at that head β including, say, a deploy workflow β whether or not it listsmanualinon:.
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.
Bring your own CI#
To use an external CI system instead, have it listen to Oak's webhooks (push, branch.created, merge), then fetch the code with oak clone --branch <name> using an API key. External systems can't yet report status back into Oak's merge gate.