Signal K provides a reusable GitHub Actions workflow that tests your plugin across all platforms where Signal K server runs. Even plugins without a test suite benefit — the workflow validates your plugin's structure, entry point, configuration schema, lifecycle, and API usage.
Create .github/workflows/signalk-ci.yml in your plugin repository:
name: SignalK Plugin CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
test:
uses: SignalK/signalk-server/.github/workflows/plugin-ci.yml@master
Push to GitHub — your plugin is now tested on Linux (x64 + arm64), macOS, Windows, and armv7 (Cerbo GX).
Add workflow_dispatch to get a "Run workflow" button in the GitHub Actions UI where you can override Node versions, toggle armv7/Cerbo GX testing, enable integration tests, and more — without editing your workflow file.
Because workflow_call and workflow_dispatch inputs are separate namespaces in GitHub Actions, the workflow needs two jobs: one for automatic runs (push/PR) with hardcoded defaults, and one for manual runs that passes through your form inputs.
See examples/plugin-caller-example.yml for the full workflow with manual trigger support.
| Platform | Architecture | Node versions | Notes |
|---|---|---|---|
| Linux | x64 | 22, 24 | GitHub-hosted runner |
| Linux | arm64 | 22, 24 | GitHub-hosted runner — Raspberry Pi 4/5 |
| macOS | arm64 | 22, 24 | GitHub-hosted runner |
| Windows | x64 | 22, 24 | GitHub-hosted runner |
| Linux | armv7 (Cerbo GX) | 20 | QEMU emulation — matches Venus OS 3.70 (Node 20) |
The desktop jobs (Linux, Linux arm64, macOS, Windows) run these checks, even if your plugin has no test suite. The list below is a summary for readers — the authoritative source for what the CI actually validates is the workflow itself: .github/workflows/plugin-ci.yml.
package.json — signalk-node-server-plugin keyword, main or exports field, engines.node declaration
Entry point — After build, verifies the plugin exports a constructor function
plugin.schema() — Calls schema() and checks it returns a JSON-serializable schema-like object without crashing (not fully validated against the JSON Schema meta-schema)
Lifecycle — Runs start() → stop() → start() (restart) with an empty configuration. Validates delta messages emitted during startup and checks that registerDeltaInputHandler handlers forward deltas correctly.
API usage — Scans source files for:
setProviderStatus → setPluginStatus, setProviderError → setPluginError)app.server, app.deltaCache, app.pluginsMap)app.get() instead of registerWithRouter())__dirname or process.cwd() instead of app.getDataDirPath())app.securityStrategy or isDummy() — plugin routes are already protected by the server)node:sqlite requires engines.node >= 22.5.0)npm pack — Verifies all files referenced by main/exports are included in the published package
App Store compatibility — Installs the plugin with --ignore-scripts (as the App Store does) and checks for native addon dependencies
Stray files — Warns when build and test steps leave untracked files
Override defaults by passing inputs to the shared workflow:
jobs:
test:
uses: SignalK/signalk-server/.github/workflows/plugin-ci.yml@master
with:
test-command: 'npm run test:ci'
build-command: 'npm run build:plugin'
enable-armv7: false
enable-signalk-integration: true
node-versions: '["22"]'
| Input | Default | Description |
|---|---|---|
test-command |
npm test |
Command to run your test suite |
build-command |
npm run build --if-present |
Build command |
format-check-command |
(empty) | Blocking format check (e.g. npm run prettier:check, npx biome check .); skipped when empty |
coverage-command |
(empty) | Runs tests with coverage (e.g. npm run coverage); replaces the standard test run and writes output to the step summary |
node-versions |
["22", "24"] |
Node versions for desktop platforms |
enable-armv7 |
true |
Test on armv7 (Cerbo GX) via QEMU |
enable-signalk-integration |
false |
Start SignalK server for integration tests |
signalk-server-versions |
["latest"] |
JSON array of signalk-server versions; the integration job fans out over each |
Both are tool-agnostic command strings — the workflow doesn't care whether you use Prettier/Biome or c8/nyc/jest --coverage. Leave either empty to opt out.
with:
format-check-command: 'npm run prettier:check'
coverage-command: 'npm run coverage'
format-check-command runs after lint and blocks the job if it fails (unlike npm run lint --if-present, which is advisory). coverage-command replaces the standard Run tests step — its stdout is captured and appended to the GitHub Actions step summary so you can see coverage output without digging through logs.
The CI validates the same fields described in the publishing guide. The most important for CI:
keywords must include signalk-node-server-pluginmain or exports must point to your entry fileengines.node should declare the minimum Node.js version (required if you use node:sqlite or other version-specific built-in modules)Plugins without a test script still get all validation checks — tests are skipped with a notice.
The Cerbo GX runs an Allwinner dual-core Cortex-A7 (ARMv7, 32-bit) with Venus OS. The CI emulates this environment using QEMU with a node:20-bookworm-slim Docker image plus python3, make, and g++ — matching Venus OS 3.70 which ships Node 20 and has build tools available via opkg.
The armv7 job runs install, build, and tests — it does not repeat the full validation suite (that's covered by the desktop jobs). The armv7 Node version is fixed to match the Cerbo GX and is not user-configurable. Expect armv7 jobs to take 3-5x longer than native x64. armv7 failures are advisory and non-blocking.
Enable enable-signalk-integration: true to run your plugin against a real Signal K server.
The job installs a Signal K server, packs and installs your plugin, auto-enables it, and starts the server with sample NMEA 0183 + NMEA 2000 data so the plugin has a realistic data environment (navigation, wind, depth, temperature, battery, and more). It then verifies the plugin loaded, checks provider API registrations, and runs npm run test:integration if defined. Your tests receive SIGNALK_URL=http://localhost:3000 to connect to the running server.
The authoritative sequence of steps lives in the workflow itself: .github/workflows/plugin-ci.yml.
Pass signalk-server-versions as a JSON array to fan the integration job out over multiple server versions — useful for catching regressions across the baconjs 1 → 3 transition (server 2.23.x vs 2.24.0+) and similar cross-generation breakage:
with:
enable-signalk-integration: true
signalk-server-versions: '["2.23.0", "latest"]'
The integration job runs the full Cartesian product of node-versions × signalk-server-versions. The default ["22", "24"] × ["latest"] is 2 jobs; ["22", "24"] × ["2.23.0", "latest"] is 4. To keep the matrix small, shrink either dimension — integration coverage often only needs a single Node version (node-versions: '["22"]') even when the desktop jobs exercise several.
If your plugin registers as a provider for one of the server's provider APIs, the integration test verifies the registration actually works by calling the corresponding endpoint:
| Provider API | Registration method | Endpoint checked |
|---|---|---|
| History API v2 | app.registerHistoryApiProvider() |
/signalk/v2/api/history/values must not return 501 |
This catches a common class of bugs where a plugin calls a registration method but the endpoint still returns "no provider configured" — for example due to an API mismatch between the plugin and the server version being tested.
For testing against actual hardware (GPIO, CAN bus, serial ports), add a self-hosted runner on a Cerbo GX or Raspberry Pi:
test-cerbo-hardware:
name: Cerbo GX (real hardware)
runs-on: [self-hosted, cerbo-gx]
steps:
- uses: actions/checkout@v6
- run: npm ci
- run: npm test