embeddedCI documentation

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

  1. The runner checks out your code and builds the firmware.
  2. It installs the embeddedci SDK and runs pytest.
  3. 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.elf

Why 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 / envDefaultPurpose
--benchpod-connectionSet to embeddedci:<device-name> for the cloud.
--benchpod-firmwarePath to the firmware image the test flashes.
--benchpod-api-base / BENCHPOD_API_BASEhttps://embeddedci.comEmbeddedCI 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.