Run HIL in GitHub Actions
Build firmware on a GitHub runner, then flash and test it on a real board over the cloud. The BenchPod lives wherever the hardware is plugged in and connects out to embeddedci.com; the job drives it through the connection string embeddedci:<name>. There is no BenchPod on the runner, and no API key or secret is stored. Auth is the workflow's GitHub OIDC token.
How it works
- The runner checks out your code and builds the firmware.
- It installs the
embeddedciSDK and runspytest. - The test connects to your pod through embeddedci.com with
embeddedci:<name>, flashes the firmware over the tunnel, power-cycles the target, and asserts on the UART output, all on real hardware.
The job proves which repository it is with a GitHub OIDC token. The server exchanges it for a short-lived session scoped to the pods that repo is allowed to drive, then bridges a byte tunnel to the pod, exactly like PyPI Trusted Publishing. That needs one permission block in the job:
permissions: id-token: write # REQUIRED — lets the job mint a GitHub OIDC token for embeddedci contents: read
One-time setup
1) Register and name the pod
Skip the registration if benchpod discover says the pod is already registered to your account (a pod set up for you is). Otherwise, from the machine wired to the pod, register it. Then give it a stable name on the BenchPod page (URL-safe, unique per org, e.g. benchpod-v1.0.0):
benchpod register --connection <pod-ip>
2) Trust your repository
On BenchPod → GitHub Actions, add your repository as OWNER/REPO (click Look up to fill the numeric ids) and choose Any pod or the specific pods this repo may drive. No secret is exchanged; the repo identity comes from the OIDC token at run time.
The workflow
A complete .github/workflows/selftest-cloud.yml that builds the firmware and runs it on benchpod-v1.0.0 over the cloud:
name: Selftest (cloud HIL)
# Runs the firmware on the board wired to the pod registered as "benchpod-v1.0.0",
# driven over embeddedci.com from pytest — no BenchPod on the runner.
on:
push:
paths:
- "selftest-stm32/selftest.c"
- "selftest-stm32/tests/**"
- ".github/workflows/selftest-cloud.yml"
workflow_dispatch: {}
permissions:
id-token: write # REQUIRED: mints the GitHub OIDC token for embeddedci
contents: read
jobs:
selftest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
# --- Build the firmware (arm-none-eabi + STM32CubeF4 HAL) ---
- name: Install ARM toolchain
uses: carlosperate/arm-none-eabi-gcc-action@v1
with:
release: "13.2.Rel1"
- name: Install xPack OpenOCD
# Flashing uses OpenOCD's cmsis-dap TCP backend (cmsis_dap_tcp), which only exists in
# builds newer than 0.12.0 — Ubuntu's apt openocd 0.12.0 lacks it. xPack 0.12.0-7 has it.
run: |
V=0.12.0-7
curl -fsSL "https://github.com/xpack-dev-tools/openocd-xpack/releases/download/v$V/xpack-openocd-$V-linux-x64.tar.gz" \
| sudo tar xz -C /opt
echo "/opt/xpack-openocd-$V/bin" >> "$GITHUB_PATH"
- name: Build firmware
run: make -C selftest-stm32
# --- Run it on benchpod-v1.0.0 over the cloud ---
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install the embeddedci SDK (cloud + pytest extras)
# [cloud] adds the WebSocket client for embeddedci:<name>; [pytest] installs pytest itself.
run: pip install "embeddedci[cloud,pytest]"
- name: Self-test on benchpod-v1.0.0 (cloud)
run: |
pytest selftest-stm32/tests -v \
--benchpod-connection=embeddedci:benchpod-v1.0.0 \
--benchpod-firmware=selftest-stm32/build/selftest.elfThe workflow passes no LA voltage. The pod refuses flashing, UART and capture until its LA bank voltage is chosen, and that is a property of the board, so the test suite declares it once in its conftest.py and the benchpod fixture selects it on connect:
# selftest-stm32/tests/conftest.py
import pytest
@pytest.fixture(scope="session")
def benchpod_la_voltage():
return 3.3 # the board's I/O voltage — change to 1.8 for a 1V8 boardWhy the OpenOCD step
OpenOCD runs on the runner and flashes through the pod's on-board CMSIS-DAP probe using its cmsis-dap adapter's TCP backend (cmsis_dap_tcp), bridged through the cloud tunnel to the device. That backend only exists in OpenOCD builds newer than 0.12.0. Ubuntu's apt openocd 0.12.0 lacks it, and the SDK stops with a FlashError saying so. The workflow installs an xPack OpenOCD snapshot (0.12.0-7), which carries the backend.
Config options
| Option / env | Default | Purpose |
|---|---|---|
--benchpod-connection | — | Set to embeddedci:<name> for the cloud. |
--benchpod-la-voltage / BENCHPOD_LA_VOLTAGE | — | Override the DUT's logic level (1.8 or 3.3) for one run, e.g. a job for a 1V8 board variant. Normally set once in conftest.py via the benchpod_la_voltage fixture; the flag wins over it, and the env var is the fallback. |
--benchpod-firmware | — | Path to the firmware image the test flashes. |
--benchpod-api-base / BENCHPOD_API_BASE | https://www.embeddedci.com | EmbeddedCI server base URL. |
--benchpod-api-key / BENCHPOD_API_KEY | — | An API key to authenticate with instead of OIDC, for running the same suite from a laptop or a non-GitHub CI system. When set, it is used in preference to OIDC. |
--benchpod-lease-wait | 600 | Seconds to wait for the shared pod when another run holds its lease, before failing with DeviceBusyError. Concurrent jobs queue on the pod. |
--benchpod-no-lease | off | Skip the exclusive lease (only safe when no other run uses the device). |
Without an API key the SDK mints a GitHub OIDC token. If that fails, the error says exactly why. It is one of: not running inside a GitHub Action (set BENCHPOD_API_KEY instead), the job is missing id-token: write, or the token request itself failed.
Publish the firmware for manual flashing
To make a CI build flashable from the web UI without running tests, add the upload-artifact action after the build step. It uses the same OIDC token and the same trusted-repository entry as the tests above, touches no BenchPod, and records the build with the wiring that pre-fills the flash dialog:
- uses: embeddedci-com/submit-job-action/upload-artifact@v1
with:
firmware: selftest-stm32/build/selftest.elf
build_target: stm32f4
openocd_target: target/stm32f4x.cfg
swclk: "11"
swdio: "12"
efuse: "1"Its full input list and the build_id output are on the Cloud builds page.