Run firmware tests in CI against a simulated MCU. No hardware, no toolchain, no self-hosted runner.
name: firmware tests
on: [push, pull_request]
jobs:
labwired:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # hosted report
pull-requests: write # results comment
steps:
- uses: actions/checkout@v4
- uses: labwired/firmware-test@v1
with:
script: tests/labwired.ymlThat's it — no API key and no signup. The simulation runs on your own GitHub runner; LabWired hosts the report.
| Input | Default | Description |
|---|---|---|
script |
(required) | Path to your LabWired test script YAML. |
firmware |
"" |
Firmware ELF, if not set inside the script. |
system |
"" |
System manifest YAML, if not set inside the script. |
output_dir |
out/artifacts |
Where result.json, uart.log and junit.xml land. |
version |
v0.18.0 |
LabWired CLI release tag. |
gallery |
true |
List public repos at https://app.labwired.com/ci. Set false to stay unlisted. |
comment |
true |
Post a results comment on pull requests. |
allow_unproven |
false |
Let an unproven run pass the job. |
status (pass/fail/error/unproven), report_url, artifacts_dir.
A twin imported from a schematic contains only the parts LabWired can model.
The importer lists the rest as design-only, and LabWired's compiler writes that
list into the system manifest as one coverage: line. A run on such a twin can
pass every assertion and still say nothing about the board you designed: the
op-amp that is not on the twin explains a missing ADC reading as well as a
firmware bug does.
So when the manifest carries a coverage record, the action reports one of four verdicts instead of three:
| Verdict | When |
|---|---|
error |
The run did not complete. Coverage is not consulted. |
unproven |
The run completed, and design-only parts are still missing from the twin, or it passed without asserting anything. |
fail |
An assertion failed on a complete twin. |
pass |
Every assertion passed on a complete twin. |
An unproven run fails the job with exit code 4 and an error naming the missing
parts. The summary and pull request comment list them, and the gallery shows
the run as Unproven. To accept an unproven run while the catalog catches up
with your design, set allow_unproven: true. The job's result then follows
the simulator's exit code, and every report still says unproven.
A manifest with no coverage record, which includes every hand-written one, is judged exactly as before.
Vendor the reporter into an azure/ directory in your repository — the
pipeline, report_azure.py, and the two modules it shares with the GitHub
action (verdict.py and comment.py):
mkdir -p azure
base=https://raw.githubusercontent.com/LabWired/firmware-test/main
curl -fsSL -o azure/azure-pipelines.yml "$base/azure/azure-pipelines.yml"
curl -fsSL -o azure/report_azure.py "$base/azure/report_azure.py"
curl -fsSL -o azure/verdict.py "$base/verdict.py"
curl -fsSL -o azure/comment.py "$base/comment.py"Set script in the pipeline and push. The same pinned CLI release runs on the
agent and the run reports natively:
| Surface | Where | Needs |
|---|---|---|
| Assertions | Tests tab, via PublishTestResults@2 on the CLI's junit.xml |
nothing |
| Build summary | Build summary tab (##vso[task.uploadsummary]) |
nothing |
| Build tag | labwired-pass / labwired-fail / labwired-error / labwired-unproven |
nothing |
| Pull request | one upserted thread comment | System.AccessToken mapped (the example does) and the build service identity granted Contribute to pull requests |
| Artifacts | result.json, uart.log, junit.xml, labwired-summary.md |
nothing |
| Hosted report | linked from the summary, listed in the gallery | System.AccessToken mapped (the example does) and a public Azure DevOps project |
The build fails on fail and error, and on unproven unless
allow_unproven: true — the same verdicts and exit code 4 as the GitHub
action. The reporter reuses the action's verdict.py; to pick up reporter
fixes, re-run the vendoring commands above.
The hosted report is authenticated by the pipeline's own System.AccessToken:
LabWired asks Azure DevOps for the build record, so no secret is created or
stored. Private projects are rejected server-side before the run body is read.
Set gallery: false to keep the run unlisted and send run metadata only — no
firmware bundle leaves the agent.
Private repositories are never uploaded: the action checks
github.event.repository.private before sending anything, so no request
leaves the runner for a private repo (the API also rejects private repos
server-side, as a backstop).
Public repositories are listed in the public gallery by default and their
result metadata plus firmware artifact bundle are uploaded. Set
gallery: false to opt out — this does not merely unlist the run, it changes
what is sent: only run metadata (status, test counts, board, timings) is
uploaded, and the firmware artifact bundle is never sent, because a stored
bundle is retrievable unauthenticated by hash once it exists. Email
andrii@labwired.com to have an existing gallery listing removed.
Uploading is best-effort — if the LabWired API is unreachable, your build still passes or fails on the test result alone.
MIT.