EMI analysis in CI
Run the EMI Analyzer against a board committed to your repository, on every push that touches the layout. The action uploads the board, waits for the rules tier — geometric checks that take seconds, not a full-wave solve — annotates the findings on the workflow run, and fails the build on the severity you choose. For why any of this catches real radiation problems, read EMI Analysis in CI.
1. Create an API key
Unlike the HIL actions, this one authenticates with an API key, not the job's GitHub OIDC token. An EMI run is filed under the organisation that owns the key, so the credential has to identify a person rather than a repository — there is nothing to add to permissions: and no trusted-repo entry to create.
Create the key with the emi:analyze scope and nothing else, then store it as a repository secret named EMBEDDEDCI_API_KEY. A key missing that scope is refused with a message saying so; a key that also carries benchpod:control would let a compromised workflow drive your hardware, which is exactly what per-scope keys exist to prevent.
2. Add the action
That is the whole workflow — no toolchain to install, no runner requirements beyond curl and jq, which every GitHub runner already has.
name: EMI
# Only when the layout actually changed. Without a paths filter this re-analyses
# an unchanged board on every commit.
on:
push:
paths:
- "hardware/**.kicad_pcb"
pull_request:
paths:
- "hardware/**.kicad_pcb"
jobs:
emi:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: embeddedci-com/embeddedci-github-action/emi@main
with:
api_key: ${{ secrets.EMBEDDEDCI_API_KEY }}
board: hardware/mainboard.kicad_pcbThe paths filter matters more than it looks. The action is cheap but not free, and re-analysing a board that did not change tells you nothing — the finding count is identical by construction.
3. Inputs
| Input | Default | What it does |
|---|---|---|
api_key | required | An EmbeddedCI API key carrying the emi:analyze scope. |
board | required | Path to the board: a .kicad_pcb, a zip of the KiCad project, or a zip of Gerbers + drill + IPC-D-356 netlist. |
project | repository name | The EMI project boards are filed under. Keep it stable — boards accumulate in it across commits, and that history is what a later comparison reads. |
source_kind | inferred | kicad or gerber. |
fail_on | critical | critical, warning, or none. |
api_base | https://www.embeddedci.com | Override for a self-hosted server. |
timeout_seconds | 300 | How long to wait for the run to finish. |
summary | true | Write the findings table to the job summary. |
4. Choosing what fails the build
fail_on: critical is the default deliberately. Gating on warnings sounds stricter, but a board of any real complexity carries warnings a human has already looked at and accepted — a via stub on a net that never switches fast, a long trace that happens to be a DC rail. A check that cries wolf on every push gets switched off within the week, and then it catches nothing at all.
Start at critical. If your boards come back clean and you want more pressure, move to warning once you have triaged the existing ones.
fail_on: none never fails the build. Use it to collect the findings and decide in a later step:
- uses: embeddedci-com/embeddedci-github-action/emi@main
id: emi
with:
api_key: ${{ secrets.EMBEDDEDCI_API_KEY }}
board: hardware/mainboard.kicad_pcb
fail_on: none # collect the findings, decide for yourself
- name: Post the count
run: |
echo "critical=${{ steps.emi.outputs.critical }}"
echo "warning=${{ steps.emi.outputs.warning }}"
jq '.findings[] | select(.severity == "critical")' \
"${{ steps.emi.outputs.rules_json }}"5. Outputs
| Output | What it is |
|---|---|
critical / warning / info | Finding counts by severity. |
rules_json | Path to the downloaded findings on the runner, for a step that wants the detail. |
run_id | The EMI run, for looking the results up in the web UI. |
board_id / project_id | The board and the project it was filed under. |
Every finding is also emitted as a GitHub annotation — ::error for critical, ::warning, ::notice — so they appear against the workflow run rather than only in the log, and the counts go to the job summary with a link back to the board viewer.
rules_json points at the raw findings file:
{
"format_version": 1,
"summary": {
"critical": 1,
"warning": 3,
"info": 2,
"assumed_max_frequency_hz": 1000000000
},
"findings": [
{
"id": "return-via-0",
"rule": "return-via",
"severity": "critical",
"title": "Layer change on USB_DP has no return via within 3 mm",
"detail": "nearest ground via is 7.2 mm away",
"net": "USB_DP",
"layer": "In1.Cu",
"x": 41.275,
"y": 22.86
}
]
}Each finding carries board coordinates, so the same data drives the clickable overlay in the web viewer.
6. What the rules tier checks
Five geometric checks, each with an electromagnetic justification. The companion article explains why each one radiates.
| Rule | Finds |
|---|---|
return-via | A layer change with no ground via near it, so the return current has to detour. |
plane-gap | A net crossing a gap in its reference plane. |
via-stub | The unused remainder of a through via, which resonates at a predictable frequency. |
radiator | A net long enough to be a meaningful fraction of a wavelength (past about λ/20). |
edge-proximity | A net routed close enough to the board edge that fields fringe off it. |
7. Board formats
KiCad is the shortest path: a .kicad_pcb already carries nets, traces, vias, pads and the stackup, so nothing has to be reconstructed.
Gerbers work too, but the zip must include the drill file and an IPC-D-356 netlist:
- uses: embeddedci-com/embeddedci-github-action/emi@main
with:
api_key: ${{ secrets.EMBEDDEDCI_API_KEY }}
board: fab/mainboard-gerbers.zip
source_kind: gerberNet names arrive truncated to 14 characters on the Gerber path. That is a property of the IPC-D-356 format, not something the tool can recover.
8. When it fails
The action distinguishes its failure modes rather than reporting one generic error, so the message names the fix:
rejected the API key— the key is invalid, revoked, or missing theemi:analyzescope. Check it on the API keys page; scopes are editable after creation.could not reach …— a network or DNS problem on the runner, not a credential problem. Checkapi_base.timed out … If it never left 'new', no EMI worker was online— the run was queued but nothing picked it up. Workers dial in like build agents; if your organisation has none online, the run sits innewuntil it times out.no uploaded object at that key— the upload did not complete. Usually a very large board against a short job timeout.EMI run failed— the worker could not parse the board. The message carries the parser's reason; a Gerber zip missing its IPC-D-356 netlist is the common one.
Re-running a workflow on an unchanged board does not re-upload it. The action sends the file's SHA-256 first and the server recognises bytes it already holds, so a retry costs one request rather than tens of megabytes.
What this is not
A green check here is not a compliance prediction, and the tool will never print one. It does not model your components, cannot see common-mode current on an attached harness — which is what most real EMC failures below ~300 MHz actually are — and its absolute numbers depend on a stackup you supplied.
What survives all of that is the comparison: this layout against the last one, same board, same assumptions, errors cancelling between the two runs. That is the claim CI is uniquely placed to make, and it is the reason to wire this up. The companion article makes the argument in full, including the complete list of limitations.
Where to next
- EMI Analysis in CI — why these checks catch real radiation problems, and what the tool cannot tell you.
- HTTP API & API keys — scopes, and the REST endpoints behind this action.
- Run HIL in GitHub Actions — the other half of hardware CI: firmware on a real board.