name: CI on: push: branches: [develop, '1.2.x'] pull_request: branches: [develop, '1.2.x', 'refactor/c17-series-*'] permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: dco: runs-on: ubuntu-24.04 timeout-minutes: 5 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 # pull_request normally checks out GitHub's synthetic merge commit, # which cannot carry the contributor's DCO trailer. ref: ${{ github.event.pull_request.head.sha }} - name: Verify Developer Certificate of Origin trailers if: github.event_name == 'pull_request' env: BASE_REF: ${{ github.base_ref }} run: scripts/check-dco.sh "origin/${BASE_REF}..HEAD" build: name: Build (${{ matrix.compiler }}, ${{ matrix.client.name }}) runs-on: ubuntu-24.04 timeout-minutes: 20 strategy: fail-fast: false matrix: compiler: [gcc, clang] client: - name: MariaDB Connector/C package: libmariadb-dev config: mariadb_config - name: MySQL Connector/C package: libmysqlclient-dev config: mysql_config env: CC: ${{ matrix.compiler }} MYSQL_CONFIG: ${{ matrix.client.config }} # get_date_format() deliberately constructs the bounded strftime # format; keep every other warning fatal. STRICT_CFLAGS: -Wall -Wextra -Wformat=2 -Wno-format-nonliteral steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/build-spine with: mysql-client-package: ${{ matrix.client.package }} cflags: ${{ env.STRICT_CFLAGS }} - name: Build Spine run: | set -euo pipefail configured_cflags=$(sed -n 's/^CFLAGS = //p' Makefile) make -j"$(nproc)" CFLAGS="$configured_cflags -Werror" - name: Run the unit tests run: | set -euo pipefail configured_cflags=$(sed -n 's/^CFLAGS = //p' Makefile) # Test doubles and cmocka callbacks intentionally leave parameters # unused; tests/unit/test_build_fixes.c also inlines strncpy-based # helpers specifically to exercise their truncation behavior, which # -Wstringop-truncation flags on principle even though it's the # scenario under test. Every other test-source warning stays fatal. # -Wstringop-truncation is GCC-only; -Wno-unknown-warning-option # keeps clang from erroring on it under -Werror. make check CFLAGS="$configured_cflags -Werror -Wno-unused-parameter -Wno-unknown-warning-option -Wno-stringop-truncation" # Linux capabilities are opt-in at configure time. Keep one build on that # path so hasCaps() and the CAP_NET_RAW handling cannot silently rot behind # an otherwise-green default build. linux-capabilities: runs-on: ubuntu-24.04 timeout-minutes: 20 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/build-spine with: extra-packages: libcap-dev # Exercise the supported low-buffer configuration as well as the # Linux-capability path; formatter tests must not assume defaults. configure-args: --enable-lcap --enable-warnings --with-results-buffer=512 - name: Build Spine with Linux capabilities run: | set -euo pipefail make -j"$(nproc)" - name: Run the unit tests run: | set -euo pipefail make check macos: runs-on: macos-15 timeout-minutes: 25 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install build dependencies run: | set -euo pipefail brew install autoconf automake libtool help2man \ mariadb-connector-c net-snmp openssl@3 cmocka - name: Build and test env: CC: clang run: | set -euo pipefail mariadb_prefix=$(brew --prefix mariadb-connector-c) openssl_prefix=$(brew --prefix openssl@3) net_snmp_prefix=$(brew --prefix net-snmp) cmocka_prefix=$(brew --prefix cmocka) export MYSQL_CONFIG="$mariadb_prefix/bin/mariadb_config" export CPPFLAGS="-I$openssl_prefix/include -I$net_snmp_prefix/include -I$cmocka_prefix/include" export LDFLAGS="-L$mariadb_prefix/lib -L$openssl_prefix/lib -L$net_snmp_prefix/lib -L$cmocka_prefix/lib" ./bootstrap ./configure --enable-warnings --with-snmp="$net_snmp_prefix" make -j"$(sysctl -n hw.ncpu)" make check || { cat test-suite.log; exit 1; } cppcheck: runs-on: ubuntu-24.04 timeout-minutes: 20 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Run cppcheck run: | set -euo pipefail sudo apt-get update sudo apt-get install -y cppcheck # Project data structures are consumed across translation units; # checking headers in isolation otherwise reports every member. A # few externally visible helpers are deliberate unit-test seams. cppcheck \ --enable=all \ --std=c11 \ --error-exitcode=1 \ --suppress=missingInclude \ --suppress=missingIncludeSystem \ --suppress=normalCheckLevelMaxBranches \ --suppress=checkLevelNormal \ --suppress=unmatchedSuppression \ --suppress=unusedFunction \ --suppress=checkersReport \ --suppress=variableScope \ --suppress=unreadVariable \ --suppress=unusedStructMember \ --suppress=staticFunction \ --suppress=constVariablePointer \ --suppress=redundantAssignment \ --suppress=toomanyconfigs \ --library=scripts/cppcheck/spine.cfg \ ./*.c ./*.h shellcheck: runs-on: ubuntu-24.04 timeout-minutes: 10 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Run shellcheck run: | set -euo pipefail sudo apt-get update sudo apt-get install -y shellcheck find scripts tests -type f -name '*.sh' -print0 \ | xargs -0 shellcheck # Preserve the measured baseline and require stronger coverage on changed # lines. Raise both thresholds as integration coverage grows. coverage: runs-on: ubuntu-24.04 timeout-minutes: 20 services: database: image: mirror.gcr.io/library/mariadb:10.11 env: MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: '1' MARIADB_DATABASE: spine_regressions ports: - 3306:3306 options: >- --health-cmd="mariadb-admin ping --silent" --health-interval=5s --health-timeout=5s --health-retries=20 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - uses: ./.github/actions/build-spine with: extra-packages: gcovr python3-pip mariadb-client snmpd libcap2-bin cflags: -g -O0 --coverage -fprofile-update=atomic ldflags: --coverage - name: Build instrumented run: | set -euo pipefail make -j"$(nproc)" - name: Run the unit tests run: | set -euo pipefail make check - name: Cover production database and protocol scenarios env: SPINE_TEST_DB_HOST: 127.0.0.1 SPINE_TEST_SNMP_HOST: 127.0.0.1 SPINE_SNMPV3_EVIDENCE_DIR: snmpv3-evidence run: | set -euo pipefail mariadb --protocol=TCP --host=127.0.0.1 --user=root spine_regressions < tests/fixtures/configuration.sql mariadb --protocol=TCP --host=127.0.0.1 --user=root spine_regressions < tests/fixtures/poller.sql snmpd -f -Lo -C -c tests/fixtures/snmpd.conf > coverage-snmp-agent.log 2>&1 & agent_pid=$! cleanup() { sudo setcap -r ./test_spine_regressions kill "$agent_pid" wait "$agent_pid" || true } trap cleanup EXIT sleep 1 ./test_spine_regressions --database ./test_spine_regressions --snmp-agent ./test_spine_faults --database tests/tools/run_snmpv3.sh ./test_spine_regressions --icmp-no-capability sudo setcap cap_net_raw+ep ./test_spine_regressions ./test_spine_regressions --raw-icmp - name: Report coverage run: | set -euo pipefail # configure leaves instrumented conftest objects behind and gcovr # cannot resolve their sources rm -f ./*conftest*.gcno ./*conftest*.gcda gcovr -r . \ --exclude 'tests/.*' \ --txt --print-summary \ --xml coverage.xml \ --html-details coverage.html \ --fail-under-line 7 - name: Enforce changed-line coverage if: github.event_name == 'pull_request' env: BASE_REF: ${{ github.base_ref }} run: | set -euo pipefail python3 -m pip install --break-system-packages diff-cover==10.5.1 diff-cover coverage.xml \ --compare-branch="origin/${BASE_REF}" \ --fail-under=70 - name: Upload coverage reports if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v4.4.3 with: name: coverage-report path: | coverage.xml coverage.html coverage.*.html if-no-files-found: error flawfinder: runs-on: ubuntu-24.04 timeout-minutes: 20 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.x" - name: Install flawfinder run: pip install flawfinder==2.0.19 # Note: flawfinder will light up like a christmas tree on this codebase. # error-level=5 means only critical hits fail the build — the rest is # informational so we have a baseline to chip away at. - name: Run flawfinder run: | set -euo pipefail flawfinder \ --minlevel=3 \ --error-level=5 \ --columns \ --context \ . | tee flawfinder-report.txt - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 if: always() with: name: flawfinder-report path: flawfinder-report.txt # Spine is a threaded network daemon; ASan and UBSan are the checks most # likely to catch a real defect here. The unit tests are what actually # exercises the code, so they are what runs instrumented. sanitizers: runs-on: ubuntu-24.04 timeout-minutes: 20 env: CC: clang ASAN_OPTIONS: detect_leaks=1:exitcode=1 UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/build-spine with: extra-packages: clang cflags: -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1 ldflags: -fsanitize=address,undefined - name: Build with ASan and UBSan run: | set -euo pipefail make -j"$(nproc)" - name: Run the unit tests under the sanitizers run: | set -euo pipefail make check - name: Smoke-test the binary under the sanitizers run: | set -euo pipefail ./spine --version ./spine --help > /dev/null # Spine is highly concurrent. Keep race detection separate from ASan/UBSan # because the sanitizer runtimes cannot be combined. thread-sanitizer: runs-on: ubuntu-24.04 timeout-minutes: 20 env: CC: clang TSAN_OPTIONS: halt_on_error=1:second_deadlock_stack=1 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/build-spine with: extra-packages: clang cflags: -fsanitize=thread -fno-omit-frame-pointer -g -O1 ldflags: -fsanitize=thread - name: Build with ThreadSanitizer run: | set -euo pipefail make -j"$(nproc)" - name: Run the unit tests under ThreadSanitizer run: | set -euo pipefail make check # A release tarball that cannot be compiled is a release blocker, and the # only way to catch it is to build from the tarball the way a packager does. distcheck: runs-on: ubuntu-24.04 timeout-minutes: 20 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/build-spine - name: make distcheck run: | set -euo pipefail make distcheck # Branch protection should require this stable name rather than every matrix # child. Any failed, cancelled or unexpectedly skipped dependency fails an # active run. A superseded workflow must not publish a failing aggregate # after concurrency cancellation; its replacement owns the current result. required: name: CI / required if: always() && !cancelled() needs: - dco - build - linux-capabilities - macos - cppcheck - shellcheck - coverage - flawfinder - sanitizers - thread-sanitizer - distcheck runs-on: ubuntu-24.04 timeout-minutes: 5 steps: - name: Verify every required job passed env: RESULTS: ${{ toJSON(needs) }} run: | set -euo pipefail failures=$(jq -r 'to_entries[] | select(.value.result != "success") | "\(.key)=\(.value.result)"' <<<"$RESULTS") if [ -n "$failures" ]; then echo "Required CI jobs did not pass:" echo "$failures" exit 1 fi