Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Headless CI recipe

Caliper's engine needs no GPU, no display, and no hardware, which means your robot regression tests can run on the free GitHub Actions tier. This page is a copy-paste job that builds the CLI, runs a seeded simulation + eval batch on a plain ubuntu-latest runner, and proves determinism by running the seeded work twice and diffing the bytes.

The job

Drop this into .github/workflows/robot-eval.yml of a project that depends on Caliper (swap the URDF and goals for your robot):

name: robot-eval
on: [push, pull_request]

jobs:
  seeded-sim-eval:
    runs-on: ubuntu-latest        # free tier — no GPU anywhere in this job
    env:
      URDF: oracle/fixtures/corpus/so100.urdf   # your robot here
      GOAL: 0.3,0.2,-0.1,0,0,0                  # in-limit joint goal
    steps:
      # Get Caliper. Until the crates.io / PyPI names are settled, build from
      # the repo (Swatinem/rust-cache makes rebuilds incremental-fast):
      - uses: actions/checkout@v4
        with:
          repository: msannikov03/caliper   # or your fork / a vendored copy
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: Build the CLI once
        run: cargo build --release -p caliper-cli

      - name: Eval batch — seeded plans across a seed sweep + machine-readable report
        run: |
          mkdir -p out
          for seed in 1 2 3 4 5; do
            ./target/release/caliper plan "$URDF" --goal "$GOAL" --seed "$seed" \
              > "out/plan_seed$seed.txt"
          done
          ./target/release/caliper report "$URDF" --goal "$GOAL" --json > out/report.json

      - name: Simulate + record an episode, headlessly
        run: |
          ./target/release/caliper sim "$URDF" --duration 2.0 > out/sim.txt
          ./target/release/caliper record "$URDF" --out out/dataset \
            --goal "$GOAL" --ticks 500 --fps 50

      # THE DETERMINISM ASSERTION: run the seeded work twice; the bytes must
      # match. `diff` exits non-zero on any drift, failing the job — flaky
      # simulation cannot hide.
      - name: Determinism — same seed, bit-identical output
        run: |
          ./target/release/caliper plan "$URDF" --goal "$GOAL" --seed 42 > a.txt
          ./target/release/caliper plan "$URDF" --goal "$GOAL" --seed 42 > b.txt
          diff a.txt b.txt
          ./target/release/caliper report "$URDF" --goal "$GOAL" --json > r1.json
          ./target/release/caliper report "$URDF" --goal "$GOAL" --json > r2.json
          diff r1.json r2.json

      # Gate on quality, not just "it ran": --strict makes `report` exit
      # non-zero when the trajectory linter finds errors (limit violations,
      # wrap-around detours, singular corridors…).
      - name: Trajectory lint gate
        run: ./target/release/caliper report "$URDF" --goal "$GOAL" --strict

      - uses: actions/upload-artifact@v4
        with:
          name: eval-out
          path: out/

Total cold time is dominated by the one release build of the CLI (cached across runs by rust-cache); the measured work itself is seconds — see Lightweight, measured.

Why diff is a valid determinism test here

It would not be for most simulators. It is for Caliper because the engine is clock-free (state advances only on step(dt), nothing reads the wall clock) and the only randomness is a seeded splitmix64 PRNG — so a seeded run's output is a pure function of its inputs, byte for byte, and the strictest possible assertion (diff) is also the simplest. If that diff ever fires, it is a real regression in the determinism contract (Stability contract), not noise to be tolerated.

Variants

Python instead of the CLI — same free runner, engine driven through the bindings (this is exactly how Caliper's own python CI job works):

      - uses: astral-sh/setup-uv@v5
      - run: uv venv && uv pip install maturin numpy
      - run: uv run maturin develop -m crates/caliper-py/Cargo.toml
      - run: |
          uv run python - <<'EOF'
          import caliper
          robot = caliper.Robot.from_urdf("oracle/fixtures/corpus/so100.urdf")
          # ... seeded planner / sim / dataset assertions ...
          EOF

Prebuilt abi3 wheels (macOS arm64 + manylinux x86_64) are attached to each GitHub releasepip install <wheel-url> skips the Rust toolchain entirely once you pin a release.

Policy eval batch — the learning sidecar's eval harness (caliper-learn eval --json, seeded success-scored rollouts with Wilson-95 aggregates) runs on the same CPU runners; it additionally needs uv pip install -e learn and CPU torch. Budget accordingly: torch is the one heavyweight download in that variant, and it belongs to the training side of the fence, never to the engine.

Contact simulation — the optional MuJoCo backend is also CPU-only and headless-capable; enable the mujoco feature and fetch the pinned dylib with scripts/fetch_mujoco.sh first. The default recipe above deliberately uses only the built-in simulator so the job stays dependency-free.