Spawn forkd microVM sandboxes from a snapshot tag, run a command across N children, and collect the outputs — all from a GitHub Actions workflow.
Use cases:
- Parallelize CI across isolated microVMs (one sandbox per test partition).
- Run AI agents in a sandbox so untrusted code never touches the runner.
- Fan-out exploration — N copies of a primed-state sandbox each try a different strategy.
jobs:
test-in-sandboxes:
runs-on: ubuntu-latest
steps:
- uses: deeplethe/forkd-action@v1
with:
url: ${{ secrets.FORKD_URL }} # e.g. http://forkd.internal:8889
token: ${{ secrets.FORKD_TOKEN }}
snapshot_tag: python-3-12-slim
n: 4
command: pytest tests/
id: forkd
- name: Show results
run: echo '${{ steps.forkd.outputs.results }}' | jq .The action drives an already-running forkd-controller somewhere reachable from the runner — your dev box, a self-hosted runner, a managed VM, etc. The action itself is just a thin REST client; it does not install or run forkd-controller on the GitHub runner (GitHub-hosted runners lack reliable KVM access).
| Input | Required | Default | Notes |
|---|---|---|---|
url |
yes | — | forkd-controller base URL |
token |
yes | — | Bearer token; store as a GitHub secret |
snapshot_tag |
yes | — | Snapshot to fork children from |
command |
one-of | — | Single-line shell command run via sh -c |
script |
one-of | — | Multi-line script; base64-shipped, run via sh |
n |
no | 1 |
Number of sandboxes to spawn |
per_child_netns |
no | false |
Use pre-provisioned per-child netns |
memory_limit_mib |
no | 256 |
Per-child memory cap |
prewarm |
no | false |
Relocate cold-cache penalty from first BRANCH to creation |
exec_timeout_secs |
no | 300 |
Per-sandbox exec timeout |
fail_on_nonzero |
no | true |
Workflow fails if any sandbox exits non-zero |
cleanup |
no | true |
DELETE each sandbox after the run |
Exactly one of command or script must be set.
| Output | Shape |
|---|---|
results |
JSON array, one entry per sandbox: {id, exit_code, stdout, stderr} |
any_failed |
"true" if any sandbox exited non-zero, else "false" |
name: Test in sandboxes
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: deeplethe/forkd-action@v1
with:
url: ${{ secrets.FORKD_URL }}
token: ${{ secrets.FORKD_TOKEN }}
snapshot_tag: pytest-runner
n: 4
script: |
cd /opt/repo
git fetch origin ${{ github.sha }}
git checkout ${{ github.sha }}
pip install -e .
pytest tests/ --shard=$SANDBOX_INDEX/4- A reachable
forkd-controllerinstance (v0.3.0+ recommended forprewarmsupport). - A snapshot already registered under
snapshot_tag(build withforkd snapshotor the controller'sPOST /v1/snapshots). - A bearer token configured with
--token-fileon the daemon side.
- The action sends the bearer token only to the configured
urland only over the GitHub Actions runner's outbound network. Always store the token as asecrets.*value, never as a literal in workflow YAML. - For self-hosted runners with a dedicated forkd host, prefer a private network address for
url. cleanup: true(the default) ensures sandboxes are deleted after the run; this matters for shared controllers where leftover sandboxes consume memory + cgroup quota.
- Tags follow semver. Pin to a major (
@v1) for the floating-latest-compatible behavior, or to a specific tag (@v1.0.0) for reproducibility. - Release notes: https://github.com/deeplethe/forkd-action/releases
Apache-2.0. See LICENSE.
- forkd main repo: https://github.com/deeplethe/forkd
- forkd-mcp (Model Context Protocol server): https://github.com/deeplethe/forkd/tree/main/sdk/mcp
- v0.3 pause-window measurements: https://github.com/deeplethe/forkd/blob/main/bench/pause-window/RESULTS-v0.3.md