Files
codebase-memory-mcp/test-infrastructure/docker-compose.yml
T
Martin Vogel 0a40fe8bea feat(ci): local-remote parity lanes — artifact-flow smoke, glibc floor, Defender-ON everywhere
Closes the closable local-vs-remote venue gaps so that, outside arch
legs / YAML glue / release plumbing, every local red is a remote red
and vice versa on shared legs:

- scripts/package-release.sh: THE canonical archive step (names +
  five-file Windows bundle layout defined once); _build.yml's eight
  inline archive blocks become calls to it.
- scripts/ci/smoke-artifact.sh: the artifact-flow smoke lane — build,
  package, extract, then the canonical wrapper in artifact mode; wired
  as run.sh smoke-artifact (compose service), win.sh smoke-artifact,
  and directly runnable on macOS. Archive-layout bugs now surface
  locally instead of in a release dry run.
- glibc-floor leg (Dockerfile.glibc22 + compose + run.sh): portable
  binary smokes on ubuntu-22.04/glibc 2.35; the dynamic binary must
  refuse there (2.38+ floor by design).
- Defender-ON parity (user directive): scripts/ci/ensure-defender.ps1
  enables + VERIFIES real-time protection, fail-closed; runs in every
  Windows CI job (_test x2, _soak x3, _smoke, pr.yml) AND in the VM
  preflight; _smoke.yml's scan engine-failure soft-skip becomes a red
  gate. Expected cost: slower Windows jobs (AV scanning during
  install/build/test I/O); the next dry run proves the runner side.
- Contracts: launcher-bundle five-file check retargeted onto
  package-release.sh + per-archive canonical-call association;
  venue-parity contract requires the new lanes, counts one
  ensure-defender step per Windows job, adds --help probes for the new
  entries. Extended contract fails on the pre-change tree (verified:
  20 violations on HEAD).
- VM README: ephemerality/Defender posture documented — utmctl has no
  snapshot verb, so per-run revert stays a manual qcow2 option; the
  sweep preflight remains the standing mechanism.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-26 22:37:48 +02:00

303 lines
11 KiB
YAML

# Local test environment — mirrors GitHub Actions CI for ALL platforms.
#
# Coverage:
# Linux arm64: native test + build (mirrors CI ubuntu-24.04-arm)
# Linux amd64: QEMU test + build (mirrors CI ubuntu-latest)
# Windows: cross-compile with mingw-w64 (catches all compile errors)
# macOS: run natively — scripts/test.sh CC=cc && scripts/build.sh CC=cc
services:
# ── Linux test (ASan + UBSan + LeakSanitizer) ──────────────
# Always full-parallel: the suite is designed to be core-count-independent
# for correctness — timing/scheduling/subprocess suites assert invariants
# (ordering, bounded-return, RUNNING poll state), not wall-clock, so they
# pass at any parallelism. Artificial CPU starvation belongs to smoke/soak
# (CBM_LOCAL_CI_CPUS), not the regular suite.
test:
build:
context: ..
dockerfile: test-infrastructure/Dockerfile
platform: linux/arm64
volumes:
- ..:/src
- cbm-build:/src/build
- cbm-fixture-cache:/root/.cache/cbm-test-fixtures
- cbm-ccache-arm64:/root/.ccache
environment:
CCACHE_DIR: /root/.ccache
CCACHE_MAXSIZE: 1500M
command: ["CC=gcc", "CXX=g++", "BUILD_DIR=build/linux-arm64"]
test-amd64:
build:
context: ..
dockerfile: test-infrastructure/Dockerfile
platform: linux/amd64
volumes:
- ..:/src
- cbm-build:/src/build
- cbm-fixture-cache:/root/.cache/cbm-test-fixtures
- cbm-ccache-amd64:/root/.ccache
environment:
CCACHE_DIR: /root/.ccache
CCACHE_MAXSIZE: 1500M
command: ["CC=gcc", "CXX=g++", "BUILD_DIR=build/linux-amd64"]
# ── ThreadSanitizer (data-race gate) ───────────────────────────
# Overrides the default test.sh entrypoint to build + run the widened TSan
# suite set. /usr/lib/ccache is first on PATH in the image, so CC=gcc is
# ccache-masqueraded automatically (same as the ASan legs).
test-tsan:
build:
context: ..
dockerfile: test-infrastructure/Dockerfile
platform: linux/arm64
# make is PID 1 for this service. Give orphaned subprocess-test descendants
# a real reaper so killed zombies do not keep process groups appearing live.
init: true
volumes:
- ..:/src
- cbm-build:/src/build
- cbm-fixture-cache:/root/.cache/cbm-test-fixtures
- cbm-ccache-arm64:/root/.ccache
environment:
CCACHE_DIR: /root/.ccache
CCACHE_MAXSIZE: 1500M
# TSan's shadow memory needs a predictable address space; modern aarch64
# kernels' high-entropy mmap ASLR makes its runtime abort with "unexpected
# memory mapping" before any test runs. setarch -R (ADDR_NO_RANDOMIZE)
# fixes it, but the personality() syscall it uses is blocked by the default
# seccomp profile — unconfined is safe here (the container runs only our own
# test code). The no-randomize personality inherits to the make child.
security_opt: ["seccomp=unconfined"]
# The TSan leg lives in the canonical test entry (scripts/test.sh --tsan),
# same as CI's tsan jobs; the no-randomize personality inherits through it.
entrypoint: ["setarch", "-R", "bash", "scripts/test.sh", "--tsan"]
command: ["CC=gcc", "CXX=g++", "BUILD_DIR=build/linux-arm64-tsan"]
test-tsan-amd64:
build:
context: ..
dockerfile: test-infrastructure/Dockerfile
platform: linux/amd64
init: true
volumes:
- ..:/src
- cbm-build:/src/build
- cbm-fixture-cache:/root/.cache/cbm-test-fixtures
- cbm-ccache-amd64:/root/.ccache
environment:
CCACHE_DIR: /root/.ccache
CCACHE_MAXSIZE: 1500M
security_opt: ["seccomp=unconfined"]
# Canonical test entry, TSan mode — same file as CI's tsan jobs.
entrypoint: ["setarch", "-R", "bash", "scripts/test.sh", "--tsan"]
command: ["CC=gcc", "CXX=g++", "BUILD_DIR=build/linux-amd64-tsan"]
# ── Linux production build (-O2 -Werror) ───────────────────
build:
build:
context: ..
dockerfile: test-infrastructure/Dockerfile
platform: linux/arm64
volumes:
- ..:/src
entrypoint: ["scripts/build.sh"]
command: ["CC=gcc", "CXX=g++", "BUILD_DIR=build/linux-arm64"]
build-amd64:
build:
context: ..
dockerfile: test-infrastructure/Dockerfile
platform: linux/amd64
volumes:
- ..:/src
entrypoint: ["scripts/build.sh"]
command: ["CC=gcc", "CXX=g++", "BUILD_DIR=build/linux-amd64"]
# ── Windows cross-compile + test (llvm-mingw + Wine) ────────
build-windows:
build:
context: ..
dockerfile: test-infrastructure/Dockerfile.mingw
platform: linux/amd64
volumes:
- ..:/src
test-windows:
build:
context: ..
dockerfile: test-infrastructure/Dockerfile.mingw
platform: linux/amd64
volumes:
- ..:/src
command: >-
make -j4 -f Makefile.cbm clean-c BUILD_DIR=build/win-cross &&
make -j4 -f Makefile.cbm build/win-cross/test-runner
BUILD_DIR=build/win-cross
CC=x86_64-w64-mingw32-clang
CXX=x86_64-w64-mingw32-clang++ &&
echo '=== Running tests under Wine ===' &&
WINEDEBUG=-all wine64 build/win-cross/test-runner.exe &&
echo '=== Windows test: OK ==='
# ── Smoke test (build + run smoke-test.sh) ──────────────────
smoke:
build:
context: ..
dockerfile: test-infrastructure/Dockerfile
platform: linux/arm64
volumes:
- ..:/src
entrypoint: ["/bin/bash", "-c"]
command:
- |
scripts/build.sh CC=gcc CXX=g++ BUILD_DIR=build/linux-arm64 &&
scripts/smoke-local.sh ./build/linux-arm64/codebase-memory-mcp
smoke-amd64:
build:
context: ..
dockerfile: test-infrastructure/Dockerfile
platform: linux/amd64
volumes:
- ..:/src
entrypoint: ["/bin/bash", "-c"]
command:
- |
scripts/build.sh CC=gcc CXX=g++ BUILD_DIR=build/linux-amd64 &&
scripts/smoke-local.sh ./build/linux-amd64/codebase-memory-mcp
# ── Soak (both CI legs) ────────────────────────────────────
# The sequence (quick + #581 query-leak) lives in the canonical entry
# scripts/soak-legs.sh — the same file _soak.yml and the Windows VM run.
# The local ladder previously had NO unix soak at all. Duration follows
# CBM_SOAK_MINUTES (default 10, matching the dry run).
soak:
build:
context: ..
dockerfile: test-infrastructure/Dockerfile
platform: linux/arm64
volumes:
- ..:/src
entrypoint: ["/bin/bash", "-c"]
command:
- |
scripts/build.sh CC=gcc CXX=g++ BUILD_DIR=build/linux-arm64 &&
scripts/soak-legs.sh ./build/linux-arm64/codebase-memory-mcp ${CBM_SOAK_MINUTES:-10}
# ── Windows smoke (cross-compile + Wine) ──────────────────
smoke-windows:
build:
context: ..
dockerfile: test-infrastructure/Dockerfile.mingw
platform: linux/amd64
volumes:
- ..:/src
entrypoint: ["/bin/bash", "-c"]
command:
- |
scripts/build.sh CC=x86_64-w64-mingw32-clang CXX=x86_64-w64-mingw32-clang++ BUILD_DIR=build/win-cross &&
mv build/win-cross/codebase-memory-mcp.exe build/win-cross/codebase-memory-mcp.payload.exe &&
mv build/win-cross/codebase-memory-mcp-launcher.exe build/win-cross/codebase-memory-mcp.exe &&
WINEDEBUG=-all wine64 ./build/win-cross/codebase-memory-mcp.payload.exe --version &&
WINEDEBUG=-all wine64 cmd /c build/win-cross/codebase-memory-mcp.exe --version
# ── Linux portable (Alpine musl static) ────────────────────
test-portable:
build:
context: ..
dockerfile: test-infrastructure/Dockerfile.alpine
platform: linux/arm64
volumes:
- ..:/src
entrypoint: ["bash", "scripts/test.sh"]
command: ["CC=gcc", "CXX=g++", "BUILD_DIR=build/alpine"]
build-portable:
build:
context: ..
dockerfile: test-infrastructure/Dockerfile.alpine
platform: linux/arm64
volumes:
- ..:/src
entrypoint: ["bash", "scripts/build.sh"]
command: ["CC=gcc", "CXX=g++", "STATIC=1", "BUILD_DIR=build/alpine"]
smoke-portable:
build:
context: ..
dockerfile: test-infrastructure/Dockerfile.alpine
platform: linux/arm64
volumes:
- ..:/src
entrypoint: ["bash", "-c"]
command:
- |
scripts/build.sh CC=gcc CXX=g++ STATIC=1 BUILD_DIR=build/alpine &&
file build/alpine/codebase-memory-mcp | grep -q "statically linked" &&
echo "=== Verified: statically linked ===" &&
scripts/smoke-local.sh ./build/alpine/codebase-memory-mcp
# ── Artifact-flow smoke (package → extract → wrapper) ──────
# The release venue smokes the shipped archive; this lane reproduces that
# flow with local bytes via the canonical package-release.sh +
# smoke-local.sh artifact mode, so archive-layout bugs surface locally.
smoke-artifact:
build:
context: ..
dockerfile: test-infrastructure/Dockerfile
platform: linux/arm64
volumes:
- ..:/src
entrypoint: ["/bin/bash", "-c"]
command:
- |
BUILD_DIR=build/linux-arm64 \
scripts/ci/smoke-artifact.sh linux arm64 CC=gcc CXX=g++
# ── glibc floor (ubuntu-22.04, glibc 2.35) ─────────────────
# Mirrors CI's broad-matrix 22.04 legs: the portable (musl-static) binary
# must pass the canonical smoke on the oldest supported userland, and the
# dynamic binary must REFUSE to start (glibc 2.38+ floor is by design).
smoke-glibc-floor:
build:
context: ..
dockerfile: test-infrastructure/Dockerfile.glibc22
platform: linux/arm64
volumes:
- ..:/src
entrypoint: ["/bin/bash", "-c"]
command:
- |
set -e
file build/alpine/codebase-memory-mcp | grep -q "statically linked"
echo "=== glibc-floor: portable binary smoke on glibc 2.35 ==="
scripts/smoke-local.sh ./build/alpine/codebase-memory-mcp
echo "=== glibc-floor: dynamic binary must refuse (glibc 2.38+ floor) ==="
if build/linux-arm64/codebase-memory-mcp --version 2>/tmp/dynamic-floor.err; then
echo "ERROR: dynamic binary ran on glibc 2.35 — floor contract broken"
exit 1
fi
grep -qi 'GLIBC' /tmp/dynamic-floor.err
echo "=== Verified: dynamic binary refuses with a GLIBC floor error ==="
# ── Lint ────────────────────────────────────────────────────
lint:
build:
context: ..
dockerfile: test-infrastructure/Dockerfile.lint
volumes:
- ..:/src
command: ["--ci", "CLANG_FORMAT=clang-format-20"]
volumes:
cbm-ccache-arm64:
cbm-ccache-amd64:
# Build artifacts and the incremental-suite fixture cache stay on the VM's
# native filesystem: object writes over the virtiofs bind mount are the
# container legs' largest avoidable I/O cost, and the fixture cache makes
# the network clone once-per-volume instead of once-per-container.
cbm-build:
cbm-fixture-cache: