name: tests on: push: branches: [main] pull_request: jobs: build-and-test: runs-on: ubuntu-latest container: image: ubuntu:24.04 steps: # actions/checkout@v4 is a Node-based action; the bare ubuntu:24.04 # runner image has no node, so checkout fails with # `exitcode '127': command not found`. Install node + git BEFORE # checkout, then install the rest of the toolchain after. - name: Bootstrap (node + git for actions/checkout) run: | export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y --no-install-recommends \ git \ ca-certificates \ nodejs - uses: actions/checkout@v4 - name: Install C++ toolchain run: | export DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ build-essential \ cmake \ ninja-build \ libasio-dev \ libyaml-cpp-dev \ libprotobuf-dev \ protobuf-compiler \ protobuf-compiler-grpc \ libgrpc++-dev \ python3 \ python3-yaml - name: Configure (Release) run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release - name: Build (Release) run: cmake --build build - name: Unit tests (Release) run: build/secsgem_tests # The daemon's gRPC service tests (in-process channel). The grpc deps # above make secs_gemd_tests build; fail loudly if it didn't, so the # daemon can never silently drop out of CI again. - name: Daemon tests (Release) run: | test -x build/secs_gemd_tests || { echo "secs_gemd_tests not built"; exit 1; } build/secs_gemd_tests thread-sanitizer: runs-on: ubuntu-latest container: image: ubuntu:24.04 steps: - name: Bootstrap (node + git for actions/checkout) run: | export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y --no-install-recommends \ git ca-certificates nodejs - uses: actions/checkout@v4 - name: Install C++ toolchain run: | export DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ build-essential cmake ninja-build \ libasio-dev libyaml-cpp-dev \ libprotobuf-dev protobuf-compiler protobuf-compiler-grpc libgrpc++-dev \ python3 python3-yaml # Debug + -fsanitize=thread. Catches data races in the # io_context strand contract documented in docs/INTEGRATION.md §3. # halt_on_error=1 makes TSan-flagged races fail the job, not # just print a warning. - name: Configure (TSan) run: cmake -S . -B build-tsan -G Ninja -DCMAKE_BUILD_TYPE=Debug -DSECSGEM_TSAN=ON - name: Build (TSan) run: cmake --build build-tsan - name: Unit tests (TSan) env: TSAN_OPTIONS: halt_on_error=1 run: build-tsan/secsgem_tests # The daemon under TSan: run_async + concurrent gRPC handler threads is # the production threading shape — this is where a strand-contract # violation would actually surface. tools/tsan.supp silences false # positives inside the UNinstrumented system libgrpc only; our frames # stay fully checked. - name: Daemon tests (TSan) env: TSAN_OPTIONS: halt_on_error=1 suppressions=tools/tsan.supp run: | test -x build-tsan/secs_gemd_tests || { echo "secs_gemd_tests not built under TSan"; exit 1; } build-tsan/secs_gemd_tests tshark-dissector: runs-on: ubuntu-latest container: image: ubuntu:24.04 steps: - name: Bootstrap (node + git for actions/checkout) run: | export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y --no-install-recommends \ git ca-certificates nodejs - uses: actions/checkout@v4 - name: Install C++ toolchain + tshark run: | export DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ build-essential cmake ninja-build \ libasio-dev libyaml-cpp-dev \ python3 python3-yaml \ tshark tcpdump - name: Build run: | cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release cmake --build build # Capture pcap of the demo flow, dissect with Wireshark's HSMS # dissector (independent third codec), assert no malformed # packets and that every expected control + data frame parses. - name: tshark HSMS dissector validation run: bash interop/tshark_validate.sh env: PORT: "5099" secs4j-interop: runs-on: ubuntu-latest # secs4j-interop builds its own docker image (with JDK) and starts # secs_server via docker compose; needs the docker socket, which # is why this job runs on the bare runner rather than inside a # `container:` like the other jobs. # # The bare runner image varies across deployments — catthehacker # provides apt-get, but some gitea-act runners use a minimal node # image with no package manager. The bootstrap step detects what # it has (apt-get / apk / nothing) and only installs missing # pieces; node + git are usually already present so actions/checkout # can run. steps: - name: Bootstrap (node + git + docker CLI if missing) run: | set -e need_git=0; need_node=0; need_docker=0 command -v git >/dev/null || need_git=1 command -v node >/dev/null || need_node=1 command -v docker >/dev/null || need_docker=1 if [ "$need_git$need_node$need_docker" = "000" ]; then echo "runner image already has git/node/docker — skipping install" exit 0 fi if command -v apt-get >/dev/null; then export DEBIAN_FRONTEND=noninteractive apt-get update pkgs="" [ "$need_git" = "1" ] && pkgs="$pkgs git ca-certificates" [ "$need_node" = "1" ] && pkgs="$pkgs nodejs" [ "$need_docker" = "1" ] && pkgs="$pkgs docker.io docker-compose-plugin" apt-get install -y --no-install-recommends $pkgs elif command -v apk >/dev/null; then pkgs="" [ "$need_git" = "1" ] && pkgs="$pkgs git ca-certificates" [ "$need_node" = "1" ] && pkgs="$pkgs nodejs" [ "$need_docker" = "1" ] && pkgs="$pkgs docker docker-cli-compose" apk add --no-cache $pkgs else echo "no apt-get or apk — runner image has no package manager" echo "need: git=$need_git node=$need_node docker=$need_docker" exit 1 fi - uses: actions/checkout@v4 # Cross-validate against secs4java8 (Apache 2.0, kenta-shimizu) — # an independent SECS implementation in Java. 55 checks # covering S1/S2/S3/S5/S6/S7/S10/S14/S16 with full-body GEM 300 # shapes that secsgem-py couldn't easily drive. - name: secs4j cross-validation run: bash interop/secs4j_validate.sh python-interop: # secsgem-py (the Python reference implementation) judging our wire # behaviour: its GemHostHandler drives our secs_server (31 checks), the # C++ conformance host drives it too (47 checks), and the dual-face # daemon harness (gRPC tool + secsgem-py host vs secs_gemd) proves the # gRPC<->HSMS bridge. Localhost processes in one container — no # docker-in-docker. runs-on: ubuntu-latest container: image: ubuntu:24.04 steps: - name: Bootstrap (node + git for actions/checkout) run: | export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y --no-install-recommends \ git ca-certificates nodejs - uses: actions/checkout@v4 - name: Install toolchain + python run: | export DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ build-essential cmake ninja-build \ libasio-dev libyaml-cpp-dev \ libprotobuf-dev protobuf-compiler protobuf-compiler-grpc libgrpc++-dev \ python3 python3-yaml python3-venv python3-pip - name: Build run: | cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release cmake --build build - name: Python deps (secsgem-py + grpcio, same pins as interop image) run: | python3 -m venv /tmp/venv /tmp/venv/bin/pip install --no-cache-dir \ secsgem==0.3.0 grpcio==1.60.0 grpcio-tools==1.60.0 "setuptools<80" - name: secsgem-py host vs secs_server run: | build/secs_server --port 5001 & SERVER=$! sleep 1 /tmp/venv/bin/python interop/host_vs_cpp_server.py --host 127.0.0.1 --port 5001 rc=$? kill $SERVER 2>/dev/null || true exit $rc - name: secs_conformance vs secs_server run: | build/secs_server --port 5002 & SERVER=$! sleep 1 build/secs_conformance --host 127.0.0.1 --port 5002 rc=$? kill $SERVER 2>/dev/null || true exit $rc - name: daemon interop (gRPC tool + secsgem-py host vs secs_gemd) run: | build/secs_gemd --port 5003 --grpc 127.0.0.1:50051 & GEMD=$! sleep 1 /tmp/venv/bin/python interop/daemon_interop.py \ --grpc 127.0.0.1:50051 --hsms-host 127.0.0.1 --hsms-port 5003 \ --proto-dir proto/secsgem/v1 rc=$? kill $GEMD 2>/dev/null || true exit $rc libfuzzer: runs-on: ubuntu-latest container: image: ubuntu:24.04 steps: - name: Bootstrap (node + git) run: | export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y --no-install-recommends \ git ca-certificates nodejs - uses: actions/checkout@v4 - name: Install clang + libFuzzer runtime run: | export DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ cmake ninja-build \ libasio-dev libyaml-cpp-dev \ python3 python3-yaml \ clang libclang-rt-18-dev - name: Configure (fuzz) env: CC: clang CXX: clang++ run: cmake -S . -B build-fuzz -G Ninja -DCMAKE_BUILD_TYPE=Debug -DSECSGEM_FUZZ=ON - name: Build fuzz targets run: cmake --build build-fuzz --target fuzz_secs2_decode fuzz_sml_parse # 60 seconds each — long enough for libFuzzer to explore the # decoder/parser without ballooning CI time. Any crash, ASan # report, or UBSan flag fails the job. - name: Fuzz secs2::decode run: build-fuzz/fuzz_secs2_decode -max_total_time=60 -max_len=4096 - name: Fuzz secs2::try_parse_sml run: build-fuzz/fuzz_sml_parse -max_total_time=60 -max_len=4096