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

# CI with Boat

> Run your test suite in a Boat sandbox from GitHub Actions: install the CLI, sign in with a secret, start from a named snapshot, run the tests, keep the exit code, always delete the sandbox.

Each CI job creates one sandbox, runs the tests in it, and deletes it. `boat exec` exits with the exit code of the tests, so the job fails when the tests fail.

## 1. Create a CI key and a secret

Create a key with the `ci` preset. It can create, use and delete sandboxes. Then store it as a repository secret:

```bash theme={null}
boat api-key create github-ci --preset ci --ttl 90d
gh secret set BOAT_API_KEY      # paste the boat_... secret that the command printed once
```

The key expires after 90 days. Rotate it with `boat api-key rotate <id>`. See [API keys](/api-keys).

## 2. Prepare a named snapshot (one time)

Install your toolchain and dependencies once. Save the result as a named snapshot. Each CI sandbox then starts with them in place:

```bash theme={null}
id=$(boat new --json | jq -r 'select(.event == "ready") | .id')
boat exec "$id" --timeout 600 -- bash -lc 'sudo apt-get update && sudo apt-get install -y postgresql-client'
boat exec "$id" --timeout 600 -- bash -lc 'docker pull postgres:16 && docker create --name keep-postgres postgres:16'
boat snapshot "$id" ci-base
boat delete "$id" --yes
```

<Warning>
  **A named snapshot keeps only the Docker images that a container uses.** A plain `docker pull` is lost. Create a container from each image that you need, as above: `docker create --name keep-<x> <image>`. The container does not need to run. Boat pulls registry images again at each start, so a large image adds start time. See [What a snapshot saves](/snapshots/whats-saved#docker).
</Warning>

10 named snapshots are free. Run the same steps again to update `ci-base`: a save with the same name replaces it. See [Named snapshots](/snapshots/templates).

## 3. The workflow

```yaml .github/workflows/boat-tests.yml theme={null}
name: Tests on Boat

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    env:
      BOAT_API_KEY: ${{ secrets.BOAT_API_KEY }}
    steps:
      - uses: actions/checkout@v4

      - name: Install the Boat CLI
        run: |
          mkdir -p "$HOME/.local/bin"
          curl -fsSL "https://boat.dev/api/boat/cli/download?platform=linux-x64&channel=prod" \
            -o "$HOME/.local/bin/boat"
          chmod +x "$HOME/.local/bin/boat"
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"

      - name: Sign in
        run: printf '%s' "$BOAT_API_KEY" | boat login --key-stdin --json

      - name: Create a sandbox from the named snapshot
        id: sandbox
        run: |
          id=$(boat new --from ci-base --type small --ttl 1800 --json | jq -r 'select(.event == "ready") | .id')
          test -n "$id"
          echo "id=$id" >> "$GITHUB_OUTPUT"

      - name: Copy the code
        run: |
          git archive --format=tar.gz -o /tmp/src.tgz HEAD
          boat scp /tmp/src.tgz "${{ steps.sandbox.outputs.id }}:/home/user/src.tgz"
          boat exec "${{ steps.sandbox.outputs.id }}" "mkdir -p repo && tar xzf src.tgz -C repo"

      - name: Run the tests
        run: |
          set +e
          boat exec "${{ steps.sandbox.outputs.id }}" --cwd repo --timeout 600 --stream -- bash -lc 'npm ci && npm test'
          code=$?
          boat scp "${{ steps.sandbox.outputs.id }}:/home/user/repo/junit.xml" ./junit.xml || true
          exit $code

      - name: Delete the sandbox
        if: always() && steps.sandbox.outputs.id != ''
        run: boat delete "${{ steps.sandbox.outputs.id }}" --yes --json
```

What each part does:

| Part | Why |
| - | - |
| Static binary download | The install script opens a browser sign-in. The binary needs none. Use `platform=linux-arm64` on an ARM runner. |
| `boat login --key-stdin` | Keeps the key out of the process list. |
| `--from ci-base` | Starts with the toolchain and dependencies in place. |
| `--ttl 1800` | Auto-stop after 30 minutes, if the delete step never runs. |
| `--timeout 600 --stream` | A command waits at most 600 seconds. `--stream` prints the output live. |
| `set +e` and `exit $code` | Copies the test report out, then fails the job with the exit code of the tests. |
| `if: always()` | Deletes the sandbox also when a step failed or the job was cancelled. Billing stops at the delete. |

Use the sandbox ID in every command. The `current` alias does not work in CI: each step runs in a new shell.

### Tests that run longer than 10 minutes

A command that waits for its result stops after 600 seconds. Run a longer suite detached, then follow it. `--attach` exits with the exit code of the command:

```bash theme={null}
pid=$(boat exec "$id" --cwd repo --detach "npm run test:e2e" | jq -r .processId)
boat exec "$id" --attach "$pid"
```

See [Background commands](/long-running-tasks/background-commands).

## Many jobs at once: start limits

Each job starts one sandbox. Create, fork and resume each count as a start.

| Plan | Starts per minute / hour / day | Sandboxes at once |
| - | - | - |
| \$20 | 12 / 60 / 200 | 100 |
| \$100 | 30 / 210 / 840 | 300 |
| \$500 | 65 / 420 / 1,680 | 1,000 |
| \$2000 | 90 / 600 / 2,400 | 2,000 |
| [Free trial](/free-trial) | 5 / 25 / 75 | 2 |

* A start above the limit fails with `rate_limited` and the window (`minute`, `hour` or `day`). It does not count as a start.
* A matrix of 30 jobs on the \$20 plan needs about 3 minutes to start all sandboxes. Cap it with `strategy.max-parallel: 12`.
* To fail at once when no machine is free, add `--fail-fast` to `boat new`. Retry the job later.

All limits and how to raise them: [Limits](/limits).

## Cost

Boat bills each second that the sandbox runs. A `small` sandbox costs \$0.018 an hour. A 6-minute run costs about \$0.002. A deleted sandbox costs nothing more. See [Pricing](/pricing).

## Private repositories and secrets

The workflow copies the checked-out code, so the sandbox needs no GitHub access. To give the tests secrets, set them on an [environment](/environments) and start with `--environment <name>`, or pass one value with `-e KEY=VALUE`. Check that a value arrived:

```bash theme={null}
boat exec "$id" 'echo ${DATABASE_URL:+set}'
```

An environment that is safe for third parties withholds all your secrets. See [Secrets](/environments/secrets).

## Other CI systems

The steps are the same in GitLab CI, CircleCI or Buildkite: download the binary, sign in with `--key-stdin`, `boat new --json`, `boat exec`, and `boat delete --yes` in a step that always runs. In code, use the [Python](/sdks/python) or [TypeScript](/sdks/typescript) SDK instead.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.