Docs menu

Continuous integration

.md

Oak runs CI natively: workflow files, triggers, secrets, caching, and the merge gate.

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

EventFires whenRuns against
branch.createdA branch is pushed for the first timeThe branch head
pushA branch that already exists on the server gets new commits; also a revert landing on a branchThe branch head
mergeA branch is squash-merged onto mainThe new commit on main
manualYou trigger it β€” see Running a workflow by handThe 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 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.

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

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.

Something here wrong or missing? Run oak feedback -m "…" or email [email protected].