ci: unify smoke/soak/test venues onto canonical leg entries

The staged venue-unification bundle: _smoke.yml onto the shared
wrappers with an extracted-artifact input (CBM_SMOKE_ARTIFACT_DIR),
soak-legs.sh as the one canonical soak entry (quick + query-leak legs
across all venues), protected per-user TEMP roots shared across
venues, clean-disk preflights before every VM/Docker run, win.sh and
run.sh routed through the canonical scripts, soak.yml retired, and
the venue-parity contract (whitelist walker: workflows may provision
or call canonical entries, nothing else).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-07-26 22:23:05 +02:00
parent 4fda0d9064
commit 3fc93d12da
31 changed files with 1948 additions and 500 deletions
+75 -91
View File
@@ -100,58 +100,40 @@ jobs:
with:
name: binaries-${{ matrix.goos }}-${{ matrix.goarch }}
- name: Extract binary
- name: Extract release artifact
run: |
SUFFIX=${{ matrix.variant == 'ui' && '-ui' || '' }}
tar -xzf codebase-memory-mcp${SUFFIX}-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
chmod +x codebase-memory-mcp
mkdir -p "$RUNNER_TEMP/cbm-artifact"
tar -xzf codebase-memory-mcp${SUFFIX}-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz \
-C "$RUNNER_TEMP/cbm-artifact"
chmod +x "$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp"
- name: Start artifact server
# ONE harness for every venue. This is the same wrapper local CI and PR CI
# run, handed the DOWNLOADED artifact instead of a local build: it stages
# the release fixture, serves it on a kernel-assigned port, and runs
# smoke-test.sh under the isolated profile/TEMP the other venues use.
# It replaces an inline copy of that staging logic which had drifted from
# the wrapper (no env isolation, no user-PATH guard, fixed port).
- name: Smoke test (shared wrapper)
run: |
mkdir -p /tmp/smoke-server
cp codebase-memory-mcp /tmp/smoke-server/
OS=${{ matrix.goos }}; ARCH=${{ matrix.goarch }}
SUFFIX=${{ matrix.variant == 'ui' && '-ui' || '' }}
tar -czf "/tmp/smoke-server/codebase-memory-mcp${SUFFIX}-${OS}-${ARCH}.tar.gz" \
-C /tmp/smoke-server codebase-memory-mcp
if [ -n "$SUFFIX" ]; then
cp "/tmp/smoke-server/codebase-memory-mcp${SUFFIX}-${OS}-${ARCH}.tar.gz" \
"/tmp/smoke-server/codebase-memory-mcp-${OS}-${ARCH}.tar.gz"
fi
# The linux binary self-updates from the fully-static "-portable" asset
# (build_update_url in src/cli/cli.c appends -portable on linux; _build.yml's
# build-linux-portable job ships it), so the smoke server must serve that name
# too -- otherwise `update` 404s and Phase 14 fails. Mirror the tarball(s) under
# the -portable name on linux only.
if [ "$OS" = "linux" ]; then
cp "/tmp/smoke-server/codebase-memory-mcp${SUFFIX}-${OS}-${ARCH}.tar.gz" \
"/tmp/smoke-server/codebase-memory-mcp${SUFFIX}-${OS}-${ARCH}-portable.tar.gz"
if [ -n "$SUFFIX" ]; then
cp "/tmp/smoke-server/codebase-memory-mcp${SUFFIX}-${OS}-${ARCH}-portable.tar.gz" \
"/tmp/smoke-server/codebase-memory-mcp-${OS}-${ARCH}-portable.tar.gz"
fi
fi
cd /tmp/smoke-server
sha256sum *.tar.gz > checksums.txt 2>/dev/null || shasum -a 256 *.tar.gz > checksums.txt
python3 -m http.server 18080 -d /tmp/smoke-server &
- name: Smoke test
run: scripts/smoke-test.sh ./codebase-memory-mcp
scripts/smoke-local.sh \
"$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp" ${{ matrix.variant }}
env:
SMOKE_DOWNLOAD_URL: http://localhost:18080
SMOKE_UPDATE_FIXTURE_DIR: /tmp/smoke-server
CBM_SMOKE_ARTIFACT_DIR: ${{ runner.temp }}/cbm-artifact
- name: Security audits
run: |
scripts/security-strings.sh ./codebase-memory-mcp
scripts/security-install.sh ./codebase-memory-mcp
scripts/security-network.sh ./codebase-memory-mcp
BIN="$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp"
scripts/security-strings.sh "$BIN"
scripts/security-install.sh "$BIN"
scripts/security-network.sh "$BIN"
- name: MCP robustness test (linux-amd64 standard only)
if: matrix.variant == 'standard' && matrix.goos == 'linux' && matrix.goarch == 'amd64'
run: |
scripts/security-fuzz.sh ./codebase-memory-mcp
scripts/security-fuzz-random.sh ./codebase-memory-mcp 60
BIN="$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp"
scripts/security-fuzz.sh "$BIN"
scripts/security-fuzz-random.sh "$BIN" 60
- name: ClamAV scan (Linux)
if: startsWith(matrix.os, 'ubuntu')
@@ -165,7 +147,7 @@ jobs:
grep -q "DatabaseMirror" /etc/clamav/freshclam.conf 2>/dev/null || \
echo "DatabaseMirror database.clamav.net" | sudo tee -a /etc/clamav/freshclam.conf > /dev/null
sudo freshclam --quiet
clamscan --no-summary ./codebase-memory-mcp
clamscan --no-summary "$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp"
- name: ClamAV scan (macOS)
if: startsWith(matrix.os, 'macos')
@@ -178,7 +160,7 @@ jobs:
echo "DatabaseMirror database.clamav.net" >> "$CLAMAV_ETC/freshclam.conf"
fi
freshclam --quiet --no-warnings 2>/dev/null || freshclam --quiet 2>/dev/null || echo "WARNING: freshclam update failed"
clamscan --no-summary ./codebase-memory-mcp
clamscan --no-summary "$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp"
smoke-windows:
needs: setup-matrix
@@ -205,66 +187,53 @@ jobs:
with:
name: binaries-windows-${{ matrix.arch }}
- name: Extract binary
- name: Extract release artifact
shell: msys2 {0}
run: |
SUFFIX=${{ matrix.variant == 'ui' && '-ui' || '' }}
ARCH=${{ matrix.arch }}
unzip -o "codebase-memory-mcp${SUFFIX}-windows-${ARCH}.zip"
test -s codebase-memory-mcp.exe
test -s codebase-memory-mcp.payload.exe
ARTIFACT_DIR="$(cygpath -u "$RUNNER_TEMP")/cbm-artifact"
mkdir -p "$ARTIFACT_DIR"
unzip -o "codebase-memory-mcp${SUFFIX}-windows-${ARCH}.zip" -d "$ARTIFACT_DIR"
test -s "$ARTIFACT_DIR/codebase-memory-mcp.exe"
test -s "$ARTIFACT_DIR/codebase-memory-mcp.payload.exe"
# Pre-flight both images from a profile-rooted directory: the shared
# runner workspace is deliberately not a valid launcher bundle root.
PROFILE_ROOT="$(cygpath -u "$USERPROFILE")"
LAUNCH_DIR="$(mktemp -d "$PROFILE_ROOT/cbm-release-version.XXXXXX")"
trap 'rm -rf "$LAUNCH_DIR"' EXIT
cp codebase-memory-mcp.exe codebase-memory-mcp.payload.exe "$LAUNCH_DIR/"
cp "$ARTIFACT_DIR/codebase-memory-mcp.exe" \
"$ARTIFACT_DIR/codebase-memory-mcp.payload.exe" "$LAUNCH_DIR/"
"$LAUNCH_DIR/codebase-memory-mcp.payload.exe" --version
"$LAUNCH_DIR/codebase-memory-mcp.exe" --version
- name: Start artifact server
# ONE harness for every venue: the same script win.sh smoke-install and
# pr.yml run, handed the DOWNLOADED artifact instead of a local build. It
# stages the release fixture under a disposable profile root, serves it on
# a kernel-assigned port, guards AND verifies the user-PATH registry
# mutation, and neutralizes every agent-config destination before running
# smoke-test.sh. It replaces an inline copy of that staging which had none
# of the isolation and never verified the PATH mutation it performed.
- name: Smoke test (shared wrapper)
shell: msys2 {0}
run: |
mkdir -p /tmp/smoke-server
cp codebase-memory-mcp.exe codebase-memory-mcp.payload.exe \
LICENSE install.ps1 THIRD_PARTY_NOTICES.md /tmp/smoke-server/
SUFFIX=${{ matrix.variant == 'ui' && '-ui' || '' }}
ARCH=${{ matrix.arch }}
cd /tmp/smoke-server
zip -q "codebase-memory-mcp${SUFFIX}-windows-${ARCH}.zip" \
codebase-memory-mcp.exe codebase-memory-mcp.payload.exe \
LICENSE install.ps1 THIRD_PARTY_NOTICES.md
if [ -n "$SUFFIX" ]; then
cp "codebase-memory-mcp${SUFFIX}-windows-${ARCH}.zip" "codebase-memory-mcp-windows-${ARCH}.zip"
fi
sha256sum *.zip > checksums.txt
# Pin to explicit IPv4: on windows-11-arm, `localhost` resolves to ::1 (IPv6)
# for msys2 curl while python's http.server is IPv4-only -> instant connect
# failure in smoke Phase 12a. --bind 127.0.0.1 + a 127.0.0.1 URL pin both ends.
python3 -m http.server 18080 --bind 127.0.0.1 -d /tmp/smoke-server &
- name: Smoke test
shell: msys2 {0}
run: |
PROFILE_ROOT="$(cygpath -u "$USERPROFILE")"
SMOKE_DIR="$(mktemp -d "$PROFILE_ROOT/cbm-release-smoke.XXXXXX")"
trap 'rm -rf "$SMOKE_DIR"' EXIT
cp codebase-memory-mcp.exe codebase-memory-mcp.payload.exe "$SMOKE_DIR/"
CBM_CACHE_DIR="$(cygpath -m "$SMOKE_DIR/cache")" \
SMOKE_TEMP_ROOT="$SMOKE_DIR" \
scripts/smoke-test.sh "$SMOKE_DIR/codebase-memory-mcp.exe"
env:
SMOKE_DOWNLOAD_URL: http://127.0.0.1:18080
SMOKE_UPDATE_FIXTURE_DIR: /tmp/smoke-server
SMOKE_ARCH: ${{ matrix.arch }}
SMOKE_VARIANT: ${{ matrix.variant }}
run: |
export CBM_SMOKE_ARTIFACT_DIR="$(cygpath -u "$RUNNER_TEMP")/cbm-artifact"
bash test-infrastructure/vm/vm-smoke.sh
- name: Security audits
shell: msys2 {0}
run: |
scripts/security-strings.sh ./codebase-memory-mcp.exe
scripts/security-strings.sh ./codebase-memory-mcp.payload.exe
ARTIFACT_DIR="$(cygpath -u "$RUNNER_TEMP")/cbm-artifact"
scripts/security-strings.sh "$ARTIFACT_DIR/codebase-memory-mcp.exe"
scripts/security-strings.sh "$ARTIFACT_DIR/codebase-memory-mcp.payload.exe"
PROFILE_ROOT="$(cygpath -u "$USERPROFILE")"
SECURITY_DIR="$(mktemp -d "$PROFILE_ROOT/cbm-release-security.XXXXXX")"
trap 'rm -rf "$SECURITY_DIR"' EXIT
cp codebase-memory-mcp.exe codebase-memory-mcp.payload.exe "$SECURITY_DIR/"
cp "$ARTIFACT_DIR/codebase-memory-mcp.exe" \
"$ARTIFACT_DIR/codebase-memory-mcp.payload.exe" "$SECURITY_DIR/"
TMPDIR="$SECURITY_DIR" \
scripts/security-install.sh "$SECURITY_DIR/codebase-memory-mcp.exe"
@@ -272,8 +241,9 @@ jobs:
shell: pwsh
run: |
& "C:\Program Files\Windows Defender\MpCmdRun.exe" -SignatureUpdate 2>$null
$artifactDir = Join-Path $env:RUNNER_TEMP "cbm-artifact"
foreach ($binary in @("codebase-memory-mcp.exe", "codebase-memory-mcp.payload.exe")) {
$result = & "C:\Program Files\Windows Defender\MpCmdRun.exe" -Scan -ScanType 3 -File "$PWD\$binary" -DisableRemediation
$result = & "C:\Program Files\Windows Defender\MpCmdRun.exe" -Scan -ScanType 3 -File (Join-Path $artifactDir $binary) -DisableRemediation
$code = $LASTEXITCODE
Write-Host $result
# MpCmdRun -Scan exit codes: 0 = clean, 2 = threat found. Any OTHER non-zero
@@ -304,25 +274,39 @@ jobs:
with:
name: binaries-linux-${{ matrix.arch }}-portable
- name: Extract and smoke test
- name: Extract release artifact
run: |
SUFFIX=${{ matrix.variant == 'ui' && '-ui' || '' }}
tar -xzf codebase-memory-mcp${SUFFIX}-linux-${{ matrix.arch }}-portable.tar.gz
chmod +x codebase-memory-mcp
scripts/smoke-test.sh ./codebase-memory-mcp
mkdir -p "$RUNNER_TEMP/cbm-artifact"
tar -xzf codebase-memory-mcp${SUFFIX}-linux-${{ matrix.arch }}-portable.tar.gz \
-C "$RUNNER_TEMP/cbm-artifact"
chmod +x "$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp"
# Same wrapper as every other venue. This leg previously called
# smoke-test.sh with no fixture server, which silently SKIPPED the
# download/checksum/install-script phases (12-13) — the portable binary is
# what every linux install and update actually delivers, so those are
# exactly the phases it most needs to run.
- name: Smoke test (shared wrapper)
run: |
scripts/smoke-local.sh \
"$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp" ${{ matrix.variant }}
env:
CBM_SMOKE_ARTIFACT_DIR: ${{ runner.temp }}/cbm-artifact
# The -portable binary is what all linux install/update paths now deliver;
# it MUST start on old glibc (Debian 11 / RHEL 8 / Ubuntu 20.04). Runs it
# in debian:bullseye (glibc 2.31) — the standard dynamic binary would fail
# here with `GLIBC_2.38 not found`.
- name: Old-glibc compatibility
run: scripts/ci/check-glibc-compat.sh ./codebase-memory-mcp
run: scripts/ci/check-glibc-compat.sh "$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp"
- name: Security audits
run: |
scripts/security-strings.sh ./codebase-memory-mcp
scripts/security-install.sh ./codebase-memory-mcp
scripts/security-network.sh ./codebase-memory-mcp
BIN="$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp"
scripts/security-strings.sh "$BIN"
scripts/security-install.sh "$BIN"
scripts/security-network.sh "$BIN"
# ── Packaging check (non-gating) ──────────────────────────────────
# Builds pkg/glama/Dockerfile — the image Glama builds to score the
+62 -54
View File
@@ -5,9 +5,13 @@ on:
workflow_call:
inputs:
duration_minutes:
description: 'Soak duration in minutes'
description: 'Soak duration in minutes (per leg)'
type: number
default: 10
legs:
description: "Soak legs for the quick jobs, passed to scripts/soak-legs.sh --legs (e.g. 'quick,query-leak' or 'query-leak' for a long #581-only run)"
type: string
default: 'quick,query-leak'
run_asan:
description: 'Run ASan soak in addition to quick soak'
type: boolean
@@ -47,12 +51,13 @@ jobs:
cc: cc
cxx: c++
runs-on: ${{ matrix.os }}
# BUG FIX: this was hard-coded to 30, but the caller (nightly-soak.yml)
# passes duration_minutes: 240. GitHub killed the job at 30 min, so the
# "4h nightly soak" was SILENTLY TRUNCATED to 30 min and never once ran
# multi-hour. Budget must always exceed the passed duration; 300 covers
# the 240-min nightly with headroom (build + analysis + idle phases).
timeout-minutes: 300
# Budget doctrine (this line has been silently violated TWICE): the job
# timeout must exceed the WORST-CASE run length, and duration_minutes is
# per LEG. Two 240-min legs (the nightly) need ~480 min + build + analysis;
# the previous 300 silently killed the second leg at the timeout — the same
# truncation class as the original hard-coded 30. Keep a literal (the
# inputs context is not reliably available at job-setup time): 560.
timeout-minutes: 560
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install deps (Linux)
@@ -60,15 +65,11 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 git
- name: Build
run: scripts/build.sh ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
- name: Soak (${{ inputs.duration_minutes }} min)
run: scripts/soak-test.sh build/c/codebase-memory-mcp ${{ inputs.duration_minutes }}
# #581 guard: read-only soak (never reindex/mutate) so any memory growth is
# a query-path leak, not WAL/indexing. Reuses the build above.
- name: Query-leak soak (#581, read-only)
env:
CBM_SOAK_MODE: query-leak
RESULTS_DIR: soak-results-query-leak
run: scripts/soak-test.sh build/c/codebase-memory-mcp ${{ inputs.duration_minutes }} --skip-crash-test
# The soak SEQUENCE and its guards live in the ONE canonical entry
# scripts/soak-legs.sh — never in workflow YAML. Leg selection is a
# workflow input so a long #581-only run needs no separate workflow.
- name: Soak legs (${{ inputs.legs }}, ${{ inputs.duration_minutes }} min each)
run: scripts/soak-legs.sh --legs "${{ inputs.legs }}" build/c/codebase-memory-mcp ${{ inputs.duration_minutes }}
- name: Upload metrics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@@ -86,10 +87,9 @@ jobs:
soak-quick-windows:
runs-on: windows-latest
# BUG FIX (same 30→240 mismatch as soak-quick above): the caller passes
# duration_minutes: 240, so a 30-min cap truncated the nightly soak here
# too. 300 covers the 240-min nightly with headroom.
timeout-minutes: 300
# Budget doctrine (see soak-quick): duration_minutes is per LEG, so two
# 240-min legs need ~480 min + build; 300 silently killed the second leg.
timeout-minutes: 560
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
@@ -103,25 +103,28 @@ jobs:
make
git
coreutils
# Same protected root the test legs and the VM soak use. Without it the
# soak ran under the runner's inherited LocalAppData\Temp ACLs while the
# local soak ran hardened — the two venues were soaking different shapes.
- name: Create protected per-user temp root
shell: pwsh
run: |
$root = & scripts/ci/new-protected-temp-root.ps1 `
-Prefix 'cbm-ci-tmp-' `
-ProtectDir (Join-Path $env:GITHUB_WORKSPACE "build\c")
"CBM_CI_TEMP_ROOT=$root" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
- name: Build
shell: msys2 {0}
run: scripts/build.sh ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=clang CXX=clang++
- name: Soak (${{ inputs.duration_minutes }} min)
# Both release-gating legs via the ONE canonical soak entry; only the
# protected-TEMP provisioning is venue text. soak-legs.sh resolves .exe.
- name: Soak legs (${{ inputs.legs }}, ${{ inputs.duration_minutes }} min each)
shell: msys2 {0}
run: |
BIN=build/c/codebase-memory-mcp
[ -f "${BIN}.exe" ] && BIN="${BIN}.exe"
scripts/soak-test.sh "$BIN" ${{ inputs.duration_minutes }}
# #581 guard on Windows — the platform the bug is reported on.
- name: Query-leak soak (#581, read-only)
shell: msys2 {0}
env:
CBM_SOAK_MODE: query-leak
RESULTS_DIR: soak-results-query-leak
run: |
BIN=build/c/codebase-memory-mcp
[ -f "${BIN}.exe" ] && BIN="${BIN}.exe"
scripts/soak-test.sh "$BIN" ${{ inputs.duration_minutes }} --skip-crash-test
export TEMP="$(cygpath -m "$CBM_CI_TEMP_ROOT")"
export TMP="$TEMP"
export TMPDIR="$(cygpath -u "$CBM_CI_TEMP_ROOT")"
scripts/soak-legs.sh --legs "${{ inputs.legs }}" build/c/codebase-memory-mcp ${{ inputs.duration_minutes }}
- name: Upload metrics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@@ -141,7 +144,8 @@ jobs:
# Native ARM64 Windows soak (CLANGARM64, no sanitizer — ASan is unavailable
# on native ARM64 Windows). Builds from source like soak-quick-windows.
runs-on: windows-11-arm
timeout-minutes: 300
# Budget doctrine (see soak-quick): per-leg duration, two legs, 560.
timeout-minutes: 560
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
@@ -155,25 +159,28 @@ jobs:
make
git
coreutils
# Same protected root the test legs and the VM soak use. Without it the
# soak ran under the runner's inherited LocalAppData\Temp ACLs while the
# local soak ran hardened — the two venues were soaking different shapes.
- name: Create protected per-user temp root
shell: pwsh
run: |
$root = & scripts/ci/new-protected-temp-root.ps1 `
-Prefix 'cbm-ci-tmp-' `
-ProtectDir (Join-Path $env:GITHUB_WORKSPACE "build\c")
"CBM_CI_TEMP_ROOT=$root" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
- name: Build
shell: msys2 {0}
run: scripts/build.sh ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=clang CXX=clang++
- name: Soak (${{ inputs.duration_minutes }} min)
# Both release-gating legs via the ONE canonical soak entry; only the
# protected-TEMP provisioning is venue text. soak-legs.sh resolves .exe.
- name: Soak legs (${{ inputs.legs }}, ${{ inputs.duration_minutes }} min each)
shell: msys2 {0}
run: |
BIN=build/c/codebase-memory-mcp
[ -f "${BIN}.exe" ] && BIN="${BIN}.exe"
scripts/soak-test.sh "$BIN" ${{ inputs.duration_minutes }}
# #581 guard on Windows — the platform the bug is reported on.
- name: Query-leak soak (#581, read-only)
shell: msys2 {0}
env:
CBM_SOAK_MODE: query-leak
RESULTS_DIR: soak-results-query-leak
run: |
BIN=build/c/codebase-memory-mcp
[ -f "${BIN}.exe" ] && BIN="${BIN}.exe"
scripts/soak-test.sh "$BIN" ${{ inputs.duration_minutes }} --skip-crash-test
export TEMP="$(cygpath -m "$CBM_CI_TEMP_ROOT")"
export TMP="$TEMP"
export TMPDIR="$(cygpath -u "$CBM_CI_TEMP_ROOT")"
scripts/soak-legs.sh --legs "${{ inputs.legs }}" build/c/codebase-memory-mcp ${{ inputs.duration_minutes }}
- name: Upload metrics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@@ -234,7 +241,10 @@ jobs:
- name: ASan soak (15 min)
env:
ASAN_OPTIONS: "detect_leaks=1:halt_on_error=0:log_path=soak-results/asan"
run: scripts/soak-test.sh build/c/codebase-memory-mcp 15
# Single-leg variant of the canonical soak entry: the ASan build is too
# slow for the two-leg sequence; leg selection is a documented flag, not
# a divergent inline procedure.
run: scripts/soak-legs.sh --legs quick build/c/codebase-memory-mcp 15
- name: Upload metrics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@@ -272,10 +282,8 @@ jobs:
shell: msys2 {0}
env:
ASAN_OPTIONS: "detect_leaks=0:halt_on_error=0:log_path=soak-results/asan"
run: |
BIN=build/c/codebase-memory-mcp
[ -f "${BIN}.exe" ] && BIN="${BIN}.exe"
scripts/soak-test.sh "$BIN" 15
# Single-leg variant of the canonical soak entry (see the unix ASan job).
run: scripts/soak-legs.sh --legs quick build/c/codebase-memory-mcp 15
- name: Upload metrics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+21 -75
View File
@@ -222,8 +222,10 @@ jobs:
export PATH=/usr/lib/ccache:$PATH
sudo sysctl -w vm.mmap_rnd_bits=28 || true
fi
export CCACHE_COMPILERCHECK=content
make -f Makefile.cbm test-tsan CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
# The TSan leg lives in the canonical test entry (same file the
# compose test-tsan service runs); env.sh supplies the verified
# ccache masquerade.
scripts/test.sh --tsan CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
env:
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 1500M
@@ -259,41 +261,15 @@ jobs:
git
- name: Create protected per-user temp root
# The daemon suites fail closed on the MSYS-shared /tmp and on the
# runner's inherited LocalAppData\Temp ACLs. Give the whole harness a
# per-user root under the profile with an owner-stamped, protected
# current-SID DACL, matching the launcher/daemon trust policy.
# One implementation, shared with _soak.yml and the VM leg
# (test-infrastructure/vm/vm-run-tests.sh) — see the script's header for
# why the harness cannot run under a shared or ACL-inherited temp.
shell: pwsh
run: |
$root = Join-Path $env:USERPROFILE ("cbm-ci-tmp-" + [guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Path $root | Out-Null
$sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
$acl = [System.Security.AccessControl.DirectorySecurity]::new()
$acl.SetOwner($sid)
$acl.SetAccessRuleProtection($true, $false)
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
$sid,
[System.Security.AccessControl.FileSystemRights]::FullControl,
([System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
[System.Security.AccessControl.InheritanceFlags]::ObjectInherit),
[System.Security.AccessControl.PropagationFlags]::None,
[System.Security.AccessControl.AccessControlType]::Allow)
$acl.AddAccessRule($rule) | Out-Null
Set-Acl -LiteralPath $root -AclObject $acl
$root = & scripts/ci/new-protected-temp-root.ps1 `
-Prefix 'cbm-ci-tmp-' `
-ProtectDir (Join-Path $env:GITHUB_WORKSPACE "build\c")
"CBM_CI_TEMP_ROOT=$root" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
# Pre-create the build directory with the same protected DACL so
# every build product inherits it. The workspace drive root grants
# Authenticated Users Modify by inheritance — a shape real user
# checkouts (under the profile) do not have — and the activation
# transaction's source-directory policy correctly refuses it, which
# would fail the install-flow tests on the environment, not the code.
$build = Join-Path $env:GITHUB_WORKSPACE "build\c"
New-Item -ItemType Directory -Path $build -Force | Out-Null
$bacl = [System.Security.AccessControl.DirectorySecurity]::new()
$bacl.SetOwner($sid)
$bacl.SetAccessRuleProtection($true, $false)
$bacl.AddAccessRule($rule) | Out-Null
Set-Acl -LiteralPath $build -AclObject $bacl
- name: Compiler cache (content-verified)
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
@@ -324,7 +300,9 @@ jobs:
export TEMP="$(cygpath -m "$CBM_CI_TEMP_ROOT")"
export TMP="$TEMP"
export TMPDIR="$(cygpath -u "$CBM_CI_TEMP_ROOT")"
scripts/test.sh CC=clang CXX=clang++ ${{ matrix.os == 'windows-11-arm' && '"SANITIZE=-fsanitize=undefined -fsanitize-trap=undefined -fstack-protector-strong -fno-omit-frame-pointer"' || '' }}
# No per-leg flag logic here: test.sh itself applies the CLANGARM64
# trap-UBSan default, so local and CI build identical test binaries.
scripts/test.sh CC=clang CXX=clang++
env:
CBM_TEST_SHARD: ${{ matrix.shard }}
CBM_TEST_LEG: ${{ matrix.os }}-${{ matrix.msystem }}
@@ -369,51 +347,19 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
# The union proof lives in the canonical scripts/ci entry, not inline
# YAML (venue-parity contract) — checkout (into repo/, so the manifest
# download below stays untouched) provides the script.
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
path: repo
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: shard-manifest-*
path: manifests
- name: Verify cross-shard union per leg
run: |
set -eu
files=$(find manifests -name shard-manifest.txt | sort)
if [ -z "$files" ]; then
echo "FAIL: no shard manifests were uploaded" >&2
exit 1
fi
rc=0
for leg in $(grep -h '^leg=' $files | sort -u | sed 's/^leg=//'); do
leg_files=$(grep -l "^leg=$leg\$" $files)
n=$(grep -h '^shard=' $leg_files | sed 's|.*/||' | sort -u)
if [ "$(printf '%s\n' "$n" | wc -l)" -ne 1 ]; then
echo "FAIL: $leg shards disagree on shard count: $n" >&2
rc=1
continue
fi
indices=$(grep -h '^shard=' $leg_files | sed 's/^shard=//;s|/.*||' | sort -n)
if [ "$indices" != "$(seq 1 "$n")" ]; then
echo "FAIL: $leg shard indices [$indices] != 1..$n" >&2
rc=1
continue
fi
list_sha=$(grep -h '^list_sha256=' $leg_files | sed 's/^list_sha256=//' | sort -u)
if [ "$(printf '%s\n' "$list_sha" | wc -l)" -ne 1 ]; then
echo "FAIL: $leg shards saw different suite lists" >&2
rc=1
continue
fi
union_sha=$(for f in $leg_files; do
sed -n '/^--- slice ---$/,$p' "$f" | tail -n +2
done | sort | sha256sum | awk '{print $1}')
if [ "$union_sha" != "$list_sha" ]; then
echo "FAIL: $leg union of shard slices != full suite list (GATE-QUALITY LOSS)" >&2
rc=1
continue
fi
echo "OK: $leg — $n shard(s), union of slices == full suite list"
done
exit "$rc"
run: repo/scripts/ci/verify-shard-union.sh manifests
# Windows product-surface regression guards. Distinct from test-windows above
# (the sanitizer C suite): these drive a real product binary + embedded HTTP UI
+26 -3
View File
@@ -1,4 +1,11 @@
# Weekly soak test: 4h sustained load + ASan leak detection
# Soak dispatcher — the weekly scheduled soak AND every on-demand soak run.
#
# Absorbs the retired soak.yml (the standalone "multi-hour #581" workflow): its
# reasons to exist — a job budget that actually covers a 4h run, and the
# query-leak mode that never reindexes so the #581 leak can accumulate — are
# now _soak.yml facts (timeout 560, legs input). A long #581-only run is
# `legs: query-leak` here, not a separate workflow with its own copy of the
# soak procedure. NON-GATING: never a required check, never blocks a merge.
name: Nightly Soak
on:
@@ -7,9 +14,22 @@ on:
workflow_dispatch:
inputs:
duration_minutes:
description: 'Soak duration in minutes (default: 240 = 4h)'
description: 'Soak duration in minutes PER LEG (default: 240 = 4h)'
type: number
default: 240
legs:
description: 'Soak legs (query-leak = the #581 detector alone)'
type: choice
options: ['quick,query-leak', 'quick', 'query-leak']
default: 'quick,query-leak'
run_asan:
description: 'Also run the 15-min ASan soak legs'
type: boolean
default: true
# Iteration convenience carried over from soak.yml: pushing a qa/soak-**
# branch starts a real run from that branch's own copy of the workflows.
push:
branches: ['qa/soak-**']
permissions:
contents: read
@@ -18,5 +38,8 @@ jobs:
soak:
uses: ./.github/workflows/_soak.yml
with:
# On schedule/push events the inputs context is empty — fall back to the
# weekly defaults (4h, both legs, ASan on).
duration_minutes: ${{ inputs.duration_minutes || 240 }}
run_asan: true
legs: ${{ inputs.legs || 'quick,query-leak' }}
run_asan: ${{ github.event_name != 'workflow_dispatch' || inputs.run_asan }}
+4 -10
View File
@@ -156,16 +156,10 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
# Needs a checkout (unlike before): the gate logic lives in the canonical
# scripts/ci entry, not inline YAML (venue-parity contract).
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: All PR stages must have succeeded
env:
RESULTS: ${{ toJSON(needs) }}
run: |
echo "$RESULTS" | python3 -c "
import json, sys
needs = json.load(sys.stdin)
bad = {k: v['result'] for k, v in needs.items() if v['result'] not in ('success', 'skipped')}
if bad:
print('CI NOT OK:', bad)
sys.exit(1)
print('CI OK:', ', '.join(needs))
"
run: scripts/ci/require-all-green.sh
+3 -32
View File
@@ -110,39 +110,10 @@ jobs:
with:
subject-path: checksums.txt
# SBOM content lives in the canonical scripts/ci entry, not inline YAML
# (venue-parity contract): vendored versions are reviewable there.
- name: Generate SBOM
run: |
python3 -c "
import json, glob, os
n_grammars = len([d for d in glob.glob('internal/cbm/vendored/grammars/*') if os.path.isdir(d)])
sbom = {
'spdxVersion': 'SPDX-2.3',
'dataLicense': 'CC0-1.0',
'SPDXID': 'SPDXRef-DOCUMENT',
'name': 'codebase-memory-mcp-${{ inputs.version }}',
'documentNamespace': 'https://github.com/DeusData/codebase-memory-mcp/releases/${{ inputs.version }}',
'creationInfo': {
'created': '$(date -u +%Y-%m-%dT%H:%M:%SZ)',
'creators': ['Tool: codebase-memory-mcp-release-pipeline']
},
'packages': [
{'SPDXID': 'SPDXRef-Package-sqlite3', 'name': 'sqlite3', 'versionInfo': '3.51.3', 'licenseDeclared': 'blessing', 'downloadLocation': 'https://sqlite.org', 'filesAnalyzed': False},
{'SPDXID': 'SPDXRef-Package-yyjson', 'name': 'yyjson', 'versionInfo': '0.12.0', 'licenseDeclared': 'MIT', 'downloadLocation': 'https://github.com/ibireme/yyjson', 'filesAnalyzed': False},
{'SPDXID': 'SPDXRef-Package-mimalloc', 'name': 'mimalloc', 'versionInfo': '3.3.2', 'licenseDeclared': 'MIT', 'downloadLocation': 'https://github.com/microsoft/mimalloc', 'filesAnalyzed': False},
{'SPDXID': 'SPDXRef-Package-xxhash', 'name': 'xxhash', 'versionInfo': '0.8.3', 'licenseDeclared': 'BSD-2-Clause', 'downloadLocation': 'https://github.com/Cyan4973/xxHash', 'filesAnalyzed': False},
{'SPDXID': 'SPDXRef-Package-tre', 'name': 'tre', 'versionInfo': '0.8.0', 'licenseDeclared': 'BSD-2-Clause', 'downloadLocation': 'https://github.com/laurikari/tre', 'filesAnalyzed': False},
{'SPDXID': 'SPDXRef-Package-tree-sitter', 'name': 'tree-sitter', 'versionInfo': '0.24.4', 'licenseDeclared': 'MIT', 'downloadLocation': 'https://github.com/tree-sitter/tree-sitter', 'filesAnalyzed': False},
{'SPDXID': 'SPDXRef-Package-lz4', 'name': 'lz4', 'versionInfo': '1.10.0', 'licenseDeclared': 'BSD-2-Clause', 'downloadLocation': 'https://github.com/lz4/lz4', 'filesAnalyzed': False},
{'SPDXID': 'SPDXRef-Package-zstd', 'name': 'zstd', 'versionInfo': '1.5.7', 'licenseDeclared': 'BSD-3-Clause', 'downloadLocation': 'https://github.com/facebook/zstd', 'filesAnalyzed': False},
{'SPDXID': 'SPDXRef-Package-simplecpp', 'name': 'simplecpp', 'versionInfo': '1.x', 'licenseDeclared': '0BSD', 'downloadLocation': 'https://github.com/danmar/simplecpp', 'filesAnalyzed': False},
{'SPDXID': 'SPDXRef-Package-verstable', 'name': 'verstable', 'versionInfo': '2.2.1', 'licenseDeclared': 'MIT', 'downloadLocation': 'https://github.com/JacksonAllan/Verstable', 'filesAnalyzed': False},
{'SPDXID': 'SPDXRef-Package-wyhash', 'name': 'wyhash', 'versionInfo': 'final-4.3', 'licenseDeclared': 'Unlicense', 'downloadLocation': 'https://github.com/wangyi-fudan/wyhash', 'filesAnalyzed': False},
{'SPDXID': 'SPDXRef-Package-nomic-embed-code', 'name': 'nomic-embed-code-token-embeddings', 'versionInfo': '1.0', 'licenseDeclared': 'Apache-2.0', 'downloadLocation': 'https://huggingface.co/nomic-ai/nomic-embed-code', 'filesAnalyzed': False, 'comment': 'Derived int8 token embeddings; see vendored/nomic/NOTICE'},
{'SPDXID': 'SPDXRef-Package-tree-sitter-grammars', 'name': 'tree-sitter-grammars-aggregate', 'versionInfo': f'{n_grammars}-grammars', 'licenseDeclared': 'MIT', 'downloadLocation': 'NOASSERTION', 'filesAnalyzed': False, 'comment': f'Aggregate of {n_grammars} vendored tree-sitter grammars, compiled statically. Predominantly MIT; non-MIT families present include CC0-1.0 (clojure, fennel), Apache-2.0 (elixir, erlang, gleam, hcl, ini, jinja2, just, pkl, sway, wit), ISC (pine, templ), Unlicense (fish); first-party grammars (c) DeusData, MIT. This summary is non-exhaustive; the authoritative complete per-grammar license list is internal/cbm/vendored/grammars/MANIFEST.md, and full license texts ship in THIRD_PARTY_NOTICES.md inside each archive.'}
]
}
json.dump(sbom, open('sbom.json', 'w'), indent=2)
"
run: python3 scripts/ci/generate-sbom.py "${{ inputs.version }}"
- name: Attest SBOM
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
-130
View File
@@ -1,130 +0,0 @@
# Real multi-hour soak — #581 query-only memory-leak reproducer.
#
# WHY THIS EXISTS (separate from _soak.yml / nightly-soak.yml):
# The nightly path was structurally incapable of running a real long soak:
# 1. nightly-soak.yml passes duration_minutes: 240, but _soak.yml's
# soak-quick / soak-asan jobs hard-cap `timeout-minutes: 30` (45 for
# ASan). GitHub kills the job at 30 min → the "4h" soak NEVER ran past
# 30 min. (Fixed in _soak.yml too, but this workflow guarantees the
# right budget for the long #581 run.)
# 2. scripts/soak-test.sh's default mode reindexes every 2 min;
# index_repository triggers cbm_mem_collect (mimalloc page return),
# which sweeps the query-only leak — masking #581 even on a long run.
# This workflow drives CBM_SOAK_MODE=query-leak, which never reindexes
# and never mutates files, so the leak can accumulate and be detected
# by soak-test.sh's RSS slope / ratio / ceiling analysis.
#
# NON-GATING: workflow_dispatch + push to qa/soak-** only. Never a required
# check, never blocks a merge.
#
# CRITICAL: timeout-minutes = duration + 60. A 240-min soak gets ~300 min.
name: Soak (multi-hour #581)
on:
workflow_dispatch:
inputs:
duration_minutes:
description: 'Soak duration in minutes (default: 240 = 4h)'
type: number
default: 240
mode:
description: 'Soak mode (query-leak = #581 detector, no reindex/mutate)'
type: choice
options: ['default', 'query-leak']
default: 'query-leak'
# Iteration convenience: pushing a qa/soak-** branch starts a real run from
# that branch's own copy of this file (no main merge needed). Non-gating.
push:
branches: ['qa/soak-**']
permissions:
contents: read
jobs:
# ── Unix: full matrix (linux amd64+arm64, darwin arm64+amd64) ──────────────
soak-unix:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
cc: gcc
cxx: g++
- os: ubuntu-24.04-arm
cc: gcc
cxx: g++
- os: macos-14
cc: cc
cxx: c++
- os: macos-15-intel
cc: cc
cxx: c++
runs-on: ${{ matrix.os }}
# Fixed budget (NOT the 30 min that silently truncated nightly). 320 min covers
# the 240-min default soak + build + analysis. `timeout-minutes` is evaluated at
# workflow setup where the `inputs` context is null on push events, so an
# inputs-based expression here is a startup failure — keep it a literal.
# (A workflow_dispatch run with duration > ~250 min should bump this.)
timeout-minutes: 320
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install deps (Linux)
if: startsWith(matrix.os, 'ubuntu')
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 git
- name: Build (prod binary)
run: scripts/build.sh CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
- name: Soak
env:
# On push events there are no inputs → fall back to shell defaults
# (240 min / query-leak) so a qa/soak-** push runs the real #581 soak.
CBM_SOAK_MODE: ${{ inputs.mode || 'query-leak' }}
DURATION_MINUTES: ${{ inputs.duration_minutes || '240' }}
run: scripts/soak-test.sh build/c/codebase-memory-mcp "${DURATION_MINUTES}"
- name: Upload metrics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: soak-${{ matrix.os }}-${{ inputs.mode || 'query-leak' }}
path: soak-results/
retention-days: 14
# ── Windows: the platform #581 actually crashes on (50+ GB → crash) ───────
soak-windows:
runs-on: windows-latest
timeout-minutes: 320
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
with:
msystem: CLANG64
path-type: inherit
install: >-
mingw-w64-clang-x86_64-clang
mingw-w64-clang-x86_64-zlib
mingw-w64-clang-x86_64-python3
make
git
coreutils
- name: Build (prod binary)
shell: msys2 {0}
run: scripts/build.sh CC=clang CXX=clang++
- name: Soak
shell: msys2 {0}
env:
CBM_SOAK_MODE: ${{ inputs.mode || 'query-leak' }}
DURATION_MINUTES: ${{ inputs.duration_minutes || '240' }}
run: |
BIN=build/c/codebase-memory-mcp
[ -f "${BIN}.exe" ] && BIN="${BIN}.exe"
scripts/soak-test.sh "$BIN" "${DURATION_MINUTES}"
- name: Upload metrics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: soak-windows-${{ inputs.mode || 'query-leak' }}
path: soak-results/
retention-days: 14