This repository contains reusable GitHub Actions CI workflows for openDAQ-based module projects.
The unified reusable.yml workflow is designed to provide a centralized approach to CI management and compatibility testing of openDAQ-based projects against upstream SDK. It dynamically configures CI pipelines from caller inputs — handling toolchain selection and environment setup — and orchestrates jobs across target platforms, architectures, and compilers.
- uses: openDAQ/openDAQ-CI/.github/workflows/reusable.yml@main
with:
# Override openDAQ SDK git reference.
# Format: tag, branch, or SHA
# Optional: '' - empty string, resolved by the caller
opendaq-ref: ''
# Exclude matrix jobs by name.
# Format: JSON array of strings
# Pattern: wildcard
# Optional: [] empty, no jobs excluded
# Example: '["*-debug", "macos-*"]'
exclude-jobs: ''
# Install additional packages per job.
# Format: JSON array of objects
# Fields:
# match-jobs — job names list (optional: ["*"] default)
# apt-install — apt packages (optional, collect)
# pip-install — pip packages (optional, collect)
# brew-install — brew packages (optional, collect)
# choco-install — choco packages (optional, collect)
# winget-install — winget packages (optional, collect)
# run — run after packages setup (optional, last match wins)
# Optional: [] empty, no extra packages
# Example: >
# [
# {
# "match-jobs": ["ubuntu-*"],
# "apt-install": ["libpcap-dev", "mosquitto"],
# "run": "mosquitto -d"
# },
# {
# "match-jobs": ["windows-*"],
# "winget-install": ["EclipseFoundation.Mosquitto"]
# },
# {
# "match-jobs": ["macos-*"],
# "brew-install": ["mosquitto"],
# "run": "$(brew --prefix mosquitto)/sbin/mosquitto -d"
# }
# ]
packages: ''
# Map CMake presets to jobs.
# Format: JSON array of objects
# Fields:
# match-jobs — job names list (optional: ["*"] default)
# configure-preset — configure preset name (mostly required)
# inherited, toolchains and build type are ignored
# test-preset — test preset name (optional)
# if omitted, tests will not run for matched jobs
# Optional: [] empty, no configuration inherited, no tests run
# Example: >
# [
# {
# "configure-preset": "module",
# "test-preset": "module-test"
# }
# ]
cmake-presets: ''
# Artifact names pattern, `*` is replaced with original name.
# Use to avoid conflicts when calling reusable.yml multiple times within the same workflow.
# Format: string with `*` placeholder
# Optional: '*' default
# '' disables artifact upload
# Example: 'full-*'
upload-pattern: ''
# Override job timeout per platform.
# Format: JSON array of objects
# Fields:
# match-jobs — job names list (optional: ["*"] default)
# timeout-minutes — job timeout in minutes (required)
# Optional: [] empty, default 120 minutes
# Example: >
# [
# {
# "timeout-minutes": 180
# },
# {
# "match-jobs": ["windows-*"],
# "timeout-minutes": 240
# }
# ]
timeout: ''| Output | Description |
|---|---|
matrix |
Generated jobs matrix content, JSON |
cmake-user-presets |
Generated CMakeUserPresets.json content |
The workflow runs in two stages:
- Generate — configures the pipeline: creates the jobs matrix and CMake user presets from caller inputs
- CI — uses a matrix strategy to run each job on a dedicated runner within its own environment
Every CI job has a unique name that serves as a key — the caller uses it to request additional packages for the environment and to map CMake presets for building and testing the project. Job names follow this pattern:
<os>-<arch>-<generator>-<compiler>-<build_type>The pattern components and their values are listed in the platform naming table:
| os | arch | generator | compiler | build_type |
|---|---|---|---|---|
| windows-2025 | x86_64 | msvs | v143 | debug |
| windows-2025 | x86 | msvs | v143 | release |
| ubuntu-20.04 | x86_64 | ninja | gcc-7 | release |
| ubuntu-20.04 | x86_64 | ninja | clang-9 | release |
| ubuntu-24.04 | x86_64 | ninja | gcc-14 | debug |
| ubuntu-24.04 | x86_64 | ninja | gcc-14 | release |
| ubuntu-24.04 | x86_64 | ninja | clang-18 | release |
| macos-26 | x86_64 | ninja | appleclang | debug |
| macos-26 | armv8 | ninja | appleclang | release |
| macos-15 | x86_64 | ninja | appleclang | release |
Example: ubuntu-24.04-x86_64-ninja-gcc-14-release
This naming approach allows the caller to flexibly configure which jobs to run or to set additional parameters for a specific job or a group of jobs using wildcard patterns. Moreover, using an array of patterns or full names enables targeting multiple jobs with a single argument.
The generate stage takes the full set of matrix jobs and processes them as follows:
Exclude filtering. Each job name is matched against exclude-jobs patterns. Filters are applied until the first match — if a match is found, the job is excluded from the matrix. If no patterns match, the job is created.
Package resolution. The packages array is iterated in order. For each entry, the match-jobs filter is applied to the job name — if it matches, the corresponding package lists are appended to the packages that will be installed during that job's execution. The run command, if specified, is executed after package installation; each subsequent match overrides the previous — last match wins. The run value can be a direct command or a path (absolute or relative to the working directory) to a shell script within the project repository.
Preset mapping. The cmake-presets array is iterated in the same way. If the job name matches the match-jobs filter, the configure and test preset names are recorded in the matrix entry to be used as inherited presets in the generated CMakeUserPresets.json. If the configure preset is explicitly set to an empty string, the job will run with a default preset containing only the compiler and build type. If no test preset is provided or it is set to an empty string explicitly, tests will not run for that job.
Artifact naming. GTest results are uploaded as artifacts named test-results-<job-name>. If the reusable workflow is called more than once within the same workflow, artifact names will collide and produce an error. To avoid this, pass upload-pattern with a * placeholder — each call should have its own unique pattern. The * is replaced with the original artifact name, e.g. my-call (*) renames test-results-windows-2025-x86_64-msvs-v143-release into my-call (test-results-windows-2025-x86_64-msvs-v143-release).
CMake user presets. Based on the matrix — specifically build type, compiler, and inherited preset names — the generate stage produces a CMakeUserPresets.json file with preset names matching job names. These presets are then used in CMake commands for configuration, building, and running tests. Output directories, build types, compilers, and generators are set in the generated presets to ensure consistent CMake invocations across the execution environment. Even if these parameters are defined in the parent preset, the generated user preset overrides them.
Each CI matrix job runs the following steps:
# Checkout the caller project
git checkout <project-url>
# Install packages: ubuntu
sudo apt-get install -y <apt-packages>
# Install packages: macos
brew install <brew-packages>
# Install packages: windows
choco install -y <choco-packages>
winget install --id <winget-package> #; winget install --id <winget-another-package>; <etc>
# Install packages: all
pip install <pip-packages>
# Run post-package command (if passed via packages input)
<run-command>
# Write generated CMake user presets
cat > CMakeUserPresets.json <<'EOF'
<generated-presets>
EOF
# Configure
cmake --preset <job-name>
# Build
cmake --build --preset <job-name>
# Test (if test preset is passed via cmake-presets input)
ctest --preset <job-name>The recommended approach is to create minimal configure and test presets in your module's CMakePresets.json:
{
"version": 4,
"configurePresets": [
{
"name": "module",
"hidden": true,
"cacheVariables": {
"MY_MODULE_ENABLE_TESTS": "ON"
}
}
],
"testPresets": [
{
"name": "module-test",
"hidden": true,
"configurePreset": "module"
}
]
}Then call the workflow without specifying match-jobs, implying the presets apply to all configurations:
jobs:
ci:
name: My Module
uses: openDAQ/openDAQ-CI/.github/workflows/reusable.yml@main
with:
cmake-presets: >
[
{
"configure-preset": "module",
"test-preset": "module-test"
}
]To build against a specific openDAQ SDK git ref:
jobs:
ci:
name: My Module
uses: openDAQ/openDAQ-CI/.github/workflows/reusable.yml@main
with:
opendaq-ref: 'main'
cmake-presets: >
[
{
"configure-preset": "module",
"test-preset": "module-test"
}
]To exclude all debug jobs and all macOS ARM jobs:
jobs:
ci:
name: My Module
uses: openDAQ/openDAQ-CI/.github/workflows/reusable.yml@main
with:
exclude-jobs: '["*-debug", "macos-*-armv8-*"]'
cmake-presets: >
[
{
"configure-preset": "module",
"test-preset": "module-test"
}
]To enable tests for all jobs but skip them on macOS. The first entry implicitly applies configure-preset to all jobs, while the second explicitly clears test-preset for macOS, so tests will not run on those jobs:
jobs:
ci:
name: My Module
uses: openDAQ/openDAQ-CI/.github/workflows/reusable.yml@main
with:
cmake-presets: >
[
{
"configure-preset": "module",
"test-preset": "module-test"
},
{
"match-jobs": ["macos-*"],
"test-preset": ""
}
]To install extra packages on specific platforms:
jobs:
ci:
name: My Module
uses: openDAQ/openDAQ-CI/.github/workflows/reusable.yml@main
with:
packages: >
[
{
"match-jobs": ["ubuntu-*"],
"apt-install": ["libpcap-dev"]
}
]When multiple entries match the same job, packages are chained. For example:
packages: >
[
{
"match-jobs": ["ubuntu-*"],
"apt-install": ["libpcap-dev"]
},
{
"match-jobs": ["ubuntu-20.04-*"],
"apt-install": ["mosquitto"]
}
]In this case, Ubuntu 20.04 runners will execute:
apt install libpcap-dev mosquittowhile Ubuntu 24.04 runners will only install:
apt install libpcap-devImportant: The reusable workflow chains only packages, but not the run field value, as explained below.
To run a command after package installation, e.g. starting a service:
jobs:
ci:
name: My Module
uses: openDAQ/openDAQ-CI/.github/workflows/reusable.yml@main
with:
packages: >
[
{
"match-jobs": ["ubuntu-*"],
"apt-install": ["mosquitto"],
"run": "mosquitto -d"
},
{
"match-jobs": ["macos-*"],
"brew-install": ["mosquitto"],
"run": "$(brew --prefix mosquitto)/sbin/mosquitto -d"
},
{
"match-jobs": ["windows-*"],
"winget-install": ["EclipseFoundation.Mosquitto"],
"run": "Start-Process 'C:\\Program Files\\mosquitto\\mosquitto.exe' -WindowStyle Hidden"
}
]The result is expanded as follows:
# All Ubuntu runners:
apt install mosquitto
mosquitto -d# All macOS runners:
brew install mosquitto
$(brew --prefix mosquitto)/sbin/mosquitto -d# All Windows runners:
winget install --id EclipseFoundation.Mosquitto
Start-Process 'C:\Program Files\mosquitto\mosquitto.exe' -WindowStyle HiddenAlthough packages are chained when multiple entries match, the run field applies a last-match-wins strategy — the order of entries matters. Consider the following input:
with:
packages: >
[
{
"match-jobs": ["ubuntu-*"],
"run": "./common-linux-config.sh"
},
{
"match-jobs": ["ubuntu-20.04-*"],
"run": "./special-linux-config.sh"
}
]The result is expanded as follows:
# Ubuntu 24.04 runners will execute:
./common-linux-config.sh# Ubuntu 20.04 runners will execute:
./special-linux-config.shSince the order matters, it is possible to skip run for specific configurations by setting it to an empty string:
packages: >
[
{
"match-jobs": ["ubuntu-*"],
"run": "./common-linux-config.sh"
},
{
"match-jobs": ["ubuntu-20.04-*"],
"run": ""
}
]In this case, the input will expand only into:
# Only Ubuntu 24.04 runners will execute:
./common-linux-config.sh
# while Ubuntu 20.04 runners will skip the commandrun exclusion approach and the last-match-wins strategy: the latest match overrides the previous one if the entries order is reversed.
❌ Consider the following mistake:
packages: >
[
{
"match-jobs": ["ubuntu-20.04-*"],
"run": "./special-linux-config.sh"
},
{
"match-jobs": ["ubuntu-*"],
"run": "./common-linux-config.sh"
}
]The result will differ from the caller's intent:
# Caller expects Ubuntu 20.04 to execute
# ./special-linux-config.sh
#
# But instead all Ubuntu runners will execute:
./common-linux-config.shTo call the reusable workflow multiple times within the same workflow, use upload-pattern to avoid artifact name collisions:
jobs:
ci-unit:
name: Unit Tests
uses: openDAQ/openDAQ-CI/.github/workflows/reusable.yml@main
with:
upload-pattern: 'unit-*'
cmake-presets: >
[
{
"configure-preset": "module",
"test-preset": "module-unit-test"
}
]
ci-integration:
name: Integration Tests
uses: openDAQ/openDAQ-CI/.github/workflows/reusable.yml@main
with:
upload-pattern: 'integration-*'
cmake-presets: >
[
{
"configure-preset": "module",
"test-preset": "module-integration-test"
}
]The default job timeout is 120 minutes. To override it for all jobs:
jobs:
ci:
name: My Module
uses: openDAQ/openDAQ-CI/.github/workflows/reusable.yml@main
with:
timeout: '[{"timeout-minutes": 240}]'To set different timeouts per platform:
jobs:
ci:
name: My Module
uses: openDAQ/openDAQ-CI/.github/workflows/reusable.yml@main
with:
timeout: >
[
{
"timeout-minutes": 180
},
{
"match-jobs": ["windows-*"],
"timeout-minutes": 240
}
]In this example, all jobs get a 180-minute timeout, except Windows jobs which get 240 minutes.
timeout the same way as to run. The order of entries matters — the latest match overrides the previous one if the entries order is reversed.
❌ Consider the following mistake:
with:
timeout: >
[
{
"match-jobs": ["windows-*"],
"timeout-minutes": 240
},
{
"timeout-minutes": 180
}
]The caller expects Windows runners to have 240-minute timeout. But instead all runners including Windows will get 180-minute timeout.
This project is licensed under the Apache License 2.0.