Register runners with dedicated labels only. GitHub adds the default self-hosted label unless you override it during registration; remove that label from the provisioning and test runners so they never pick up build or cross-rig jobs.
| Runner | Labels | Purpose |
|---|---|---|
*-build-A/B/C |
self-hosted-build |
Firmware builds, compliance, SonarCloud |
*-host |
self-hosted-test |
Coredump and FOTA on DUT 2, FOTA on DUT 5 |
*-prov |
self-hosted-provisioning |
Provisioning on DUT 1, DUT 3 and DUT 4, FOTA on DUT 3 |
Example registration for the provisioning rig:
# On the provisioning rig — use a new runner name, e.g. smha-provisioning
mkdir actions-runner-provisioning && cd actions-runner-provisioning
curl -o actions-runner-linux-x64-2.XXX.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.XXX.0/actions-runner-linux-x64-2.XXX.0.tar.gz
tar xzf ./actions-runner-linux-x64-*.tar.gz
./config.sh --url https://github.com/<org>/<repo> --token <registration-token> \
--name smha-provisioning --labels self-hosted-provisioning,Linux,X64 --unattended
sudo ./svc.sh install && sudo ./svc.sh start
Example for a build runner:
./config.sh --url https://github.com/<org>/<repo> --token <registration-token> \
--name smha-build-a --labels self-hosted-build,Linux,X64 --unattended
Example for the test rig (DUT 2):
./config.sh --url https://github.com/<org>/<repo> --token <registration-token> \
--name smha-test --labels self-hosted-test,Linux,X64 --unattended
Both runners need Docker and USB access to their DKs (--privileged -v /dev:/dev in the test workflow containers). Set the GitHub repository variables for each rig on the same repo (CI_NRF54L15_PROVISION_* and CI_NRF54L15_PROVISION_SERIAL_MODEM_* for DUT 1, CI_NRF54L15_* and CI_NRF54L15_SERIAL_MODEM_* for DUT 2, CI_NRF54LM20B_PROVISION_* and CI_NRF54LM20B_PROVISION_SERIAL_MODEM_* for DUT 3, CI_NRF93M1_PROVISION_* for DUT 4, CI_NRF93M1_* for DUT 5). The provisioning runner needs access to DUT 1, DUT 3 and DUT 4, and the test runner to DUT 2 and DUT 5; tests select their board by SEGGER serial number, so the DKs can share one runner host.
91m1 on-target tests flash the newest upstream Serial Modem release that ships the external-MCU zip on the nRF9151 DK before programming the host.
The test DUT must be provisioned once (manually or by running the provisioning flow locally against it). CI flashes baseline firmware without recovery, so TF-M credentials persist. FOTA and coredump do not remove the device from nRF Cloud or Memfault after each run.
For the first-time setup for the test DUT (CI_NRF54L15_*): follow 91m1_ppp cloud provisioning steps 3–6 on that board, or run the provisioning test locally with TEST_JSON from 91m1_ppp-provision-nrf54l15-nrf91 while pointing the CI_NRF54L15_PROVISION_* variables at the test board (once only). Register the device in the shared Memfault cohort ci-91m1-test-nrf54l15-nrf91 (used by both coredump and FOTA tests).
The nRF54LM20B FOTA test reuses DUT 3 (CI_NRF54LM20B_PROVISION_*) on the provisioning runner. The device must reach cloud connect before CI; the test calls ensure_provisioned() on the first run if credentials are missing and assigns the DUT to the Memfault cohort ci-91m1-test-nrf54lm20b-nrf91 (separate from the provision cohort). When provision and FOTA run in the same CI batch, the FOTA test moves the device into the test cohort automatically.
The nRF54LM20B build reports Memfault hardware version smha-nrf54lm20dk instead of the NCS default board name (see boards/nrf54lm20dk_nrf54lm20b_cpuapp_ns.conf). The shared Memfault project binds nrf54lm20dk to another software type, so OTA payloads for smha-91m1 are rejected under that name. The nRF93M1 DK build reports smha-nrf93m1dk for the same reason (see boards/nrf93m1dk_nrf54l15_cpuapp_ns.conf): nrf93m1dk is bound to another software type, so OTA payloads for smha-93m1 are rejected under that name.
Memfault coredump tests connect to nRF Cloud, trigger mflt test busfault over the shell, and verify a new bus fault coredump for the device appears in the Memfault Traces REST API. "New" means newer than the device's newest coredump recorded before the fault, so the check does not depend on the device clock agreeing with the runner. A bus fault is used because TF-M traps HardFaults before Memfault's handler runs. Local run:
export REPO_ROOT=$PWD
export TEST_JSON="$(PYTHONPATH=tests/on_target python3 -m ci.catalog load 91m1_ppp-memfault-coredump-nrf54l15-nrf91)"
PYTHONPATH=tests/on_target pytest tests/on_target/tests/test_memfault/ -c tests/on_target/tests/pytest.ini -v
Run coredump then FOTA locally (same order as CI):
export REPO_ROOT=$PWD
export TEST_JSON="$(PYTHONPATH=tests/on_target python3 -m ci.catalog load 91m1_ppp-memfault-coredump-nrf54l15-nrf91)"
PYTHONPATH=tests/on_target pytest tests/on_target/tests/test_memfault/ -c tests/on_target/tests/pytest.ini -v
export TEST_JSON="$(PYTHONPATH=tests/on_target python3 -m ci.catalog load 91m1_ppp-application-fota-nrf54l15-nrf91)"
PYTHONPATH=tests/on_target pytest tests/on_target/tests/test_fota/ -c tests/on_target/tests/pytest.ini -v
Local run for nRF54LM20B FOTA (plain nrf54lm20b build, same DUT as provision tests):
export REPO_ROOT=$PWD
export TEST_JSON="$(PYTHONPATH=tests/on_target python3 -m ci.catalog load 91m1_ppp-application-fota-nrf54lm20b-nrf91)"
PYTHONPATH=tests/on_target pytest tests/on_target/tests/test_fota/ -c tests/on_target/tests/pytest.ini -v
The nRF93M1 FOTA test runs on its own nRF93M1 DK (DUT 5, CI_NRF93M1_*, host console on VCOM0) in Memfault cohort ci-93m1-test-nrf93m1. Like the nRF54LM20B FOTA test, it provisions the board through ensure_provisioned() on the first run. Local run:
export REPO_ROOT=$PWD
export TEST_JSON="$(PYTHONPATH=tests/on_target python3 -m ci.catalog load 93m1_ppp-application-fota-nrf93m1)"
PYTHONPATH=tests/on_target pytest tests/on_target/tests/test_fota/ -c tests/on_target/tests/pytest.ini -v
Set NRF_CLOUD_*, MEMFAULT_*, and the host plus Serial Modem CI_NRF54L15_* / CI_NRF54L15_PROVISION_* / CI_NRF54LM20B_PROVISION_* / CI_NRF93M1_PROVISION_* / CI_NRF93M1_* variables/secrets documented in .github/workflows/test.yml.