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

# CI

> Run GitHub Actions jobs on fresh machines, or one command on InstaBox from any CI.

At the end of this page, your tests run on InstaBox. There are two ways: send whole GitHub Actions
jobs to fresh machines with `runs-on: instabox-medium`, or call the CLI from a step in any CI.

| | `runs-on: instabox-medium` (recommended) | The CLI from any CI |
| - | - | - |
| What runs on InstaBox | Each job, on a fresh machine of its own | One command per step |
| Setup | Install the GitHub App, change `runs-on` | An API key as a CI secret |
| Works with | GitHub Actions | Any CI |
| GitHub Actions minutes | None | The job's runner uses them |

## Run GitHub Actions jobs on InstaBox

**1. Connect GitHub**

On the dashboard's **CI** page, click **Connect GitHub**.

![The CI page: Connect GitHub, then change runs-on to instabox-medium](https://mintcdn.com/instacloud/N9PM4qc9k2LHbGZu/images/ci-setup.webp?fit=max\&auto=format\&n=N9PM4qc9k2LHbGZu\&q=85\&s=8ca6ef275693bcfcee35d8eed13e4684)

**2. Change runs-on in your workflow**

```yaml theme={null}
jobs:
  test:
    runs-on: instabox-medium
```

Each job gets a fresh Ubuntu 24.04 machine with the tools of GitHub's `ubuntu-24.04` image. The
machine is deleted when the job ends. The image is built from GitHub's runner-images release
`ubuntu24/20261026.354`, so tools and versions are those in its
[Ubuntu 24.04 software list](https://github.com/actions/runner-images/blob/ubuntu24/20261026.354/images/ubuntu/Ubuntu2404-Readme.md).
A job reads the release in `$ImageVersion`, and `/usr/local/lib/instabox/ci/software-list` prints
what its machine has.

**3. See your jobs**

The dashboard's **CI** page and `instabox ci` list your jobs, and say why a job waits or was
refused. GitHub shows such a job only as queued.

| Label | Machine | Plans |
| - | - | - |
| `instabox-small` | 2 vCPU, 4 GiB | Pro, Builder, Team |
| `instabox-medium` | 4 vCPU, 8 GiB | Free, Pro, Builder, Team |
| `instabox-large` | 8 vCPU, 16 GiB | Pro, Builder, Team |
| `instabox-xlarge` | 16 vCPU, 32 GiB | Builder, Team |
| `instabox-2xlarge` | 32 vCPU, 64 GiB | Team |

> [!NOTE]
>
> Not installed: Haskell, Swift, Julia, CodeQL, Edge, and Homebrew. The Android SDK is a trimmed one,
> with a single NDK (27).

## Start jobs from a snapshot

A job can start from a snapshot of your own instead of a fresh machine, so your repository,
toolchain, and package caches are already there.

**1. Prepare a machine**

Create a machine from the `ci` template and set it up as your jobs need it:

```bash theme={null}
instabox create --name ci-warm --template ci
instabox ssh ci-warm
```

Clone your repository to `~/work/<repo>/<repo>` (where `actions/checkout` puts it), install
your toolchain, and fill your package caches.

**2. Save a snapshot**

```bash theme={null}
instabox stop ci-warm
instabox snapshot save ci-warm --name acme-warm
```

**3. Name it in runs-on**

Add `-snapshot-` and the snapshot's name to the size label:

```yaml theme={null}
jobs:
  test:
    runs-on: instabox-medium-snapshot-acme-warm
```

This label is the job's whole `runs-on`: no size label, `self-hosted`, or other label next to
it. The size still sets vCPUs and memory, and storage is at least the snapshot's. You can name
the snapshot by its ID too: `instabox-medium-snapshot-sn_...`. Only a job with this exact
`runs-on` can run on a machine from the snapshot.

`instabox ci` and the dashboard's **CI** page show which snapshot each job started from:

```text theme={null}
ID    REPO      JOB        SIZE             SNAPSHOT   STATE               WAIT  RUN  REASON
1042  acme/app  CI / test  instabox-medium  acme-warm  running             9s    41s
1041  acme/app  CI / lint  instabox-medium  -          completed: success  12s   1m
```

A job is refused, with the reason in `instabox ci`, when the snapshot is not in the workspace
the GitHub App is linked to, is still saving or failed, or was not saved from a `ci` machine. It
never falls back to a fresh machine.

Snapshots work in private repositories only, with **Run workflows from fork pull requests** turned
off (the repository's **Settings > Actions > General**). In a public repository, or one that runs
workflows from forks, jobs that name a snapshot are refused: a fork's workflow could name the
snapshot too. If GitHub doesn't answer instabox's check, the job waits and is tried again.

> [!WARNING]
>
> Keep secrets out of the snapshot: no tokens, SSH keys, or `.env` files. Any workflow on a branch
> of a linked repository can name it. Jobs get secrets from GitHub Actions as usual. A job can't
> change the snapshot; each one gets its own copy.

> [accordion] What to put in the snapshot
>
> The repository under `~/work`, toolchains, and caches such as `~/.cargo/registry`,
> `~/.npm`, `~/.cache/pip`, or `~/go/pkg/mod`. Leave out secrets, and stop any services you
> started before saving. A leftover runner registration or work directory does no harm: each
> job's runner gets its own registration.

> [accordion] Keep it fresh
>
> The snapshot keeps the GitHub Actions runner it was saved with, and GitHub stops sending jobs to
> a runner more than 30 days behind its latest release. Save it again from a new `ci` machine
> at least once a month.

## Run the CLI from any CI

A step creates a machine, uploads the checkout, runs one command, and deletes the machine.

**1. Store an API key as a CI secret**

```bash theme={null}
instabox keys create --name github-ci --expires-in-days 90 --json | jq -r .key | gh secret set INSTABOX_API_KEY
```

The key goes straight into the repository secret `INSTABOX_API_KEY` and is not printed.

**2. Run the tests**

`npm ci && npm test` stands for your own test command.

```yaml theme={null}
jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    env:
      INSTABOX_API_KEY: ${{ secrets.INSTABOX_API_KEY }}
    steps:
      - uses: actions/checkout@v4
      - name: Install the instabox CLI
        run: npm install -g instabox
      - name: Run the tests
        run: instabox run --push . --timeout 10m -e CI=1 -- 'npm ci && npm test'
```

The step's log shows the push, the command's output, and the machine's deletion. The step fails
when the command fails. The command runs 10 minutes at most.

To pass secrets, use `-e NAME="$NAME"`. Pull requests from forks get no secrets, so the step
cannot run there.

## GitHub App permissions

> [accordion] What the GitHub App asks for
>
> The [instabox GitHub App](https://github.com/apps/instabox-cloud) asks for these permissions:
>
> | Permission | Used for |
> | - | - |
> | Metadata: read | Required by GitHub for every App |
> | Actions: read | A job's branch, commit, and failed step |
> | Administration: read and write | Registering and deleting each job's runner |
> | Members: read (organizations) | Checking that whoever links an organization belongs to it |
>
> The App asks for nothing on your code, pull requests, issues, or workflows.

> [accordion] Why Administration?
>
> GitHub lets an App add a runner to a repository only through Administration. Personal accounts
> have no organization-level runners, so Administration is the only way to run their CI.

> [accordion] Don't want to grant it?
>
> Call the CLI from any CI instead. It needs no GitHub App, only an API key. See
> [Run the CLI from any CI](#run-the-cli-from-any-ci). On
> Enterprise, we can set up a narrower App for your organization:
> [contact us](https://instabox.sh/contact).

> [card] [API keys](https://instabox.sh/docs/security/api-keys.md)
>
> Scopes and expiry for the key your CI uses.

> [card] [CLI reference](https://instabox.sh/docs/reference/cli.md)
>
> `instabox run` and its exit codes.
