yams/.github/workflows/tests.yml
Trevon 2d01ba314c
Assertion hardening, metadata correctness, & daemon lifecycle fixes (#41)
* fix(core): add assertions and harden daemon lifecycle

* fix(daemon): tighten shutdown lifecycle invariants

* fix(test): add PressureLimitedPoller capability-sleep shutdown test

* refactor(vector): harden backend contracts

Split the vector backend surface into narrower contracts and
move entity read paths to Result<> so callers can distinguish
backend errors from empty results.

Also fixes the TurboQuant lazy-init race, makes VectorDatabase
error state thread-safe, wires eager index preparation, and
adds regression plus memory-scaling benchmark coverage.

* fix(vector): address audit findings plus hardening

Fix the five confirmed bugs: coroutine lifetime hazard in
embedding_generator (await_with_timeout outliving captured state),
VectorSystemManager init retry latch poisoning, incremental HNSW
persistence race, vec0 registry key collisions, and simeon PMI
output_dim/config() drift.

Add invariant hardening: PostIngestQueue stage validation +
numChunks assertion, VectorSystemManager success postcondition,
VectorDatabase TurboQuant non-null assertions on all quantize/
dequantize paths, and sqlite_vec_backend Simeon PQ range and
divisibility preconditions.

Also adds regression coverage for init-retry and lock-busy paths
and a locked node snapshot helper for safe incremental HNSW
persistence.

* fix(test): add PressureLimitedPoller wake-timer shutdown tests

* chore(vector): resolve clang-tidy hygiene across queue, vdb, and sqlite-vec

Replace unprotected std::getenv calls with locked getenvCopy helpers,
add debug-level logging to previously-silent catch blocks in
PostIngestQueue, VectorDatabase, and SqliteVecBackend, wrap
PostIngestQueue timer-detach allocations in try-catch, and fix
unsigned-widening / optional-deref / unused-include hygiene.

Verified with LSP diagnostics and rebuilds of catch2_vector_submodule
and catch2_daemon_component_submodule.

* fix(daemon): harden PressureLimitedPoller inFlightCounter null check

* fix(daemon): harden PostIngestQueue wait_until deadline checks

* fix(daemon): clean up recovery sentinels regardless of salvage count

Previously the .recovered-* sentinel and .corrupt-* file cleanup
was gated on documentsSalvaged > 0, leaving quarantine
artifacts to accumulate on repeated crash/recovery cycles where
the corrupt DB had 0 salvageable docs.

Move cleanup outside the salvage-count guard so stale
quarantine artifacts are always removed after examination.

* feat(vec0): restore persisted ANN index on daemon restart

After vec0 rebuild, probe for vectors_<dim>_hnsw_meta shadow tables
and load a previously-persisted HNSW index.  When found, the ANN
index is set directly on the vec0 table, skipping the O(n log n)
rebuild that was previously paid on every daemon restart.

Removes the stub restoreVec0AnnIndexIfPresentUnlocked() in favor
of inline restoration in rebuildVec0DimUnlocked(), which has
the vec0 table already created and ready for ann_index assignment.

* fix: enable DCHECK by default and assert metadata counter consistency

* fix(services): assert document deletion ordering content-before-metadata

* fix(test): add fork-based assertion validation tests

* fix(test): expand assertion tests for POSTCONDITION, DCHECK, UNREACHABLE

* fix(ci): enable -Denable-dcheck=true in all test CI lanes

* fix(test): address assertion review findings

* refactor(daemon): isolate poller config assertion checks

* refactor(services): reduce deletion and poller complexity

* style(ci): normalize test workflow spacing

* refactor(services): extract list query helpers

* refactor(services): extract list hydration helpers

* refactor(daemon): extract metrics snapshot helpers

* refactor(daemon): extract metrics counter snapshot

* refactor(daemon): extract metrics resource snapshot

* feat(graph): add daemon-backed explore

Wire graph explore through daemon IPC, keep CLI rendering stable,
and fall back locally when daemon transport or protocol skew fails.

Also add graph context service/tests, improve graph follow-up hints,
and keep release builds working with the current JSON library.

* fix(services): prevent prune delete metadata orphans

* fix(cli): honor data-dir in graph explore

- bypass daemon explore when the CLI pins a data dir so\n  temp fixtures do not resolve against a long-lived global DB\n- guard snippet truncation and ratio math to remove Fold-\n  reported underflow and div-by-zero paths\n- clamp zero-based symbol lines before rendering graph\n  context

* fix(daemon): make lifecycle stop completion safe

* fix(daemon): gate db-open startup against shutdown

* fix(metadata): serialize FTS indexed counter updates

* test(daemon): simulate vector lock contention cross-process

* test(daemon): isolate post-ingest identity batch

* style(daemon): normalize test include order

* refactor(graph): split cli graph helpers

Keep graph command paths smaller and easier to evolve.

Lock graph explore/topology contracts into focused presenter,
dispatcher, and support tests so follow-up refactors keep the
current IPC and hot-path behavior intact.

* fix(config): harden expand_tilde and fix Windows XDG state path doubling

- expand_tilde: handle bare ~ and ~/ explicitly to avoid std::out_of_range
  on substr(2) for single-char input
- getXDGStateHome: on Windows, go up two levels from %LOCALAPPDATA%\yams\state
  to %LOCALAPPDATA% so callers' xdg/yams append doesn't double the yams segment

* fix(static-analysis): add logging to empty catch blocks and fix nosemgrep syntax

* fix(static-analysis): strip trailing text from nosemgrep comments

* fix(doctor): add logging to daemon_check empty catch blocks

* fix(db_repair): add logging to empty catch blocks

* fix(chunking): add precondition checks for rabin polynomial and windowSize

* fix(safety): add null precondition checks for Fold proved null-deref sites

* fix(daemon): widen init-failure dispatch, add null/dbz preconditions

InitializationFailedEvent races with shutdown — async init
fails after teardown begins. Widen canDispatch to accept
Ready/ShuttingDown. Add static_assert on SnapshotRing N>0.
Add YAMS_PRECONDITION null guards on hex/stages/data/bytes.

* ci(ubsan): use restricted build targets to avoid runner timeout

* fix(daemon): detach wake timers on Windows stop path to prevent SIGSEGV

* fix(config): cast parseTomlU32 to size_t for batch fields

* fix(ci): use static helper for Windows timer leak, restrict ubsan tests
2026-06-04 12:53:42 -04:00

833 lines
34 KiB
YAML

name: Tests
on:
push:
branches: [main, master, experimental]
pull_request:
branches: [main] # Or your default branch
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
# Keep main-branch history intact so slower matrix lanes still publish evidence.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
warm:
uses: ./.github/workflows/matrix-warm.yml
with:
build_type: Debug
enable_onnx: false
quality:
name: Static Analysis
runs-on: ubuntu-24.04
timeout-minutes: 20
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: false
- name: Initialize required submodules
shell: bash
run: |
git submodule update --init --depth 1 third_party/sqlite-vec-cpp || true
git submodule update --init --depth 1 third_party/simeon || true
git submodule update --init --depth 1 third_party/symspell || true
- name: Install static analysis tools
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends cppcheck clang-tidy
- name: Run cppcheck (CI profile, changed lines)
shell: bash
run: |
set -euo pipefail
BASE=""
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
BASE="${{ github.event.pull_request.base.sha }}"
elif [[ -n "${{ github.event.before }}" && "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]]; then
BASE="${{ github.event.before }}"
elif git rev-parse HEAD^ >/dev/null 2>&1; then
BASE="HEAD^"
fi
# Push events can report a `before` SHA that is no longer reachable
# after a force-push/rewrite. In that case, fall back to the checked
# out parent instead of failing before cppcheck runs.
if [[ -n "$BASE" ]] && ! git rev-parse --verify "${BASE}^{commit}" >/dev/null 2>&1; then
echo "Diff base '$BASE' is not available in this checkout; falling back to HEAD^."
if git rev-parse HEAD^ >/dev/null 2>&1; then
BASE="HEAD^"
else
BASE=""
fi
fi
if [[ -n "$BASE" ]]; then
scripts/lint.sh cppcheck --diff "$BASE"
else
scripts/lint.sh cppcheck
fi
- name: Run clang-tidy on changed files (PR only)
if: github.event_name == 'pull_request'
shell: bash
run: |
set -euo pipefail
COMPILE_DB_DIR=""
if [ -f builddir/compile_commands.json ]; then
COMPILE_DB_DIR="builddir"
elif [ -f compile_commands.json ]; then
COMPILE_DB_DIR="."
else
echo "No compile_commands.json found; skipping clang-tidy." | tee clang-tidy-pr.txt
exit 0
fi
mapfile -t CHANGED_FILES < <(git diff --name-only "origin/${{ github.base_ref }}...HEAD" \
| grep -E '\.(cpp|h|hpp|cc|cxx)$' \
| grep -v -E '(third_party|external|subprojects|\.pb\.(h|cc))' \
|| true)
if [ "${#CHANGED_FILES[@]}" -eq 0 ]; then
echo "No C++ files changed in this PR."
exit 0
fi
printf 'Running clang-tidy on:\n'
printf ' %s\n' "${CHANGED_FILES[@]}"
clang-tidy \
-p "$COMPILE_DB_DIR" \
--warnings-as-errors='bugprone-*,clang-analyzer-*,concurrency-*' \
"${CHANGED_FILES[@]}" \
2>&1 | tee clang-tidy-pr.txt
- name: Upload clang-tidy results
if: github.event_name == 'pull_request' && always()
uses: actions/upload-artifact@v4
with:
name: clang-tidy-pr-${{ github.run_number }}
path: clang-tidy-pr.txt
if-no-files-found: ignore
retention-days: 7
tests:
needs: warm
if: ${{ success() }}
name: Tests (${{ matrix.suffix }})
env:
BUILD_DIR: builddir
YAMS_DISABLE_FAISS: 1
strategy:
fail-fast: false
matrix:
include:
- suffix: linux-x86_64
runs_on: ubuntu-24.04
conan_host_profile: ./conan/profiles/host-linux-clang
conan_arch: x86_64
needs_rosetta: false
build_type: Debug
coverage: true
extra_meson_flags: "--buildtype=debug -Dbuild-tests=true -Denable-onnx=disabled -Dtest-timeout-scale=2 -Denable-dcheck=true"
- suffix: linux-arm64
runs_on: ubuntu-24.04-arm
conan_host_profile: ./conan/profiles/host-linux-clang
conan_arch: armv8
needs_rosetta: false
build_type: Debug
coverage: false
extra_meson_flags: "--buildtype=debug -Dbuild-tests=true -Denable-onnx=disabled -Dtest-timeout-scale=2 -Denable-bench-tests=false -Dbuild-benchmarks=false -Denable-dcheck=true"
- suffix: macos-arm64
runs_on: macos-15
conan_host_profile: ./conan/profiles/host-macos-apple-clang
conan_arch: armv8
needs_rosetta: false
build_type: Debug
coverage: false
extra_meson_flags: "--buildtype=debug -Dbuild-tests=true -Denable-onnx=disabled -Dtest-timeout-scale=2 -Denable-dcheck=true"
- suffix: windows-x86_64
runs_on: windows-latest
conan_host_profile: ./conan/profiles/host-windows-msvc
conan_arch: x86_64
needs_rosetta: false
build_type: Debug
coverage: false
extra_meson_flags: "--buildtype=debug -Dbuild-tests=true -Denable-onnx=disabled -Dtest-timeout-scale=2 -Denable-dcheck=true"
- suffix: linux-x86_64-asan
runs_on: ubuntu-24.04
conan_host_profile: ./conan/profiles/host-linux-clang
conan_arch: x86_64
needs_rosetta: false
build_type: Debug
coverage: false
extra_meson_flags: "--buildtype=debug -Dbuild-tests=true -Denable-onnx=disabled -Denable-asan=true -Db_sanitize=address -Db_lundef=false -Dtest-timeout-scale=2 -Denable-bench-tests=false -Dbuild-benchmarks=false -Denable-dcheck=true"
- suffix: linux-x86_64-ubsan
runs_on: ubuntu-24.04
conan_host_profile: ./conan/profiles/host-linux-clang
conan_arch: x86_64
needs_rosetta: false
build_type: Debug
coverage: false
extra_meson_flags: "--buildtype=debug -Dbuild-tests=true -Denable-onnx=disabled -Denable-ubsan=true -Db_sanitize=undefined -Db_lundef=false -Dtest-timeout-scale=2 -Denable-dcheck=true"
# TSAN intentionally excluded from CI: boost::asio strand destruction triggers
# a heap-use-after-free that TSAN detects but cannot be suppressed via tsan.suppressions.
# Run TSAN locally with: meson setup builddir-tsan -Denable-tsan=true -Db_sanitize=thread
runs-on: ${{ matrix.runs_on }}
# Windows debug builds can consume most of the previous 100-minute budget before tests start.
timeout-minutes: 180
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: false # Manual submodule init below (some use SSH URLs)
- name: Skip non-linux-x86_64 matrix entries under act
if: github.actor == 'nektos/act' && matrix.suffix != 'linux-x86_64'
shell: bash
run: |
echo "Skipping matrix entry '${{ matrix.suffix }}' under act."
echo "Act provides Linux container execution only; keep local runs deterministic on linux-x86_64."
exit 0
- name: Free disk space (Linux)
if: runner.os == 'Linux'
uses: jlumbroso/free-disk-space@v1.3.1
with:
tool-cache: false # Keep tools we might need
android: true # ~14GB savings
dotnet: true # ~2.7GB savings
haskell: true # Haskell runtime
large-packages: false # Keep - might need some of these
docker-images: true # Cached docker images
swap-storage: false # Keep swap for large links
- name: Initialize required submodules
shell: bash
run: |
retry_cmd() {
local attempts="$1"
shift
local attempt=1
until "$@"; do
if [ "$attempt" -ge "$attempts" ]; then
return 1
fi
echo "Command failed (attempt ${attempt}/${attempts}): $*" >&2
sleep $((attempt * 5))
attempt=$((attempt + 1))
done
}
# Force HTTPS for GitHub to handle SSH submodule URLs in CI
git config --global url."https://github.com/".insteadOf "git@github.com:"
# Only init submodules required for the build
retry_cmd 4 git submodule update --init --depth 1 third_party/sqlite-vec-cpp
retry_cmd 4 git submodule update --init --depth 1 third_party/simeon
retry_cmd 4 git submodule update --init --depth 1 third_party/symspell
- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail
retry_cmd() {
local attempts="$1"
shift
local attempt=1
until "$@"; do
if [ "$attempt" -ge "$attempts" ]; then
return 1
fi
echo "Command failed (attempt ${attempt}/${attempts}): $*" >&2
sleep $((attempt * 10))
attempt=$((attempt + 1))
done
}
if [ "$(dpkg --print-architecture)" = "arm64" ]; then
source_files="$(sudo grep -RIl '://ports.ubuntu.com/ubuntu-ports' \
/etc/apt/sources.list /etc/apt/sources.list.d 2>/dev/null || true)"
if [ -n "$source_files" ]; then
printf '%s\n' "$source_files" | xargs sudo sed -i \
's|http://ports.ubuntu.com/ubuntu-ports|https://azure.ports.ubuntu.com/ubuntu-ports|g; s|https://ports.ubuntu.com/ubuntu-ports|https://azure.ports.ubuntu.com/ubuntu-ports|g'
fi
fi
retry_cmd 4 sudo apt-get -o Acquire::Retries=5 -o Acquire::ForceIPv4=true update
# Provide system Boost as a fallback per docs/BUILD-GCC.md when CMakeDeps resolution
# is not detected by CMake (module mode will pick this up).
# Install libbenchmark-dev for Google Benchmark support
retry_cmd 4 sudo apt-get -o Acquire::Retries=5 -o Acquire::ForceIPv4=true \
install -y --fix-missing --no-install-recommends \
libboost-all-dev pkg-config libbenchmark-dev xz-utils libomp-18-dev
PYTOOLS_DIR="${RUNNER_TEMP}/yams-python-tools"
python3 -m venv "$PYTOOLS_DIR"
"$PYTOOLS_DIR/bin/python" -m pip install --upgrade pip
"$PYTOOLS_DIR/bin/python" -m pip install --upgrade 'meson==1.4.*' 'ninja==1.11.*'
echo "$PYTOOLS_DIR/bin" >> "$GITHUB_PATH"
export PATH="$PYTOOLS_DIR/bin:$PATH"
echo "meson version: $(meson --version || echo 'missing')"
echo "ninja version: $(ninja --version || echo 'missing')"
echo "Installed system Boost headers/libs for module-mode fallback"
- name: Install Zig (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail
ARCH=$(uname -m)
case "$ARCH" in
x86_64) ZIG_ARCH="x86_64" ;;
aarch64) ZIG_ARCH="aarch64" ;;
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
esac
ZIG_VERSION="0.15.2"
curl -fsSL "https://ziglang.org/download/${ZIG_VERSION}/zig-${ZIG_ARCH}-linux-${ZIG_VERSION}.tar.xz" -o /tmp/zig.tar.xz
sudo tar -xJf /tmp/zig.tar.xz -C /opt
sudo mv /opt/zig-${ZIG_ARCH}-linux-${ZIG_VERSION} /opt/zig
sudo ln -sf /opt/zig/zig /usr/local/bin/zig
rm /tmp/zig.tar.xz
zig version
- name: Install system dependencies (macOS)
if: runner.os == 'macOS'
shell: bash
run: |
set -euo pipefail
for attempt in 1 2 3; do
if brew update; then
break
fi
if [ "$attempt" -eq 3 ]; then
echo "brew update failed after ${attempt} attempts" >&2
exit 1
fi
sleep $((attempt * 10))
done
# Align with release workflow to ensure Meson builds have required toolchain bits.
# Install google-benchmark for benchmark tests, zig for zyp PDF plugin
brew install pkg-config cmake ccache boost google-benchmark zig || true
PYTOOLS_DIR="${RUNNER_TEMP}/yams-python-tools"
python3 -m venv "$PYTOOLS_DIR"
"$PYTOOLS_DIR/bin/python" -m pip install --upgrade pip
"$PYTOOLS_DIR/bin/python" -m pip install --upgrade 'meson==1.4.*' 'ninja==1.11.*'
echo "$PYTOOLS_DIR/bin" >> "$GITHUB_PATH"
export PATH="$PYTOOLS_DIR/bin:$PATH"
echo "meson version: $(meson --version || echo 'missing')"
echo "ninja version: $(ninja --version || echo 'missing')"
echo "zig version: $(zig version || echo 'missing')"
- name: Install system dependencies (Windows)
if: runner.os == 'Windows'
shell: bash
run: |
set -euo pipefail
PYTOOLS_DIR="${RUNNER_TEMP}/yams-python-tools"
python -m venv "$PYTOOLS_DIR"
"$PYTOOLS_DIR/Scripts/python.exe" -m pip install --upgrade pip
"$PYTOOLS_DIR/Scripts/python.exe" -m pip install --upgrade 'meson==1.4.*' 'ninja==1.11.*'
echo "$PYTOOLS_DIR/Scripts" >> "$GITHUB_PATH"
export PATH="$PYTOOLS_DIR/Scripts:$PATH"
echo "meson version: $(meson --version || echo 'missing')"
echo "ninja version: $(ninja --version || echo 'missing')"
# Clean stale builddir to avoid Meson version incompatibility
if [ -d "${{ env.BUILD_DIR }}" ]; then
echo "Removing stale builddir to ensure clean Meson configuration"
rm -rf "${{ env.BUILD_DIR }}"
fi
- name: Install Zig (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$zigVersion = "0.15.2"
$zigUrl = "https://ziglang.org/download/${zigVersion}/zig-x86_64-windows-${zigVersion}.zip"
$zigZip = "$env:TEMP\zig.zip"
$zigDir = "C:\zig"
Invoke-WebRequest -Uri $zigUrl -OutFile $zigZip
Expand-Archive -Path $zigZip -DestinationPath $zigDir -Force
Move-Item "$zigDir\zig-x86_64-windows-${zigVersion}\*" $zigDir -Force
Remove-Item "$zigDir\zig-x86_64-windows-${zigVersion}" -Recurse -ErrorAction SilentlyContinue
$env:PATH = "$zigDir;$env:PATH"
"$zigDir" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
zig version
- name: Cache Conan packages
id: cache_conan
uses: actions/cache@v4
with:
path: |
~/.conan2/p
~/.conan2/r
~/.conan2/recipes
~/.conan2/metadata
~/.conan2/s
key: conan2-${{ runner.os }}-${{ matrix.suffix }}-${{ hashFiles('conanfile.py', 'conan/profiles/**') }}
restore-keys: |
conan2-${{ runner.os }}-${{ matrix.suffix }}-
conan2-${{ runner.os }}-linux-x86_64-
conan2-${{ runner.os }}-
- name: Conan cache diagnostics
shell: bash
run: |
echo "Conan cache hit: ${{ steps.cache_conan.outputs.cache-hit }}"
du -sh ~/.conan2/p 2>/dev/null || true
ls ~/.conan2/r 2>/dev/null | head || true
- name: Cache ccache
if: matrix.suffix != 'linux-arm64'
uses: actions/cache@v4
with:
path: |
~/.cache/ccache
~/.ccache
key: ccache-${{ runner.os }}-${{ matrix.suffix }}-${{ hashFiles('conanfile.py', 'meson.build', 'meson_options.txt') }}
restore-keys: |
ccache-${{ runner.os }}-${{ matrix.suffix }}-
ccache-${{ runner.os }}-linux-x86_64-
ccache-${{ runner.os }}-
- name: Install Conan (latest)
shell: bash
run: |
set -euo pipefail
PYTOOLS_DIR="${RUNNER_TEMP}/yams-python-tools"
if [ "$RUNNER_OS" = "Windows" ]; then
VENV_PYTHON="$PYTOOLS_DIR/Scripts/python.exe"
TOOL_BIN="$PYTOOLS_DIR/Scripts"
if [ ! -f "$VENV_PYTHON" ]; then
python -m venv "$PYTOOLS_DIR"
fi
else
VENV_PYTHON="$PYTOOLS_DIR/bin/python"
TOOL_BIN="$PYTOOLS_DIR/bin"
if [ ! -x "$VENV_PYTHON" ]; then
python3 -m venv "$PYTOOLS_DIR"
fi
fi
"$VENV_PYTHON" -m pip install --upgrade pip
"$VENV_PYTHON" -m pip install --upgrade 'conan==2.21.*' 'meson==1.4.*' 'ninja==1.11.*'
echo "$TOOL_BIN" >> "$GITHUB_PATH"
export PATH="$TOOL_BIN:$PATH"
conan --version
meson --version
ninja --version
mkdir -p ~/.conan2
cp .conan/global.conf ~/.conan2/global.conf 2>/dev/null || true
# Continue directly with build/test steps (single job path)
- name: Install optional system dependencies
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail
extra_packages=(liburing-dev libarchive-dev libtag1-dev)
if [ "${{ matrix.suffix }}" != "linux-arm64" ]; then
extra_packages+=(ccache)
fi
sudo apt-get install -y --no-install-recommends "${extra_packages[@]}" || true
echo "Optional packages installed (best-effort)."
- name: Setup ccache env
if: matrix.suffix != 'linux-arm64'
shell: bash
run: |
set -euo pipefail
echo "CCACHE_DIR=$HOME/.cache/ccache" >> $GITHUB_ENV
echo "CCACHE_BASEDIR=$GITHUB_WORKSPACE" >> $GITHUB_ENV
echo "CCACHE_COMPRESS=1" >> $GITHUB_ENV
echo "CCACHE_MAXSIZE=5G" >> $GITHUB_ENV
echo "CCACHE_SLOPPINESS=time_macros,include_file_mtime,include_file_ctime,file_macro" >> $GITHUB_ENV
ccache -p 2>/dev/null || true
ccache -z 2>/dev/null || true
- name: Reclaim disk for linux-arm64 builds
if: matrix.suffix == 'linux-arm64'
shell: bash
run: |
set -euo pipefail
echo "YAMS_SKIP_CCACHE=1" >> "$GITHUB_ENV"
rm -rf ~/.cache/ccache ~/.ccache || true
python3 -m pip cache purge || true
sudo apt-get clean || true
df -h
# Conan already installed above
- name: Setup Conan Profile
shell: bash
run: |
conan profile detect --force
sed -i 's/compiler.cppstd=.*/compiler.cppstd=20/' ~/.conan2/profiles/default || true
echo "Conan profile configured:" >&2
conan profile show >&2
- name: Detect clang version (log only)
shell: bash
run: |
set -euo pipefail
if command -v clang++ >/dev/null 2>&1; then
RAW_VER=$(clang++ --version | sed -n 's/.*version \([0-9][0-9]*\).*/\1/p' | head -1 || true)
elif command -v clang >/dev/null 2>&1; then
RAW_VER=$(clang --version | sed -n 's/.*version \([0-9][0-9]*\).*/\1/p' | head -1 || true)
else
RAW_VER=""
fi
if [ -n "$RAW_VER" ]; then
echo "Detected clang major: $RAW_VER"
else
echo "WARNING: clang not found; default profile may resolve gcc instead" >&2
fi
- name: Clean corrupted Conan cache if needed
shell: bash
run: |
if ! conan list "*" 2>/dev/null;
then
echo "Conan cache appears corrupted, cleaning locks and temp files..."
conan cache clean --locks --temp 2>/dev/null || true
echo "Conan cache locks/temp cleaned"
fi
- name: Tune Conan client (parallel downloads)
shell: bash
run: |
set -euo pipefail
CONF_FILE="$HOME/.conan2/global.conf"
LINE="core.download:parallel=8"
mkdir -p "$(dirname "$CONF_FILE")"
if [ -f "$CONF_FILE" ]; then
TMP_FILE="$(mktemp)"
awk -v line="$LINE" '
BEGIN { replaced = 0 }
/^core.download:parallel=/ { if (!replaced) { print line; replaced = 1 } ; next }
{ print }
END { if (!replaced) print line }
' "$CONF_FILE" > "$TMP_FILE"
mv "$TMP_FILE" "$CONF_FILE"
else
printf '%s\n' "$LINE" > "$CONF_FILE"
fi
echo "Configured $LINE in $CONF_FILE"
grep -n '^core.download:parallel=' "$CONF_FILE" || true
conan config show core.download:parallel 2>/dev/null || true
conan --version
- name: Configure and Build with Meson (Unix)
id: build_unix
if: runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
ARCH_PREFIX=""
if [ "${{ matrix.needs_rosetta }}" = "true" ]; then
softwareupdate --install-rosetta --agree-to-license || true
ARCH_PREFIX="arch -x86_64"
fi
# Keep AF_UNIX socket path short on Linux to avoid sun_path length limits
if [ "$RUNNER_OS" = "Linux" ]; then
XDG_RUNTIME_DIR="/run/user/$(id -u)"
mkdir -p "$XDG_RUNTIME_DIR" || true
echo "XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR" >> "$GITHUB_ENV"
fi
# Use setup.sh with CI environment variables for consistent build configuration
export YAMS_CONAN_HOST_PROFILE="${{ matrix.conan_host_profile }}"
export YAMS_CONAN_ARCH="${{ matrix.conan_arch }}"
export YAMS_EXTRA_MESON_FLAGS="${{ matrix.extra_meson_flags }}"
export YAMS_ENABLE_MOBILE_BINDINGS=false
# Disable TSAN in CI - boost::asio strand destruction causes heap-use-after-free
# that TSAN detects but cannot be suppressed. Run TSAN locally for development.
export ENABLE_TSAN=false
chmod +x setup.sh
SETUP_ARGS=(Debug)
if [ "${{ matrix.coverage }}" = "true" ]; then
SETUP_ARGS+=(--coverage)
fi
$ARCH_PREFIX ./setup.sh "${SETUP_ARGS[@]}"
# Source Conan build env so meson compile uses the same meson version
# that setup.sh used for meson setup (Conan provides meson 1.10.x which
# creates build.dat incompatible with older system meson).
CONAN_BUILD_ENV="builddir/build-debug/conan/conanbuild.sh"
if [ -f "${CONAN_BUILD_ENV}" ]; then set +u; source "${CONAN_BUILD_ENV}"; set -u; fi
if [ "${{ matrix.suffix }}" = "linux-x86_64-asan" ] || \
[ "${{ matrix.suffix }}" = "linux-x86_64-ubsan" ]; then
$ARCH_PREFIX meson compile -C builddir \
catch2_daemon_metrics \
catch2_post_ingest_queue_unit \
catch2_storage_submodule \
catch2_work_coordinator
else
$ARCH_PREFIX meson compile -C builddir
fi
- name: Configure and Build with Meson (Windows)
id: build_windows
if: runner.os == 'Windows'
shell: pwsh
env:
YAMS_CONAN_HOST_PROFILE: ${{ matrix.conan_host_profile }}
YAMS_CONAN_ARCH: ${{ matrix.conan_arch }}
YAMS_EXTRA_MESON_FLAGS: ${{ matrix.extra_meson_flags }}
YAMS_ENABLE_MOBILE_BINDINGS: "false"
run: |
.\setup.ps1 Debug
# Diagnostic for Windows Boost lookup. Reads BOOST_* env vars set by
# setup.ps1 (persisted to GITHUB_ENV) and lists the actual Conan boost
# lib filenames so we can pattern-match meson's expected naming if the
# build above fails. continue-on-error + always() so it never blocks.
- name: (diag) Boost env + Conan lib layout (Windows)
if: runner.os == 'Windows' && always()
shell: pwsh
continue-on-error: true
run: |
Write-Host "BOOST_ROOT=$env:BOOST_ROOT"
Write-Host "BOOST_INCLUDEDIR=$env:BOOST_INCLUDEDIR"
Write-Host "BOOST_LIBRARYDIR=$env:BOOST_LIBRARYDIR"
if ($env:BOOST_LIBRARYDIR -and (Test-Path $env:BOOST_LIBRARYDIR)) {
Write-Host "--- $env:BOOST_LIBRARYDIR contents (boost_*) ---"
Get-ChildItem $env:BOOST_LIBRARYDIR -Filter 'boost_*' | Select Name | Format-Table -AutoSize | Out-String
Write-Host "--- $env:BOOST_LIBRARYDIR contents (libboost_*) ---"
Get-ChildItem $env:BOOST_LIBRARYDIR -Filter 'libboost_*' -ErrorAction SilentlyContinue | Select Name | Format-Table -AutoSize | Out-String
}
- name: ccache stats (post-build)
if: always() && runner.os != 'Windows'
shell: bash
run: |
set +e
ccache -s 2>/dev/null || true
du -sh "${CCACHE_DIR:-$HOME/.cache/ccache}" 2>/dev/null || true
# - name: Check mobile ABI surface
# shell: bash
# run: |
# set -euo pipefail
# LIB_PATH=$(find "${{ env.BUILD_DIR }}" -type f \( -name 'libyams_mobile*.so' -o -name 'libyams_mobile*.dylib' -o -name 'yams_mobile.dll' \) | head -n1)
# if [ -z "$LIB_PATH" ]; then
# echo "libyams_mobile shared library not found under ${{ env.BUILD_DIR }}" >&2
# exit 1
# fi
# scripts/ci/check_mobile_abi.sh "$LIB_PATH"
- name: Run Tests with Meson (Unix)
id: meson_tests_unix
if: ${{ !matrix.needs_rosetta && runner.os != 'Windows' }}
shell: bash
continue-on-error: true
env:
YAMS_TEST_SAFE_SINGLE_INSTANCE: "1"
YAMS_SQLITE_MINIMAL_PRAGMAS: "1"
YAMS_SQLITE_VEC_INIT_TIMEOUT_MS: "1500"
YAMS_SQLITE_BUSY_TIMEOUT_MS: "1000"
YAMS_VDB_IN_MEMORY: "1"
YAMS_SQLITE_VEC_SKIP_INIT: "1"
YAMS_DISABLE_VECTORS: "1"
YAMS_TESTING: "1"
run: |
set -euo pipefail
if [ "$RUNNER_OS" = "Linux" ]; then
mkdir -p "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" || true
echo "Using XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
fi
CONAN_BUILD_ENV=""
for candidate in \
"${{ env.BUILD_DIR }}/build-debug/conan/conanbuild.sh" \
"${{ env.BUILD_DIR }}/conan/conanbuild.sh"
do
if [ -f "$candidate" ]; then
CONAN_BUILD_ENV="$candidate"
break
fi
done
if [ -n "$CONAN_BUILD_ENV" ]; then
set +u
source "$CONAN_BUILD_ENV"
set -u
fi
# Run unit and integration tests (TSAN disabled in CI). ASAN is a sanitizer smoke lane;
# build/test selected targets only so hosted runners do not get recycled.
if [ "${{ matrix.suffix }}" = "linux-x86_64-asan" ] || \
[ "${{ matrix.suffix }}" = "linux-x86_64-ubsan" ]; then
meson test -C ${{ env.BUILD_DIR }} --no-rebuild --print-errorlogs --timeout-multiplier 2 \
daemon_metrics_status \
post_ingest_queue_unit \
storage_submodule \
work_coordinator
else
meson test -C ${{ env.BUILD_DIR }} --suite unit --suite integration --print-errorlogs --timeout-multiplier 2
fi
- name: Run Tests with Meson (Windows)
id: meson_tests_windows
if: runner.os == 'Windows'
shell: pwsh
continue-on-error: true
env:
YAMS_TEST_SAFE_SINGLE_INSTANCE: "1"
YAMS_SQLITE_MINIMAL_PRAGMAS: "1"
YAMS_SQLITE_VEC_INIT_TIMEOUT_MS: "1500"
YAMS_SQLITE_BUSY_TIMEOUT_MS: "1000"
YAMS_VDB_IN_MEMORY: "1"
YAMS_SQLITE_VEC_SKIP_INIT: "1"
YAMS_DISABLE_VECTORS: "1"
YAMS_TESTING: "1"
run: |
$ErrorActionPreference = "Stop"
$conanBuildPs1 = $null
$candidates = @(
(Join-Path "${{ env.BUILD_DIR }}" "build-debug/conan/conanbuild.ps1"),
(Join-Path "${{ env.BUILD_DIR }}" "conan/conanbuild.ps1")
)
foreach ($candidate in $candidates) {
if (Test-Path $candidate) {
$conanBuildPs1 = $candidate
break
}
}
if ($conanBuildPs1) {
. $conanBuildPs1
}
# Run only unit and integration tests (exclude bench, stress, daemon-long, etc.)
meson test -C ${{ env.BUILD_DIR }} --suite unit --suite integration --print-errorlogs --timeout-multiplier 2
- name: Run Tests with Meson (Rosetta x86_64)
id: meson_tests_rosetta
if: ${{ matrix.needs_rosetta }}
shell: bash
continue-on-error: true
env:
YAMS_TEST_SAFE_SINGLE_INSTANCE: "1"
YAMS_SQLITE_MINIMAL_PRAGMAS: "1"
YAMS_SQLITE_VEC_INIT_TIMEOUT_MS: "1500"
YAMS_SQLITE_BUSY_TIMEOUT_MS: "1000"
YAMS_VDB_IN_MEMORY: "1"
YAMS_SQLITE_VEC_SKIP_INIT: "1"
YAMS_DISABLE_VECTORS: "1"
YAMS_TESTING: "1"
run: |
set -euo pipefail
# Ensure Rosetta is present (idempotent)
softwareupdate --install-rosetta --agree-to-license || true
CONAN_BUILD_ENV=""
for candidate in \
"${{ env.BUILD_DIR }}/build-debug/conan/conanbuild.sh" \
"${{ env.BUILD_DIR }}/conan/conanbuild.sh"
do
if [ -f "$candidate" ]; then
CONAN_BUILD_ENV="$candidate"
break
fi
done
if [ -n "$CONAN_BUILD_ENV" ]; then
set +u
source "$CONAN_BUILD_ENV"
set -u
fi
# Run only unit and integration tests (exclude bench, stress, daemon-long, etc.)
arch -x86_64 meson test -C ${{ env.BUILD_DIR }} --suite unit --suite integration --print-errorlogs --timeout-multiplier 2
- name: Upload build and test logs (artifacts)
if: ${{ always() && (steps.build_unix.outcome == 'failure' || steps.build_windows.outcome == 'failure' || steps.meson_tests_unix.outcome == 'failure' || steps.meson_tests_windows.outcome == 'failure' || steps.meson_tests_rosetta.outcome == 'failure') }}
uses: actions/upload-artifact@v4
with:
name: logs-${{ matrix.suffix }}-${{ github.run_number }}
path: |
${{ env.BUILD_DIR }}/meson-logs/
${{ env.BUILD_DIR }}/meson-info/
${{ env.BUILD_DIR }}/meson-logs/testlog.txt
${{ env.BUILD_DIR }}/**/conan-install/**/*.log
${{ env.BUILD_DIR }}/**/conan*.log
if-no-files-found: ignore
retention-days: 14
- name: Job Summary (artifacts link)
if: always()
shell: bash
run: |
echo "### Artifacts" >> "$GITHUB_STEP_SUMMARY"
echo "[View artifacts for this run]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)" >> "$GITHUB_STEP_SUMMARY"
- name: Enforce test failure
if: ${{ always() && (steps.meson_tests_unix.outcome == 'failure' || steps.meson_tests_windows.outcome == 'failure' || steps.meson_tests_rosetta.outcome == 'failure') }}
shell: bash
run: |
echo "Meson tests reported failures; enforcing job failure." >&2
exit 1
- name: Generate Coverage Report
if: matrix.coverage == true
shell: bash
run: |
set -euo pipefail
pip3 install gcovr
GCOV_EXECUTABLE="gcov"
if command -v clang++ >/dev/null 2>&1; then
CLANG_MAJOR=$(clang++ --version | sed -n 's/.*version \([0-9][0-9]*\).*/\1/p' | head -1 || true)
if [ -n "$CLANG_MAJOR" ] && command -v "llvm-cov-${CLANG_MAJOR}" >/dev/null 2>&1; then
GCOV_EXECUTABLE="llvm-cov-${CLANG_MAJOR} gcov -p"
elif command -v llvm-cov >/dev/null 2>&1; then
GCOV_EXECUTABLE="llvm-cov gcov -p"
fi
fi
echo "Using gcov executable: $GCOV_EXECUTABLE"
COMMON_GCOVR_ARGS=(
--root "$GITHUB_WORKSPACE"
--gcov-executable "$GCOV_EXECUTABLE"
--object-directory "$BUILD_DIR"
--exclude-directories "$BUILD_DIR/plugins"
--merge-mode-functions merge-use-line-min
--filter "$GITHUB_WORKSPACE/src"
--filter "$GITHUB_WORKSPACE/include"
--exclude "$GITHUB_WORKSPACE/src/benchmarks"
"$BUILD_DIR"
)
gcovr "${COMMON_GCOVR_ARGS[@]}" \
--html --html-details \
--output "$BUILD_DIR/coverage.html"
gcovr "${COMMON_GCOVR_ARGS[@]}" \
--xml --output "$BUILD_DIR/coverage.xml"
gcovr "${COMMON_GCOVR_ARGS[@]}" \
--print-summary --output "$BUILD_DIR/coverage.txt"
python3 - <<'PY'
import os
import pathlib
import xml.etree.ElementTree as ET
xml_path = pathlib.Path(os.environ["BUILD_DIR"]) / "coverage.xml"
root = ET.parse(xml_path).getroot()
lines_valid = int(root.attrib.get("lines-valid", "0"))
if lines_valid == 0:
raise SystemExit(f"{xml_path} has no instrumented lines")
print(f"coverage.xml lines-valid={lines_valid}")
PY
- name: Upload Coverage Report
if: matrix.coverage == true
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: |
${{ env.BUILD_DIR }}/coverage*.html
${{ env.BUILD_DIR }}/coverage.txt
if-no-files-found: error
retention-days: 5
- name: Upload Coverage XML
if: matrix.coverage == true
uses: actions/upload-artifact@v4
with:
name: coverage-xml
path: ${{ env.BUILD_DIR }}/coverage.xml
if-no-files-found: error
retention-days: 5
- name: Upload Benchmark Results
if: github.actor != 'nektos/act' && matrix.conan_arch == 'x86_64'
uses: actions/upload-artifact@v4
with:
name: benchmark-results-linux-hosted-yams
path: |
${{ env.BUILD_DIR }}/bench_results/**/*.json
${{ env.BUILD_DIR }}/bench_results/*.json
if-no-files-found: ignore
retention-days: 5