> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reasonos.com/llms.txt
> Use this file to discover all available pages before exploring further.

# CI Workflows

> Define CI pipelines in .rbs files and run them with rbs ci — DAG execution over the same build graph, with affected-package detection built in.

CI is built into rbs. Workflows are declared in the RBS language — the same language
as your build files — and executed by the rbs DAG engine: independent jobs run in parallel,
`needs` edges order the rest, and every step benefits from the shared cache. There is
no YAML dialect and no separate runner to install; the `rbs` binary that builds your
workspace also runs its CI.

## Defining a workflow

Declare workflows with `ci_workflow()` in a file named `ci.rbs`. rbs discovers every
`ci.rbs` in the workspace (workflows can also live in `WORKSPACE.rbs`), so a monorepo
can keep each service's pipeline next to its code.

```python theme={null}
ci_workflow(
    name = "main",
    on = {
        "push": {"branches": ["main"]},
        "pull_request": {"branches": ["main"]},
    },
    jobs = {
        "build": {
            "steps": [
                {"name": "Build everything", "run": "rbs build //..."},
            ],
        },
        "test": {
            "needs": ["build"],
            "steps": [
                {"name": "Test everything", "run": "rbs test //..."},
            ],
        },
    },
)
```

Jobs form a DAG: `test` waits for `build`, and jobs without edges between them run
concurrently. A dependency cycle is rejected before anything runs.

### Triggers

The `on` dict declares which events run the workflow:

* **`push`** — filter with `branches`, `branches_ignore`, `tags`, `tags_ignore`,
  `paths`, `paths_ignore`. Glob patterns work (`"v*"`, `"release/**"`). A workflow
  that only declares `tags` runs only on tag pushes; one that only declares
  `branches` runs only on branch pushes.
* **`pull_request`** — filter with `branches` (the target branch), `branches_ignore`,
  `types`, `paths`, `paths_ignore`.
* **`manual`** — runs only when invoked by name or with `--event manual`.
* **`schedule`** — `{"cron": "0 6 * * *"}`. rbs does not run a daemon; a schedule
  trigger matches when an external scheduler invokes `rbs ci run --event schedule`.

Path filters compare against the files changed since the base ref, so a docs-only
push can skip a build workflow entirely.

### Steps

Each job runs its steps in order. A step is one of four kinds:

| Key               | What it does                                                     | Example                                                |
| ----------------- | ---------------------------------------------------------------- | ------------------------------------------------------ |
| `run`             | Runs a shell command (bash by default; override with `shell`)    | `{"run": "rbs infra apply -y -e dev"}`                 |
| `rbs`             | Runs an rbs command                                              | `{"rbs": "build //..."}`                               |
| `target`          | Builds, tests, or covers one label                               | `{"target": "//services/api:test", "command": "test"}` |
| `affected_target` | Runs a target name across every affected package that defines it | `{"affected_target": "test", "command": "test"}`       |

For `target` and `affected_target` steps, `command` selects the rbs verb: `"build"`
(the default), `"test"`, or `"coverage"`.

Steps also accept `name`, `env`, `if`, `working_directory`, `timeout`, and
`continue_on_error` (a failing step with `continue_on_error` set does not fail the
job).

### Environment variables

Steps see your environment plus three layers of declared variables — workflow-level
`env`, then job-level, then step-level, later layers overriding earlier ones. rbs
adds the trigger context automatically:

* `CI_EVENT_TYPE` — `push`, `pull_request`, `manual`, …
* `CI_REF` — the fully-qualified ref (`refs/heads/main`, `refs/tags/v1.2.0`)
* `CI_BRANCH` — the branch name
* `CI_PR_NUMBER` — set for pull-request runs

### Conditions

Jobs and steps take an `if` expression. The evaluated forms are `always()`,
`success()`, `failure()`, `cancelled()`, and the literals `true` / `false`.

<Note>
  An `if` expression the engine does not recognize is treated as true — the job runs.
  Keep conditions to the supported forms.
</Note>

### Matrix jobs

A job with a `matrix` expands into one job per combination:

```python theme={null}
"test": {
    "matrix": {
        "go": ["1.21", "1.22"],
        "os": ["linux", "darwin"],
    },
    "steps": [
        {"name": "Test", "run": "rbs test //..."},
    ],
},
```

This produces four jobs, scheduled in parallel like any other independent jobs. Each
expansion's variable assignment is recorded in the run results and shown by
`rbs ci plan`.

## Running workflows

```bash theme={null}
# Run every workflow matching a push to the current branch
rbs ci run --event push

# Run one workflow by name
rbs ci run main

# Simulate a pull request
rbs ci run --event pull_request --base main --pr 42

# A tag push (release workflows)
rbs ci run --event push --tag v1.2.0
```

With no workflow name, every workflow whose triggers match the event runs. The
trigger context comes from flags first and the workspace's git state otherwise —
`--event`, `--branch`, `--tag`, `--ref` (a fully-qualified ref decides branch vs
tag), `--base`, and `--pr`.

Useful flags on `rbs ci run`:

* `--workers N` — parallel job slots (default 4)
* `--timeout 30m` — overall workflow timeout
* `--fail-fast` — stop at the first failing workflow (default on)
* `--json` — emit run results as JSON
* `--remote host:port` — run the jobs on a remote cluster (see below)

### Inspecting workflows and runs

```bash theme={null}
rbs ci list                 # every workflow, its triggers and job DAG
rbs ci trigger              # the trigger context git currently implies
rbs ci status               # recent runs (newest first)
rbs ci status <run-id>      # one run: jobs, steps, errors
```

Run records persist in the workspace, so `rbs ci status` sees runs from earlier
invocations; the most recent 50 are kept.

### Planning without running

`rbs ci plan` resolves a trigger event into the exact job DAG it would execute and
prints it as JSON on stdout — nothing runs, nothing is written:

```bash theme={null}
rbs ci plan --event push --branch main
rbs ci plan --event pull_request --base main --pr 42
```

Matrix jobs are expanded and `needs` edges are rewritten to the expanded names, so
the output is the DAG that would actually be scheduled. This is the integration
point for external schedulers and dashboards.

## Affected-package detection

For monorepos, rbs can compute which packages the current change actually touches —
directly and through the dependency graph:

```bash theme={null}
rbs ci affected                  # packages affected vs the base ref
rbs ci affected --base main      # explicit base
rbs ci affected --json --verbose
```

Inside a workflow, an `affected_target` step applies this automatically: it finds
every affected package defining the named target and runs just those, instead of the
whole workspace.

```python theme={null}
"quick-check": {
    "steps": [
        {"name": "Test what changed", "affected_target": "test", "command": "test"},
    ],
},
```

## Running CI on a cluster

If you run a remote build cluster (see
[Remote cache & execution](/ci/remote-execution)), `rbs ci run` can fan the
workflow's jobs onto its workers instead of running them locally:

```bash theme={null}
rbs ci run --remote host:8980
# or, with $RBS_REMOTE set, pass the bare flag:
rbs ci run --remote
```

Each job is submitted to the cluster with its `needs` edges preserved as job
dependencies. Workers check out the repository at the triggering commit, stream
their logs back to your terminal, and any `rbs` commands inside the job's steps
automatically use the same cluster — so every job shares one cache.

<Note>
  Fleet CI submits jobs with the `CI_*` context variables, but secret values are never
  placed in job specs — a job spec can sit in the queue, and secrets do not belong
  there. See [the secrets rule](/ci/remote-execution#secrets-never-leave-your-machine).
</Note>
