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_pcb

The 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

InputDefaultWhat it does
api_keyrequiredAn EmbeddedCI API key carrying the emi:analyze scope.
boardrequiredPath to the board: a .kicad_pcb, a zip of the KiCad project, or a zip of Gerbers + drill + IPC-D-356 netlist.
projectrepository nameThe 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_kindinferredkicad or gerber.
fail_oncriticalcritical, warning, or none.
api_basehttps://www.embeddedci.comOverride for a self-hosted server.
timeout_seconds300How long to wait for the run to finish.
summarytrueWrite 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

OutputWhat it is
critical / warning / infoFinding counts by severity.
rules_jsonPath to the downloaded findings on the runner, for a step that wants the detail.
run_idThe EMI run, for looking the results up in the web UI.
board_id / project_idThe 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.

RuleFinds
return-viaA layer change with no ground via near it, so the return current has to detour.
plane-gapA net crossing a gap in its reference plane.
via-stubThe unused remainder of a through via, which resonates at a predictable frequency.
radiatorA net long enough to be a meaningful fraction of a wavelength (past about λ/20).
edge-proximityA 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: gerber

Net 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 the emi:analyze scope. 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. Check api_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 in new until 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