Cloud builds
Build your firmware in GitHub Actions with the toolchain you already use, then push the .elf, .hex and .bin files to embeddedci.com with the upload-artifact action. Every uploaded build shows up in the BenchPod flash dialog, so anyone on your team can flash it onto a DUT from the web app without a local toolchain.
How it works
- Your workflow checks out the code and builds the firmware on the GitHub runner.
- The
upload-artifactstep records a GitHub-sourced build on embeddedci.com (repository, branch, commit, run) and attaches the firmware and its siblings with the same stem. - The build lists on the Builds page, and its files appear in the Flash tab of every BenchPod in your organization, with the wiring you passed to the action filled in.
The upload touches no BenchPod, so it is safe to run on every push.
Set up
- Trust the repository in the web app under BenchPod → GitHub Actions (see Security & trusted repos).
- Give the job
permissions: id-token: write. The action authenticates with the job's GitHub OIDC token, so no API key or secret is stored in the repository. - Add the
upload-artifactstep after your build step.
Without the trusted repository or the permission, the step fails with a message naming what is missing.
Workflow example
name: firmware
on: [push]
permissions:
id-token: write # required: the action authenticates with the job's OIDC token
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
# Your existing build: any toolchain, any build system.
- run: sudo apt-get install -y gcc-arm-none-eabi
- run: make # produces build/app.elf, build/app.hex, build/app.bin
- uses: embeddedci-com/submit-job-action/upload-artifact@v1
id: upload
with:
firmware: build/app.elf
build_target: stm32f4
openocd_target: target/stm32f4x.cfg
swclk: "11"
swdio: "12"
nreset: "true"
efuse: "1"
- run: echo "Build ${{ steps.upload.outputs.build_id }}"Inputs and outputs
| Input | Default | Description |
|---|---|---|
firmware | required | Path to the built firmware (.elf, .bin or .hex). Siblings with the same stem (.elf, .bin, .hex, .uf2) are uploaded too. |
build_target | empty | Platform id recorded against the build, e.g. stm32f4. Shown on the Builds page. |
openocd_target | empty | OpenOCD target config for the DUT, e.g. target/stm32f4x.cfg. Pre-fills the flash dialog. |
swclk | empty | LA channel (1-14) wired to SWCLK. Pre-fills the flash dialog. |
swdio | empty | LA channel (1-14) wired to SWDIO. Pre-fills the flash dialog. |
nreset | empty | true when the target's NRST is wired to the pod's reset pin (DUT header J1 pin 22). Leave empty when it is not wired. |
efuse | empty | Target-power rail: 1 = Internal 5 V (eFuse 1), 2 = External (eFuse 2). |
name | commit summary | Build name shown in the web UI. |
api_base | https://www.embeddedci.com | EmbeddedCI base URL. When empty, BENCHPOD_API_BASE from the environment is used before the public server. |
sdk_ref | empty | Tag, branch or commit of embeddedci-python to install instead of the PyPI release (embeddedci==2.5.*). |
python_version | 3.12 | Python set up for the upload step. |
allow_missing_token | false | true skips the upload instead of failing when no OIDC token is available, e.g. on a pull request from a fork. |
Output: build_id, the embeddedci build the artifacts were attached to (empty when the upload was skipped by allow_missing_token).
Pull requests from forks cannot mint an OIDC token. Set allow_missing_token: "true" to skip the upload there instead of failing the job.
Flash an uploaded build
- Open the pod from your BenchPod list and go to the Flash tab.
- Pick the build under Firmware artifact. Each entry shows its branch and commit.
- Check the OpenOCD target, SWD channels, reset and target power. They come from the action's inputs; change them if this pod is wired differently.
- Click flash. The server runs OpenOCD against the pod and shows the result.
Nothing runs on your machine, so this works from any browser, for any pod your organization can reach.
Flashing from pytest, the SDK, MCP or the CLI
These flash either a local file or a build uploaded here. A local file needs no upload: OpenOCD runs on the machine that runs your code and talks to the pod's CMSIS-DAP probe over the network or the cloud, so that machine needs an OpenOCD build with the cmsis-dap TCP backend (newer than 0.12.0). An uploaded build is named by its build id (or latest) and/or its repository and branch or tag; on an embeddedci:<pod> connection embeddedci.com runs OpenOCD, as the Flash tab does, and on a LAN or USB connection the file is downloaded and flashed locally.
| From | Local file | Uploaded build |
|---|---|---|
| Web app Flash tab | — | Pick it under Firmware artifact |
Python SDK and pytest bp.flash() | file= (pytest: --benchpod-firmware) | build=, repo=, ref=, artifact= |
MCP flash tool | file, a path on the MCP server's host | build, repo, ref, artifact |
CLI benchpod flash | --file | --build, --repo, --ref, --artifact |
Without an artifact name the build's .elf is flashed, else its .hex, else its .bin. An omitted target config falls back to the openocd_target the upload recorded. Looking a build up needs a sign-in: an API key, benchpod login, or the GitHub Actions OIDC session the SDK already uses for a cloud pod. A flash run by embeddedci.com always verifies and resets; pass server_side=False (CLI: --server-side=false) on a LAN pod connection to flash with your own OpenOCD options.
Python SDK:
from embeddedci.benchpod import BenchPod
with BenchPod("embeddedci:my-pod", la_voltage=3.3) as bp:
# SWD pins, reset and target config come from the pod's wiring profile.
bp.flash(file="build/app.elf", target="target/stm32f4x.cfg", target_power=1)pytest:
pytest --benchpod-connection=embeddedci:my-pod --benchpod-firmware=build/app.elf
# in the test:
def test_boots(benchpod, firmware):
assert benchpod.flash(file=firmware, target="target/stm32f4x.cfg").okMCP (the agent passes a path on the host running the MCP server):
flash(file="build/app.elf", target="target/stm32f4x.cfg", target_power=1)
CLI:
benchpod flash --target target/stm32f4x.cfg --file build/app.elf
An uploaded build, from the SDK:
import os
with BenchPod("embeddedci:my-pod", la_voltage=3.3) as bp:
# the newest build of main; embeddedci.com runs OpenOCD for a cloud pod
result = bp.flash(build="latest", repo="my-org/my-firmware", ref="main", target_power=1)
print(result.artifact["name"], result.artifact["sha"])
# or the exact build the upload step printed (its build_id output)
bp.flash(build=os.environ["BUILD_ID"])From MCP:
flash(repo="my-org/my-firmware", ref="main", target_power=1)
From the CLI (no local OpenOCD needed over embeddedci.com):
benchpod flash --connection embeddedci:my-pod --repo my-org/my-firmware --ref main benchpod flash --connection embeddedci:my-pod --build 3f9c2a1e-... # the upload's build_id