How to drive the sandbox from GitHub Actions (or any CI), with examples for the patterns we care about.
- run: make ci
working-directory: integration-sandbox
env:
IMAGES: edge # optional manifest selector
SBX_TOKENSMITH_IMAGE: ghcr.io/openchami/... # optional per-service overrideThat's it. make ci is fully self-contained: build → up → seed → bats → integration → down. Failure writes a forensic bundle to integration-sandbox/logs/ which you upload as an artifact.
A copy-pasteable starting point lives at .github-workflow.example.yaml. Drop it into a consumer repo as .github/workflows/sandbox.yml.
The most common case: a PR in (say) tokensmith should run the sandbox with the PR's image substituted in.
on: pull_request
jobs:
sandbox:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.24.6"
- run: sudo apt-get update && sudo apt-get install -y bats
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- run: make ci
working-directory: integration-sandbox
env:
SBX_TOKENSMITH_IMAGE: ghcr.io/openchami/tokensmith:pr-${{ github.event.pull_request.number }}
- if: failure()
uses: actions/upload-artifact@v4
with:
name: sandbox-logs
path: integration-sandbox/logs/Two PRs that depend on each other. Either pin both via env, or pass them as workflow inputs.
on:
workflow_dispatch:
inputs:
tokensmith_pr:
description: "tokensmith PR number"
type: string
boot_pr:
description: "boot-service PR number"
type: string
jobs:
sandbox:
# …
steps:
- run: make ci
working-directory: integration-sandbox
env:
SBX_TOKENSMITH_IMAGE: ghcr.io/openchami/tokensmith:pr-${{ inputs.tokensmith_pr }}
SBX_BOOT_IMAGE: ghcr.io/openchami/boot-service:pr-${{ inputs.boot_pr }}Pin every service to a release tag.
- run: make ci IMAGES=release-v1.0
working-directory: integration-sandboxTrack the latest published builds across the fleet.
on:
schedule:
- cron: "0 5 * * *" # 05:00 UTC daily
jobs:
sandbox-edge:
# …
- run: make ci IMAGES=edge
working-directory: integration-sandboxPer-job parallelism for faster feedback.
strategy:
matrix:
uc: [uc1, uc2, uc3]
steps:
- run: |
make up
make seed
make ${{ matrix.uc }}
working-directory: integration-sandbox
- run: make down
working-directory: integration-sandbox
if: always()| Resource | Floor | Recommended |
|---|---|---|
| RAM | 8 GiB | 16+ GiB (every emulator + service runs concurrently) |
| Disk | 15 GiB free | 30+ GiB (image cache grows) |
| CPU | 2 vCPU | 4 vCPU (compose can build images in parallel) |
| Outbound network | ghcr.io, docker.io, hashicorp.com | same |
ubuntu-24.04 GitHub-hosted runners (4 vCPU, 16 GiB) are the cheapest tier that consistently fits.
Image pull is the slowest step on a cold runner. Two reasonable approaches:
GHCR is fast enough that no host-side cache is needed for :main / :latest. For pinned tags, the docker daemon's content-addressable layer cache hits on every re-run within the same job.
power-control and ipmi-sim are built locally. To cache them across runs,
check out the upstream source on the runner first, then prime the layer
cache before invoking the sandbox:
- name: Check out power-control
uses: actions/checkout@v4
with:
repository: OpenCHAMI/power-control
path: power-control
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: power-control
file: power-control/Dockerfile.build
tags: ghcr.io/openchami/power-control:latest
cache-from: type=gha
cache-to: type=gha,mode=max
load: true
- run: SBX_POWER_CONTROL_SRC=$PWD/power-control make -C integration-sandbox cibuild-images.sh only invokes docker build when docker image inspect
fails, so a primed image is reused as-is.
On failure, make ci leaves integration-sandbox/logs/<UTC>-ci-failure/. Upload it:
- if: failure()
uses: actions/upload-artifact@v4
with:
name: sandbox-logs-${{ github.run_id }}
path: integration-sandbox/logs/
retention-days: 14Inside the bundle:
state.txt—docker ps,df -h,free -h,docker stats.log-<container>.txt— last 500 lines per container.
None for default usage. The sandbox uses public images and self-contained credentials. Override only if you point at a private registry.
- Don't
set -eand expect partial success.make cireturns non-zero on any failure; useif: failure()for cleanup. - Don't run
make upin one step andmake testin another withoutif: always()onmake down. A failed step leaves containers running and blocks subsequent runs on the same self-hosted runner. - Don't expose host ports above 1024 from a self-hosted runner without a network policy. The sandbox binds to 127.0.0.1 by default, but if you remove that, multiple runs can collide.
- Don't share the docker daemon across PR runs. Two runs of UC3 against the same daemon will fight over container names. Use isolated runners or job-scoped containers.