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:<device-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 device through embeddedci.com with
embeddedci:<device-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 devices 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 device
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 device or the specific device(s) 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 a physical device 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
# SWD over remote_bitbang only exists in OpenOCD master (post-0.12.0);
# Ubuntu's apt openocd 0.12.0 is jtag_only and fails. xPack ships master.
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 pytest + the embeddedci SDK (cloud extra)
# pytest is NOT a runtime dep of the SDK (it's a pytest plugin), so install it too.
run: pip install pytest "embeddedci[cloud]"
- 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.elfWhy the OpenOCD step
Flashing drives OpenOCD's remote_bitbang adapter in SWD mode, bridged through the cloud tunnel to the device. SWD support for remote_bitbang exists only in OpenOCD master (post-0.12.0) — Ubuntu's apt openocd 0.12.0 is jtag_only and fails with Can't change session's transport. The workflow installs an xPack OpenOCD snapshot, which ships the needed master build.
Config options
| Option / env | Default | Purpose |
|---|---|---|
--benchpod-connection | — | Set to embeddedci:<device-name> for the cloud. |
--benchpod-firmware | — | Path to the firmware image the test flashes. |
--benchpod-api-base / BENCHPOD_API_BASE | https://embeddedci.com | EmbeddedCI server base URL (only the embeddedci: destination uses it). |
If the token can't be minted, the error says exactly why — one of: not running inside a GitHub Action, the job is missing id-token: write, or the token request itself failed.