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

# GitHub Actions Runner

[Novita Actions Runner Controller](https://github.com/novitalabs/github-actions-runner-controller) runs every GitHub Actions job in its own isolated Novita Sandbox. A long-running controller receives assigned jobs from GitHub, creates one fresh runner sandbox per job, and kills the sandbox when the job finishes. Each job starts from a clean environment, and there are no runner machines to maintain.

The controller uses GitHub's official [`actions/scaleset`](https://github.com/actions/scaleset) client to receive jobs and desired runner capacity. The same controller image can run in two places. In Novita Hosted mode, the recommended default, the controller runs in a long-running Control Sandbox on Novita, so it stays online without depending on your machine. In Local Docker mode, the controller runs on a machine you manage and still creates ephemeral runner sandboxes on Novita.

> [!NOTE]
>
> The controller uses GitHub's Scale Set API, which GitHub currently labels Public Preview. The client API is stable, but its Go interfaces may evolve.

## How it works

```mermaid theme={"system"}
flowchart LR
    G["GitHub Actions<br/>assigned job and JIT config"] --> C["Controller Sandbox<br/>github-actions-controller"]
    C --> T["Runner Template<br/>github-actions-runner"]
    T --> R["Ephemeral Runner Sandbox<br/>one per job"]
    R --> K["Job complete<br/>kill Sandbox"]
```

Hosted mode uses two public Novita templates, so there is nothing to build before deploying:

| Role | Public template |
| - | - |
| Control Sandbox | `github-actions-controller` |
| Ephemeral CI runner | `github-actions-runner` |

Each job's just-in-time (JIT) runner configuration reaches the runner sandbox over the adapter's stdin and the sandbox filesystem API. The runner entrypoint reads and unlinks the file, then exports it only to the runner child process. The configuration never appears in sandbox creation environment values, metadata, process arguments, or controller logs.

***

## Prerequisites

Prepare the following on the machine where you run the deployment script:

* Bash and Python 3.
* Node.js 20+ and npm, if the `novita` CLI is not already installed.
* The latest `novita` CLI (version 2.1.0 or newer). See [CLI Authentication](https://docs.novita.ai/guides/sandbox-cli-auth.md).
* A Novita API key. It selects the Novita team that will own the Control Sandbox and brokered Secrets; a separate browser login is not required.
* A GitHub personal access token (PAT) that can manage self-hosted runners:
  * For a classic PAT, use `repo` for a repository target or `admin:org` for an organization target.
  * For a fine-grained PAT, grant repository `Administration: read and write`, or organization `Administration: read` plus `Self-hosted runners: read and write`.
* The exact repository or organization URL to register, such as `https://github.com/novitalabs/github-actions-runner-controller`.

Install and verify the CLI if needed:

```bash CLI icon="terminal" theme={"system"}
npm install --global novita-sandbox-cli@latest
novita --version
```

***

## Deploy on Novita (recommended)

**1. Export credentials**

Export the three required values in the current shell. Do not commit them to the repository.

```bash CLI icon="terminal" theme={"system"}
export GITHUB_CONFIG_URL=https://github.com/OWNER/REPOSITORY
export GITHUB_TOKEN=YOUR_GITHUB_PAT
export NOVITA_API_KEY=YOUR_NOVITA_API_KEY
```

`GITHUB_CONFIG_URL` is mandatory. The script rejects the example `OWNER/REPO` and `OWNER/REPOSITORY` placeholders so it cannot silently deploy against an unintended target.

Verify that the CLI sees the intended team before deployment:

```bash CLI icon="terminal" theme={"system"}
novita auth info
```

**2. Run the deployment script**

Clone the repository and run the Hosted deployment from the repository root:

```bash CLI icon="terminal" theme={"system"}
git clone https://github.com/novitalabs/github-actions-runner-controller.git
cd github-actions-runner-controller
bash scripts/deploy-hosted.sh
```

The script runs on your machine and creates the long-running Control Sandbox in Novita. On success it prints the target identity, the Control Sandbox ID, and the health state:

```json theme={"system"}
{"targetId":"novitalabs-github-action-a814d8991e","controllerId":"novita-hosted-novitalabs-github-action-a814d8991e","sandboxId":"...","healthy":true}
```

Keep this output. The resume script below accepts the sandbox ID it contains.

**3. Point workflows at the runner**

The default Scale Set label is `novita-scale-set`. In the target repository, point each job that should run on Novita at this label:

```yaml .github/workflows/ci.yml theme={"system"}
jobs:
  test:
    runs-on: novita-scale-set
```

For complete workflow files, see the [GitHub CI examples](https://github.com/novitalabs/sandbox-cookbook/tree/main/github-ci/.github/workflows) in the Novita Sandbox cookbook.

If you use a coding agent, you can also ask it to write the workflow for you:

```text Prompt theme={"system"}
Write a GitHub Actions workflow YAML for my repository that runs on novita-scale-set.
Reference: https://github.com/novitalabs/github-actions-runner-controller/blob/main/README.md
```

The controller creates or reuses the named Scale Set. It intentionally does not delete it on shutdown, so controller restarts are safe.

### Multiple repositories or organizations

One controller serves exactly one `GITHUB_CONFIG_URL` target. How many repositories that covers depends on whether the target is a personal repository or an organization.

**Personal repositories.** A controller registered against a repository URL only serves that repository. To use Novita runners in several personal repositories, run the deployment once per repository. The script derives a stable target ID from the normalized `GITHUB_CONFIG_URL`, so each repository gets its own controller identity, brokered Secrets, and Control Sandbox, and a second deployment cannot reuse or overwrite the first one.

```bash CLI icon="terminal" theme={"system"}
GITHUB_CONFIG_URL=https://github.com/OWNER/ANOTHER_REPOSITORY \
  bash scripts/deploy-hosted.sh
```

**Organizations.** A controller registered against an organization URL serves every repository in that organization. Deploy once with the organization URL and the PAT scopes for an organization target described in [Prerequisites](#prerequisites):

```bash CLI icon="terminal" theme={"system"}
GITHUB_CONFIG_URL=https://github.com/ORG_NAME \
  bash scripts/deploy-hosted.sh
```

For public repositories in the organization to use the runners, open the organization's **Settings → Actions → Runner groups → Default** on GitHub and enable **Allow public repositories**.

### Resume the Control Sandbox

The resume script accepts either the exact sandbox ID or the same GitHub target URL. Target-based lookup checks both running and paused sandboxes and fails on duplicates instead of choosing one arbitrarily.

```bash CLI icon="terminal" theme={"system"}
scripts/resume-hosted.sh SANDBOX_ID

# Or look the sandbox up by target URL:
GITHUB_CONFIG_URL=https://github.com/OWNER/REPOSITORY \
  scripts/resume-hosted.sh

# Equivalent core command:
novita sandbox resume SANDBOX_ID --timeout 525600h
```

> [!WARNING]
>
> Always pass `--timeout` when resuming manually. Without it, Novita resets a resumed sandbox to its short default lifetime (about five minutes). The lifecycle stays `pause + auto-resume`, but the controller would pause again almost immediately.

Control Sandboxes created before target metadata was introduced must still be resumed by their sandbox ID.

***

## Alternative: Local Docker

Use Local mode when the controller should run on a machine you manage. Install Docker and prepare the same GitHub PAT and Novita API key described above.

Clone the repository and copy the example environment file. The `.env` file is ignored by Git:

```bash CLI icon="terminal" theme={"system"}
git clone https://github.com/novitalabs/github-actions-runner-controller.git
cd github-actions-runner-controller
cp .env.example .env
```

Edit `.env` and fill in the target URL and credentials. The remaining values match the defaults and can stay as they are:

```bash .env theme={"system"}
GITHUB_CONFIG_URL=https://github.com/OWNER/REPOSITORY
GITHUB_TOKEN=YOUR_GITHUB_PAT
NOVITA_API_KEY=YOUR_NOVITA_API_KEY
NOVITA_RUNNER_TEMPLATE=github-actions-runner
SCALE_SET_NAME=novita-scale-set
RUNNER_LABELS=novita-scale-set
MIN_RUNNERS=0
MAX_RUNNERS=10
RUNNER_TIMEOUT=1h
RECONCILE_INTERVAL=30s
ORPHAN_GRACE=15m
```

Start the controller and check its health endpoint:

```bash CLI icon="terminal" theme={"system"}
docker compose up -d
curl http://127.0.0.1:8080/healthz
```

Local mode pulls the immutable `novitalabs/github-actions-controller:v0.1.1` image. Credentials are provided only through `GITHUB_TOKEN` and `NOVITA_API_KEY` in `.env`. Workflows use the same `runs-on: novita-scale-set` configuration shown above. For every variable the controller accepts, see [Configuration](#configuration).

***

## Custom runner template

To preinstall tools, build your own runner template on top of the public runner image and point the controller at it. See [Build Template](https://docs.novita.ai/guides/sandbox-template-build-operations.md) for the full `novita template create` reference.

**1. Write the template Dockerfile**

Start from `templates/runner/novita.Dockerfile` in the controller repository. Keep the three conventions the controller relies on: the `user` account, the `/home/user/actions-runner` directory, and the `entrypoint.sh` start command. The controller writes each job's JIT configuration into that directory and the entrypoint consumes it.

```dockerfile templates/runner/novita.Dockerfile theme={"system"}
FROM novitalabs/github-actions-runner:v0.1.1
USER root
RUN apt-get update && apt-get install -y --no-install-recommends \
      build-essential pkg-config libssl-dev zstd \
    && rm -rf /var/lib/apt/lists/*
USER user
CMD ["/home/user/actions-runner/entrypoint.sh"]
```

Add further `apt-get install` packages or tool downloads as needed. Install user-level tools such as rustup or nvm while `USER user` is active so they land in `/home/user`.

**2. Build the template**

Choose a name that does not collide with the public `github-actions-runner` template. Resources default to 2 vCPU and 4 GB; raise them for heavy compiles.

```bash CLI icon="terminal" theme={"system"}
novita template create my-gcc-runner \
  --path templates/runner \
  --dockerfile novita.Dockerfile \
  --cpu-count 4 --memory-mb 8192 \
  --cmd /home/user/actions-runner/entrypoint.sh \
  --ready-cmd 'test -x /home/user/actions-runner/entrypoint.sh'
```

Rebuild with the same name after each Dockerfile change, or use a new name if the platform rejects a duplicate.

**3. Set NOVITA\_RUNNER\_TEMPLATE**

Hosted mode reads the template name at deployment time. Kill the existing Control Sandbox for the target, then redeploy with the override:

```bash CLI icon="terminal" theme={"system"}
novita sandbox kill CONTROL_SANDBOX_ID
NOVITA_RUNNER_TEMPLATE=my-gcc-runner bash scripts/deploy-hosted.sh
```

Local Docker mode reads it from `.env`, then `docker compose up -d`:

```bash .env theme={"system"}
NOVITA_RUNNER_TEMPLATE=my-gcc-runner
```

Workflows do not change. Re-run a job and it lands on the new template.

One controller uses exactly one runner template. To offer several toolchains, deploy one controller per template with a distinct `SCALE_SET_NAME` for each, and pick the toolchain in a workflow with `runs-on`.

***

## Configuration

| Variable | Default | Description |
| - | - | - |
| `GITHUB_CONFIG_URL` | required | Repository or organization URL |
| `GITHUB_TOKEN` | required | GitHub PAT |
| `NOVITA_API_KEY` | required | Novita team API key |
| `NOVITA_RUNNER_TEMPLATE` | required | Runner template name or ID; see [Custom runner template](#custom-runner-template) |
| `SCALE_SET_NAME` | `novita` | Scale Set name |
| `RUNNER_LABELS` | Scale Set name | Comma-separated runner labels |
| `RUNNER_GROUP` | `default` | GitHub runner group |
| `MIN_RUNNERS` / `MAX_RUNNERS` | `0` / `10` | Capacity bounds |
| `RUNNER_TIMEOUT` | `1h` | Per-job sandbox hard lifetime |
| `RECONCILE_INTERVAL` | `30s` | Periodic convergence interval |
| `ORPHAN_GRACE` | `15m` | Paused orphan retention |
| `CONTROLLER_ID` | derived from target URL in Hosted mode | Ownership identity |
| `METRICS_ADDR` | `:8080` | Health and metrics listen address |
| `PYTHON` | `python3` | Python executable for the adapter |
| `NOVITA_ADAPTER` | `/app/provisioner/novita/adapter.py` | Adapter path |

Hosted deployment also accepts `CONTROLLER_TEMPLATE`, `GITHUB_SECRET_NAME`, `NOVITA_SECRET_NAME`, and `CONTROL_TIMEOUT`. Explicit identity or Secret name overrides are intended for advanced operation; the per-target defaults are safer for normal multi-project deployments. `CONTROL_TIMEOUT` controls the lifetime used during deployment and by `scripts/resume-hosted.sh`.

For lifecycle ownership and failure boundaries, see the project's [design notes](https://github.com/novitalabs/github-actions-runner-controller/blob/main/docs/design.md).


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