# Estelle CI

> Use Estelle CI to configure workflows, run builds and tests in Silos, read step evidence and logs, and require meaningful GitHub checks.

Estelle CI runs build and test commands from a workflow in your repository. Each run executes in an isolated Silo and reports step results, durations, and logs in the dashboard, with GitHub checks on the commit. Its workflow format is a deliberately supported subset of GitHub Actions; unsupported configuration is refused by file, key, and line rather than silently skipped.

![The Estelle CI page before a run, with setup instructions and an example workflow](https://docs.fatelabs.ca/docs/shots/ci.webp)

## When to use Estelle CI

Use Estelle CI for repeatable build and test evidence on pushes and same-repository pull requests, without maintaining runners. It can install Node or Python toolchains and provide one PostgreSQL service per job.

Estelle CI needs the Estelle GitHub App on the repository. It complements [Prism](https://docs.fatelabs.ca/prism), which reviews code, and [Twice](https://docs.fatelabs.ca/twice), which tests user journeys in a browser.

## Turn CI on with a pull request

### Choose the repository and test command

Open **CI**, select a repository, and optionally enter a command such as `npm test`. Choose **Turn on Estelle CI**. You can start the same flow from the repository's row in **Repositories**.

### Review the setup pull request

Estelle opens or reuses a pull request from `estelle/ci/enable` that adds `.estelle/ci.yml`. It does not push the file to your default branch. Review the commands before merging.

### Inspect the first run

CI runs on the setup pull request. Open each check's **Details** link to inspect its commit, workflow, steps, and output. Fix the workflow if its commands do not install and test the app correctly.

### Merge the workflow

Merge the setup pull request when its checks and file are correct. Later commits containing `.estelle/ci.yml` run CI on pushes and matching pull requests.

You can also commit `.estelle/ci.yml` directly. That file takes precedence over workflow auto-detection and `.github/workflows` files.

## Write a workflow

This example runs lint before tests and gives the test job a PostgreSQL database:

```yaml
name: tests
on:
  push:
    branches: [main, "release/**"]
  pull_request:
env:
  NODE_ENV: test
jobs:
  lint:
    name: Lint
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci
      - run: npm run lint
  test:
    name: Test
    needs: lint
    timeout-minutes: 20
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: app
          POSTGRES_PASSWORD: app
          POSTGRES_DB: app_test
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci
      - name: Unit and integration tests
        env:
          DATABASE_URL: postgres://app:app@127.0.0.1:5432/app_test
        run: npm test
```

Jobs linked by `needs` run serially in one Silo and publish one check. Independent job groups run in parallel, each in its own Silo with its own check. The first failing step stops its group; later steps are marked not run.

## Supported workflow syntax

| Level | Supported keys |
|---|---|
| Top level | `name`, `on`, `env`, `jobs`, `permissions` |
| `on` | `push`, `pull_request`, and `workflow_dispatch` without `inputs`; `push` and `pull_request` accept `branches` |
| Job | `name`, `needs`, `env`, `runs-on`, `timeout-minutes` from 1 to 30, `services`, `steps` |
| PostgreSQL service | One per job: `image`, `env`, `ports`, and `options` containing only `--health-*` flags |
| Step | Exactly one of `run` or `uses`, plus `name`, `env`, `working-directory`, `timeout-minutes` from 1 to 15, and `with` for a supported action |

Branch patterns use `*` within a path segment and `**` across segments. For a pull request, a branch filter applies to its base branch.

### Actions and toolchains

Pin actions as `owner/name@ref`. These are supported:

- `actions/checkout`, accepted without another checkout step because the run already checks out the commit;
- `actions/setup-node` with `node-version` 18, 20, 22, or 24;
- `actions/setup-python` with `python-version` from 3.8 to 3.14.

Use a plain version such as `20`, `3.x`, or `3.12`. Ranges and aliases such as `lts/*` are refused. The run verifies the installed version before continuing.

### PostgreSQL

Use `postgres:<major>` or `pgvector/pgvector:<x.y.z>-pg<major>` with PostgreSQL majors 13 to 18. `POSTGRES_PASSWORD` is required; `POSTGRES_USER` and `POSTGRES_DB` are optional. The service listens on `127.0.0.1:5432` inside the job's Silo. Other service types are refused.

### Refused configuration

Estelle CI refuses `${{ }}` expressions, `if`, `strategy` and `matrix`, `container`, `concurrency`, `schedule`, reusable workflows, unsupported actions, and keys outside the tables above. Use a scheduled [Automation](https://docs.fatelabs.ca/automations) instead of `schedule`.

`permissions` is accepted, but steps receive no GitHub token. `runs-on` is recorded and does not select a different machine image.

Each step runs in bash with `-eo pipefail`. Steps receive `CI`, `GITHUB_WORKSPACE`, `RUNNER_TEMP`, `GITHUB_SHA`, `GITHUB_REPOSITORY`, and `ESTELLE_CI`. They do not receive `GITHUB_ACTIONS`, `GITHUB_EVENT_NAME`, `GITHUB_REF_NAME`, `GITHUB_OUTPUT`, or `GITHUB_ENV`.

## Use an existing GitHub Actions workflow

A repository with only `.github/workflows` files does not use them until you opt in. On the CI page, open **CI settings: run GitHub Actions workflows, or turn a repository off** and add the repository to **Run Estelle CI for these repositories**.

Exactly one workflow must match the event, and it must stay within the supported subset. If the commit also contains `.estelle/ci.yml`, that file wins. Add a repository to **Never run Estelle CI for these repositories** to turn CI off even when `.estelle/ci.yml` exists.

## Run tests detected from your repository

For a repository with neither `.estelle/ci.yml` nor `.github/workflows`, **Settings → Automatic → Run CI from the tests you already have** lets Estelle derive a workflow from project manifests on pushes and pull requests.

It recognizes test and build commands in `package.json`, Python project and pytest configuration, `go.mod`, `Cargo.toml`, and a `Makefile` with a `test` target. Declared Node, Cargo, and uv workspaces can be detected per package. Detection uses the commands the manifests actually declare; it does not invent a test command for an unrecognized project.

Add `.estelle/ci.yml` when you need explicit setup, multiple jobs, a database, or commands different from the detected ones. That explicit file always takes precedence.

## Trigger and read a run

| Event | CI behavior |
|---|---|
| Push to a branch | Runs when the workflow's trigger and branch filters match. |
| Pull request opened, updated, or reopened | Runs for pull requests from branches of the same repository. |
| Tag push | Does not run. |
| Pull request from a fork | Does not run. |
| Run started through the API | Runs all jobs in one Silo and reports an **Estelle CI** check. |

The dashboard lists each job in dependency order. While a run is active, you can read its live terminal; after it finishes, each step retains its state, duration, and available log. The live view can skip lines from very verbose output, and retained logs can be bounded; the page says when output was not retained. Step results and the presence of a truncation notice are different kinds of evidence.

Use the run's **Check on GitHub** link to return to the commit. Other job groups on that commit are linked separately so one group's result is never presented as another group's pass.

## Interpret checks and test evidence

| Result | What it means |
|---|---|
| **Queued** or **Waiting for a slot** | No test result yet. The run is waiting for capacity. |
| **Passed** / **All steps passed** | Every required step in this run produced a successful result. |
| **Failed** / **A step failed** | A step exited unsuccessfully. Later steps did not run; inspect the first failing step. |
| **Incomplete** | The runner's outcome and step evidence do not support a pass. Read the missing-step explanation. |
| **Timed out** | The run exceeded its time budget. This is not a test pass. |
| **CI refused this job definition** | The workflow is outside the supported subset. Nothing was verified by running it. |
| **CI did not run: not queued** | Capacity or another preflight refusal prevented the run. Nothing was verified about the commit. |
| **Superseded** | A newer commit replaced this run. Judge the newer commit's own checks. |

Every commit for which CI is enabled also receives **Estelle CI preflight**. Preflight answers whether the workflow was accepted and its runs were queued; it does not answer whether tests passed.

| Preflight result | Meaning |
|---|---|
| **Workflow accepted: every run is queued** | The workflow is valid and its job checks were queued. Read those job checks for test results. |
| **Not every run could be queued** | Some work was not started because the account was at its run limit. The summary names the missing runs. |
| **CI refused this commit's workflow: nothing ran** | The file is invalid, unsupported, or required by repository settings but absent. |
| **CI could not decide: nothing was verified** | Estelle could not read the commit or resolve the account, so no conclusion is available. |

For branch protection, require **Estelle CI preflight** when you want a stable check for workflow acceptance, and require the relevant job checks when merging must depend on tests passing. Preflight alone is not a test gate.

## Limits

| Limit | Value |
|---|---|
| Jobs running at once per account | 4 |
| Runs waiting in the queue per account | 12 |
| Jobs per workflow | 20 |
| Steps per job | 50 |
| Time per step | 15 minutes |
| Time per job | 30 minutes |
| Workflow file size | 64 KiB |
| Characters per command | 8,000 |

There is no matrix, custom container, expression evaluation, caching, artifacts, or user-defined `concurrency` block. When a newer commit supersedes an older run, read the newer commit's checks instead. Estelle CI is not a drop-in replacement for every GitHub Actions workflow.

Silo time is charged in Estelle Credits from startup until shutdown. An account without credits is refused before the run starts. See [Plans and credits](https://docs.fatelabs.ca/plans).

## Related

- [Silos](https://docs.fatelabs.ca/silos): Understand the isolated machines that run CI jobs.
- [Twice](https://docs.fatelabs.ca/twice): Add browser evidence for user journeys.
- [Prism](https://docs.fatelabs.ca/prism): Review code and configure merge controls.
- [Automations](https://docs.fatelabs.ca/automations): Schedule recurring work outside CI.
