mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-10 09:13:53 +08:00
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:
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user