GitHub Actions Runners
Bare-metal Linux, Windows, and macOS runners for GitHub Actions. Swap one label, keep your workflow.
One-line adoption#
Sandal runners are drop-in replacements for GitHub-hosted runners. Change runs-on, nothing else.
jobs:
test:
runs-on: sandal-ubuntu-24.04
steps:
- uses: actions/checkout@v4
- run: pnpm install
- run: pnpm testThat's it. Your existing steps, actions, and secrets work unmodified, only the executor underneath changes.
JIT ephemeral, single-tenant#
Every job gets its own microVM, Firecracker on Linux/Windows, a dedicated Mac mini for macOS, provisioned just-in-time when the job is queued and destroyed the moment it finishes. No shared kernel, no leftover state, no noisy neighbors.
- Runner registration is JIT: Sandal registers a fresh, single-use runner token with GitHub for each job, so nothing sits idle in your runner pool between builds.
- Cold create <1s for Linux/Windows microVMs; macOS jobs claim from a warm pool of physical minis.
- Kernel-level isolation means untrusted forks, third-party actions, and monorepo matrices never share a blast radius.
Warm pools#
Cold starts are fine for most CI, but if you're running hundreds of jobs an hour, keep instances pre-booted:
import { Sandal } from "@sandal/sdk";
const sandal = new Sandal({ apiKey: process.env.SANDAL_API_KEY });
await sandal.runners.pools.create({
os: "macos",
labels: ["sandal-macos-m4"],
minWarm: 2,
});minWarm keeps N runners booted and idle-registered so jobs claim instantly instead of waiting on a cold boot. Pools scale down to zero automatically when idle, you only pay active-CPU while a job is actually running.
Cache and sticky disks#
Attach a persistent disk to a label so dependency caches, build artifacts, and toolchain installs survive across jobs without re-downloading:
runs-on: sandal-ubuntu-24.04
env:
SANDAL_STICKY_DISK: pnpm-store-v1Sticky disks are content-addressed and mounted read-write per job, then snapshotted incrementally at the end of the run, no full re-upload, just the delta. This is the same per-minute incremental snapshot engine (V4/RAFS) that powers sandbox pause/resume, so restores are lazy and prefetch-driven instead of blocking on a full disk pull.
Pricing#
Billed per second, active-CPU only. Paused or queued time costs nothing.
| Runner | vCPU / RAM | Price |
|---|---|---|
sandal-ubuntu-24.04 | 2 vCPU / 4GB | $0.003/min |
sandal-ubuntu-24.04-4x | 4 vCPU / 8GB | $0.006/min |
sandal-ubuntu-24.04-8x | 8 vCPU / 16GB | $0.012/min |
sandal-ubuntu-24.04-16x | 16 vCPU / 32GB | $0.024/min |
sandal-windows-2022 | 2 vCPU / 4GB | $0.006/min |
sandal-windows-2022-4x | 4 vCPU / 8GB | $0.012/min |
sandal-macos-m4 | dedicated M4 mini | $0.06/min |
Windows runners are priced at 2x the equivalent Linux shape (Windows Server microVM overhead). macOS runs on dedicated Mac minis, not shared virtualization, capacity is metered per-minute, not per-second-fractional, but billing still stops the instant the job ends.
Migration wizard#
Point the CLI at an existing workflow file and it rewrites runs-on labels, flags actions that assume GitHub-hosted-only behavior (like actions/cache paths tied to /opt/hostedtoolcache), and opens a PR:
$ npx sandal migrate --repo your-org/your-repo --workflow ci.ymlThe wizard is conservative, it never touches job logic, only the runs-on line and any pool/label wiring it can infer from your matrix strategy. Review the diff before merging.
Pools API#
For programmatic control, scaling pools ahead of a release branch cut, or tearing them down after a migration freeze, use the SDK directly:
import { Sandal } from "@sandal/sdk";
const sandal = new Sandal({ apiKey: process.env.SANDAL_API_KEY });
// Create a warm pool
const pool = await sandal.runners.pools.create({
os: "linux",
labels: ["sandal-ubuntu-24.04-4x"],
minWarm: 5,
});
// Scale it
await sandal.runners.pools.update(pool.id, { minWarm: 10 });
// List active pools
const pools = await sandal.runners.pools.list();
// Tear down
await sandal.runners.pools.delete(pool.id);Every pool emits per-second-billed metrics, active jobs, warm idle count, and claim latency, queryable from the same evidence-gated capability matrix that backs the rest of Sandal's numbers.