Refactor CI: split monolith workflows into reusable components

Before: 2 monolith YAMLs (904 + 1127 lines), duplicated matrices,
inconsistent action versions, duplicate build-windows job.

After: 5 reusable workflows + 3 lean callers (1091 total lines):
- _lint.yml: lint + security-static + codeql-gate
- _test.yml: tests on 5 platforms with CBM_SKIP_PERF support
- _build.yml: standard + UI + portable builds, all platforms
- _smoke.yml: smoke test every binary variant
- _soak.yml: quick + ASan soak, parameterized duration

Fixes: duplicate build-windows, missing Windows CBM_SKIP_PERF,
missing timeout-minutes, inconsistent action versions,
VirusTotal check extracted to scripts/ci/check-virustotal.sh.
This commit is contained in:
Martin Vogel
2026-04-02 16:57:44 +02:00
parent c761106516
commit 87188913bc
9 changed files with 904 additions and 2024 deletions
+181
View File
@@ -0,0 +1,181 @@
# Reusable: build binaries (standard + UI) on all platforms
name: Build
on:
workflow_call:
inputs:
version:
description: 'Version string (e.g. v0.8.0)'
type: string
default: ''
permissions:
contents: read
jobs:
build-unix:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
goos: linux
goarch: amd64
cc: gcc
cxx: g++
- os: ubuntu-24.04-arm
goos: linux
goarch: arm64
cc: gcc
cxx: g++
- os: macos-14
goos: darwin
goarch: arm64
cc: cc
cxx: c++
- os: macos-15-intel
goos: darwin
goarch: amd64
cc: cc
cxx: c++
runs-on: ${{ matrix.os }}
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install deps (Ubuntu)
if: startsWith(matrix.os, 'ubuntu')
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: "22"
- name: Build standard binary
run: scripts/build.sh ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
- name: Ad-hoc sign macOS binary
if: startsWith(matrix.os, 'macos')
run: codesign --sign - --force build/c/codebase-memory-mcp
- name: Archive standard binary
run: |
cp LICENSE install.sh build/c/
tar -czf codebase-memory-mcp-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz \
-C build/c codebase-memory-mcp LICENSE install.sh
- name: Build UI binary
run: scripts/build.sh --with-ui ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
- name: Ad-hoc sign macOS UI binary
if: startsWith(matrix.os, 'macos')
run: codesign --sign - --force build/c/codebase-memory-mcp
- name: Frontend integrity scan
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
run: scripts/security-ui.sh
- name: Archive UI binary
run: |
cp LICENSE install.sh build/c/
tar -czf codebase-memory-mcp-ui-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz \
-C build/c codebase-memory-mcp LICENSE install.sh
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: binaries-${{ matrix.goos }}-${{ matrix.goarch }}
path: "*.tar.gz"
build-windows:
runs-on: windows-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # v2
with:
msystem: CLANG64
path-type: inherit
install: >-
mingw-w64-clang-x86_64-clang
mingw-w64-clang-x86_64-zlib
make
zip
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: "22"
- name: Build standard binary
shell: msys2 {0}
run: scripts/build.sh ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=clang CXX=clang++
- name: Archive standard binary
shell: msys2 {0}
run: |
BIN=build/c/codebase-memory-mcp
[ -f "${BIN}.exe" ] && BIN="${BIN}.exe"
cp "$BIN" codebase-memory-mcp.exe
zip codebase-memory-mcp-windows-amd64.zip codebase-memory-mcp.exe LICENSE install.ps1
- name: Build UI binary
shell: msys2 {0}
run: scripts/build.sh --with-ui ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=clang CXX=clang++
- name: Archive UI binary
shell: msys2 {0}
run: |
BIN=build/c/codebase-memory-mcp
[ -f "${BIN}.exe" ] && BIN="${BIN}.exe"
cp "$BIN" codebase-memory-mcp.exe
zip codebase-memory-mcp-ui-windows-amd64.zip codebase-memory-mcp.exe LICENSE install.ps1
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: binaries-windows-amd64
path: "*.zip"
build-linux-portable:
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
container:
image: alpine:3.21
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install musl build deps
run: apk add --no-cache build-base linux-headers zlib-dev zlib-static nodejs npm
- name: Build standard binary (static)
run: scripts/build.sh ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=gcc CXX=g++ STATIC=1
- name: Verify static linking
run: file build/c/codebase-memory-mcp | grep -q "statically linked"
- name: Archive standard binary
run: |
cp LICENSE install.sh build/c/
tar -czf codebase-memory-mcp-linux-${{ matrix.arch }}-portable.tar.gz \
-C build/c codebase-memory-mcp LICENSE install.sh
- name: Build UI binary (static)
run: scripts/build.sh --with-ui ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=gcc CXX=g++ STATIC=1
- name: Archive UI binary
run: |
cp LICENSE install.sh build/c/
tar -czf codebase-memory-mcp-ui-linux-${{ matrix.arch }}-portable.tar.gz \
-C build/c codebase-memory-mcp LICENSE install.sh
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: binaries-linux-${{ matrix.arch }}-portable
path: "*.tar.gz"
+102
View File
@@ -0,0 +1,102 @@
# Reusable: lint + security-static + codeql-gate
name: Lint & Security
on:
workflow_call: {}
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install build deps
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev cmake
- name: Install LLVM 20
run: |
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-20 main" | sudo tee /etc/apt/sources.list.d/llvm-20.list
sudo apt-get update
sudo apt-get install -y clang-format-20
- uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
id: cppcheck-cache
with:
path: /opt/cppcheck
key: cppcheck-2.20.0-ubuntu-amd64
- name: Build cppcheck 2.20.0
if: steps.cppcheck-cache.outputs.cache-hit != 'true'
run: |
git clone --depth 1 --branch 2.20.0 https://github.com/danmar/cppcheck.git /tmp/cppcheck
cmake -S /tmp/cppcheck -B /tmp/cppcheck/build -DCMAKE_BUILD_TYPE=Release -DHAVE_RULES=OFF -DCMAKE_INSTALL_PREFIX=/opt/cppcheck
cmake --build /tmp/cppcheck/build -j$(nproc)
cmake --install /tmp/cppcheck/build
- name: Add cppcheck to PATH
run: echo "/opt/cppcheck/bin" >> "$GITHUB_PATH"
- name: Lint
run: scripts/lint.sh CLANG_FORMAT=clang-format-20
security-static:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: "Layer 1: Static allow-list audit"
run: scripts/security-audit.sh
- name: "Layer 6: UI security audit"
run: scripts/security-ui.sh
- name: "Layer 8: Vendored dependency integrity"
run: scripts/security-vendored.sh
codeql-gate:
runs-on: ubuntu-latest
timeout-minutes: 50
steps:
- name: Wait for CodeQL on current commit (max 45 min)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
CURRENT_SHA="${{ github.sha }}"
echo "Waiting for CodeQL to complete on $CURRENT_SHA..."
for attempt in $(seq 1 90); do
LATEST=$(gh api repos/${{ github.repository }}/actions/workflows/codeql.yml/runs?per_page=5 \
--jq '.workflow_runs[] | select(.head_sha == "'"$CURRENT_SHA"'") | "\(.conclusion) \(.status)"' 2>/dev/null | head -1 || echo "")
if [ -z "$LATEST" ]; then
echo " $attempt/90: no run yet..."; sleep 30; continue
fi
CONCLUSION=$(echo "$LATEST" | cut -d' ' -f1)
STATUS=$(echo "$LATEST" | cut -d' ' -f2)
if [ "$STATUS" = "completed" ] && [ "$CONCLUSION" = "success" ]; then
echo "=== CodeQL passed ==="; exit 0
elif [ "$STATUS" = "completed" ]; then
echo "BLOCKED: CodeQL $CONCLUSION"; exit 1
fi
echo " $attempt/90: $STATUS..."; sleep 30
done
echo "BLOCKED: CodeQL timeout"; exit 1
- name: Check for open code scanning alerts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Waiting 60s for alert API to settle..."
sleep 60
ALERTS=$(gh api 'repos/${{ github.repository }}/code-scanning/alerts?state=open' --jq 'length' 2>/dev/null || echo "0")
sleep 15
ALERTS2=$(gh api 'repos/${{ github.repository }}/code-scanning/alerts?state=open' --jq 'length' 2>/dev/null || echo "0")
[ "$ALERTS" -lt "$ALERTS2" ] && ALERTS=$ALERTS2
if [ "$ALERTS" -gt 0 ]; then
echo "BLOCKED: $ALERTS open alert(s)"
gh api 'repos/${{ github.repository }}/code-scanning/alerts?state=open' \
--jq '.[] | " #\(.number) [\(.rule.security_severity_level // .rule.severity)] \(.rule.id) — \(.most_recent_instance.location.path):\(.most_recent_instance.location.start_line)"' 2>/dev/null || true
exit 1
fi
echo "=== CodeQL gate passed (0 alerts) ==="
+193
View File
@@ -0,0 +1,193 @@
# Reusable: smoke test every binary (standard + UI, all platforms)
name: Smoke
on:
workflow_call: {}
permissions:
contents: read
jobs:
smoke-unix:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
goos: linux
goarch: amd64
- os: ubuntu-24.04-arm
goos: linux
goarch: arm64
- os: macos-14
goos: darwin
goarch: arm64
- os: macos-15-intel
goos: darwin
goarch: amd64
variant: [standard, ui]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: binaries-${{ matrix.goos }}-${{ matrix.goarch }}
- name: Extract binary
run: |
SUFFIX=${{ matrix.variant == 'ui' && '-ui' || '' }}
tar -xzf codebase-memory-mcp${SUFFIX}-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
chmod +x codebase-memory-mcp
- name: Start artifact server
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
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
env:
SMOKE_DOWNLOAD_URL: http://localhost:18080
- name: Security audits (standard only)
if: matrix.variant == 'standard'
run: |
scripts/security-strings.sh ./codebase-memory-mcp
scripts/security-install.sh ./codebase-memory-mcp
scripts/security-network.sh ./codebase-memory-mcp
- 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
- name: ClamAV scan (Linux)
if: matrix.variant == 'standard' && startsWith(matrix.os, 'ubuntu')
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq clamav > /dev/null 2>&1
sudo sed -i 's/^Example/#Example/' /etc/clamav/freshclam.conf 2>/dev/null || true
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
- name: ClamAV scan (macOS)
if: matrix.variant == 'standard' && startsWith(matrix.os, 'macos')
run: |
brew install clamav > /dev/null 2>&1
CLAMAV_ETC=$(brew --prefix)/etc/clamav
if [ ! -f "$CLAMAV_ETC/freshclam.conf" ]; then
cp "$CLAMAV_ETC/freshclam.conf.sample" "$CLAMAV_ETC/freshclam.conf" 2>/dev/null || true
sed -i '' 's/^Example/#Example/' "$CLAMAV_ETC/freshclam.conf" 2>/dev/null || true
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
smoke-windows:
strategy:
fail-fast: false
matrix:
variant: [standard, ui]
runs-on: windows-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # v2
with:
msystem: CLANG64
path-type: inherit
install: >-
mingw-w64-clang-x86_64-python3
unzip
zip
coreutils
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: binaries-windows-amd64
- name: Extract binary
shell: msys2 {0}
run: |
SUFFIX=${{ matrix.variant == 'ui' && '-ui' || '' }}
unzip -o "codebase-memory-mcp${SUFFIX}-windows-amd64.zip"
[ -n "$SUFFIX" ] && cp "codebase-memory-mcp${SUFFIX}.exe" codebase-memory-mcp.exe || true
- name: Start artifact server
shell: msys2 {0}
run: |
mkdir -p /tmp/smoke-server
cp codebase-memory-mcp.exe /tmp/smoke-server/
SUFFIX=${{ matrix.variant == 'ui' && '-ui' || '' }}
cd /tmp/smoke-server
zip -q "codebase-memory-mcp${SUFFIX}-windows-amd64.zip" codebase-memory-mcp.exe
if [ -n "$SUFFIX" ]; then
cp "codebase-memory-mcp${SUFFIX}-windows-amd64.zip" "codebase-memory-mcp-windows-amd64.zip"
fi
sha256sum *.zip > checksums.txt
python3 -m http.server 18080 -d /tmp/smoke-server &
- name: Smoke test
shell: msys2 {0}
run: scripts/smoke-test.sh ./codebase-memory-mcp.exe
env:
SMOKE_DOWNLOAD_URL: http://localhost:18080
- name: Security audits (standard only)
if: matrix.variant == 'standard'
shell: msys2 {0}
run: |
scripts/security-strings.sh ./codebase-memory-mcp.exe
scripts/security-install.sh ./codebase-memory-mcp.exe
- name: Windows Defender scan (standard only)
if: matrix.variant == 'standard'
shell: pwsh
run: |
& "C:\Program Files\Windows Defender\MpCmdRun.exe" -SignatureUpdate 2>$null
$result = & "C:\Program Files\Windows Defender\MpCmdRun.exe" -Scan -ScanType 3 -File "$PWD\codebase-memory-mcp.exe" -DisableRemediation
Write-Host $result
if ($LASTEXITCODE -ne 0) { Write-Host "BLOCKED: Windows Defender flagged binary!"; exit 1 }
Write-Host "=== Windows Defender: clean ==="
smoke-linux-portable:
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm
variant: [standard, ui]
runs-on: ${{ matrix.runner }}
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: binaries-linux-${{ matrix.arch }}-portable
- name: Extract and smoke test
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
+186
View File
@@ -0,0 +1,186 @@
# Reusable: soak tests (quick + ASan, all platforms)
name: Soak
on:
workflow_call:
inputs:
duration_minutes:
description: 'Soak duration in minutes'
type: number
default: 10
run_asan:
description: 'Run ASan soak in addition to quick soak'
type: boolean
default: false
version:
description: 'Version string for build'
type: string
default: ''
permissions:
contents: read
jobs:
soak-quick:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
goos: linux
goarch: amd64
cc: gcc
cxx: g++
- os: ubuntu-24.04-arm
goos: linux
goarch: arm64
cc: gcc
cxx: g++
- os: macos-14
goos: darwin
goarch: arm64
cc: cc
cxx: c++
- os: macos-15-intel
goos: darwin
goarch: amd64
cc: cc
cxx: c++
runs-on: ${{ matrix.os }}
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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
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 }}
- name: Upload metrics
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: soak-quick-${{ matrix.goos }}-${{ matrix.goarch }}
path: soak-results/
retention-days: 14
soak-quick-windows:
runs-on: windows-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # 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
shell: msys2 {0}
run: scripts/build.sh ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=clang CXX=clang++
- name: Soak (${{ inputs.duration_minutes }} min)
shell: msys2 {0}
run: |
BIN=build/c/codebase-memory-mcp
[ -f "${BIN}.exe" ] && BIN="${BIN}.exe"
scripts/soak-test.sh "$BIN" ${{ inputs.duration_minutes }}
- name: Upload metrics
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: soak-quick-windows-amd64
path: soak-results/
retention-days: 14
soak-asan:
if: ${{ inputs.run_asan }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
goos: linux
goarch: amd64
cc: gcc
cxx: g++
- os: ubuntu-24.04-arm
goos: linux
goarch: arm64
cc: gcc
cxx: g++
- os: macos-14
goos: darwin
goarch: arm64
cc: cc
cxx: c++
- os: macos-15-intel
goos: darwin
goarch: amd64
cc: cc
cxx: c++
runs-on: ${{ matrix.os }}
timeout-minutes: 45
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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 (ASan)
run: |
SANITIZE="-fsanitize=address,undefined -fno-omit-frame-pointer"
scripts/build.sh ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=${{ matrix.cc }} CXX=${{ matrix.cxx }} EXTRA_CFLAGS="$SANITIZE" EXTRA_LDFLAGS="$SANITIZE"
- 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
- name: Upload metrics
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: soak-asan-${{ matrix.goos }}-${{ matrix.goarch }}
path: soak-results/
retention-days: 14
soak-asan-windows:
if: ${{ inputs.run_asan }}
runs-on: windows-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # 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 (ASan)
shell: msys2 {0}
run: |
SANITIZE="-fsanitize=address,undefined -fno-omit-frame-pointer"
scripts/build.sh ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=clang CXX=clang++ EXTRA_CFLAGS="$SANITIZE" EXTRA_LDFLAGS="$SANITIZE"
- name: ASan soak (15 min, no leak detection)
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
- name: Upload metrics
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: soak-asan-windows-amd64
path: soak-results/
retention-days: 14
+67
View File
@@ -0,0 +1,67 @@
# Reusable: unit + integration tests on all platforms
name: Test
on:
workflow_call:
inputs:
skip_perf:
description: 'Skip incremental perf tests (phases 2-7)'
type: boolean
default: true
permissions:
contents: read
jobs:
test-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 }}
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install deps (Ubuntu)
if: startsWith(matrix.os, 'ubuntu')
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
- name: Test
run: scripts/test.sh CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
env:
CBM_SKIP_PERF: ${{ inputs.skip_perf && '1' || '' }}
test-windows:
runs-on: windows-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # v2
with:
msystem: CLANG64
path-type: inherit
install: >-
mingw-w64-clang-x86_64-clang
mingw-w64-clang-x86_64-compiler-rt
mingw-w64-clang-x86_64-zlib
make
- name: Test
shell: msys2 {0}
run: scripts/test.sh CC=clang CXX=clang++
env:
CBM_SKIP_PERF: ${{ inputs.skip_perf && '1' || '' }}
+36 -877
View File
@@ -1,22 +1,24 @@
# Manual trigger: test everything before pushing a release.
# Each step can be skipped for faster iteration.
name: Dry Run
on:
workflow_dispatch:
inputs:
skip_lint:
description: 'Skip lint step (faster iteration)'
description: 'Skip lint + security + CodeQL'
type: boolean
default: false
skip_tests:
description: 'Skip test steps (faster iteration on build/smoke)'
description: 'Skip unit/integration tests'
type: boolean
default: false
skip_builds:
description: 'Skip build+smoke steps (faster iteration on lint/tests)'
description: 'Skip build + smoke'
type: boolean
default: false
soak_level:
description: 'Soak test level: full (quick+asan+nightly), quick (10min only), none'
description: 'Soak: full (quick+asan), quick (10min), none'
type: choice
options: ['full', 'quick', 'none']
default: 'quick'
@@ -25,880 +27,37 @@ permissions:
contents: read
jobs:
# ── Step 1: Lint (clang-format + cppcheck) ───────────────────
# ── Lint + Security ──────────────────────────────────────────
lint:
if: ${{ inputs.skip_lint != true }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: ./.github/workflows/_lint.yml
secrets: inherit
- name: Install build deps
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev cmake
- name: Install LLVM 20
run: |
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-20 main" | sudo tee /etc/apt/sources.list.d/llvm-20.list
sudo apt-get update
sudo apt-get install -y clang-format-20
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
id: cppcheck-cache
with:
path: /opt/cppcheck
key: cppcheck-2.20.0-ubuntu-amd64
- name: Build cppcheck 2.20.0
if: steps.cppcheck-cache.outputs.cache-hit != 'true'
run: |
git clone --depth 1 --branch 2.20.0 https://github.com/danmar/cppcheck.git /tmp/cppcheck
cmake -S /tmp/cppcheck -B /tmp/cppcheck/build -DCMAKE_BUILD_TYPE=Release -DHAVE_RULES=OFF -DCMAKE_INSTALL_PREFIX=/opt/cppcheck
cmake --build /tmp/cppcheck/build -j$(nproc)
cmake --install /tmp/cppcheck/build
- name: Add cppcheck to PATH
run: echo "/opt/cppcheck/bin" >> "$GITHUB_PATH"
- name: Lint
run: scripts/lint.sh CLANG_FORMAT=clang-format-20
# ── Step 1b: Security audit (source-only, runs parallel with lint+tests) ──
# No build needed — scans source files and vendored deps only.
# Binary-level security (L2/L3/L4/L7) runs in smoke jobs per-platform.
security-static:
if: ${{ inputs.skip_lint != true }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: "Layer 1: Static allow-list audit"
run: scripts/security-audit.sh
- name: "Layer 6: UI security audit"
run: scripts/security-ui.sh
- name: "Layer 8: Vendored dependency integrity"
run: scripts/security-vendored.sh
# ── Step 1c: CodeQL SAST gate ────────────────────────────────
codeql-gate:
if: ${{ inputs.skip_lint != true }}
runs-on: ubuntu-latest
steps:
- name: Wait for CodeQL on current commit (max 45 min)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
CURRENT_SHA="${{ github.sha }}"
echo "Current commit: $CURRENT_SHA"
echo "Waiting for CodeQL to complete on this commit..."
for attempt in $(seq 1 90); do
LATEST=$(gh api repos/${{ github.repository }}/actions/workflows/codeql.yml/runs?per_page=5 \
--jq '.workflow_runs[] | select(.head_sha == "'"$CURRENT_SHA"'") | "\(.conclusion) \(.status)"' 2>/dev/null | head -1 || echo "")
if [ -z "$LATEST" ]; then
echo " Attempt $attempt/90: No CodeQL run found for $CURRENT_SHA yet..."
sleep 30
continue
fi
CONCLUSION=$(echo "$LATEST" | cut -d' ' -f1)
STATUS=$(echo "$LATEST" | cut -d' ' -f2)
if [ "$STATUS" = "completed" ] && [ "$CONCLUSION" = "success" ]; then
echo "=== CodeQL completed successfully on current commit ==="
exit 0
elif [ "$STATUS" = "completed" ]; then
echo "BLOCKED: CodeQL completed with conclusion: $CONCLUSION"
exit 1
fi
echo " Attempt $attempt/90: CodeQL status=$STATUS (waiting 30s)..."
sleep 30
done
echo "BLOCKED: CodeQL did not complete within 45 minutes"
exit 1
- name: Check for open code scanning alerts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Wait for GitHub to finish processing alert state changes.
# There is a race between CodeQL marking the workflow as "completed"
# and the alerts API reflecting new/closed alerts from that scan.
echo "Waiting 60s for alert API to settle after CodeQL completion..."
sleep 60
# Poll alerts twice with a gap to confirm the count is stable
ALERTS1=$(gh api 'repos/${{ github.repository }}/code-scanning/alerts?state=open' --jq 'length' 2>/dev/null || echo "0")
echo "Open alerts (check 1): $ALERTS1"
sleep 15
ALERTS2=$(gh api 'repos/${{ github.repository }}/code-scanning/alerts?state=open' --jq 'length' 2>/dev/null || echo "0")
echo "Open alerts (check 2): $ALERTS2"
# Use the higher count (conservative — if either check sees alerts, block)
ALERTS=$ALERTS2
if [ "$ALERTS1" -gt "$ALERTS2" ]; then
ALERTS=$ALERTS1
fi
if [ "$ALERTS" -gt 0 ]; then
echo "BLOCKED: $ALERTS open code scanning alert(s) found."
gh api 'repos/${{ github.repository }}/code-scanning/alerts?state=open' \
--jq '.[] | " #\(.number) [\(.rule.security_severity_level // .rule.severity)] \(.rule.id) — \(.most_recent_instance.location.path):\(.most_recent_instance.location.start_line)"' 2>/dev/null || true
echo "Fix them: https://github.com/${{ github.repository }}/security/code-scanning"
exit 1
fi
echo "=== CodeQL gate passed (0 open alerts) ==="
# ── Step 2: Unit tests (ASan + UBSan) ───────────────────────
# macOS: use cc (Apple Clang) — GCC on macOS doesn't ship ASan runtime
# Linux: use system gcc — full ASan/UBSan support
# Windows: MSYS2 MinGW GCC
test-unix:
if: ${{ inputs.skip_tests != true && always() && (needs.lint.result == 'success' || needs.lint.result == 'skipped') }}
# ── Tests (all platforms, perf tests skipped on CI) ──────────
test:
needs: [lint]
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
arch: amd64
cc: gcc
cxx: g++
- os: ubuntu-24.04-arm
arch: arm64
cc: gcc
cxx: g++
- os: macos-14
arch: arm64
cc: cc
cxx: c++
- os: macos-15-intel
arch: amd64
cc: cc
cxx: c++
runs-on: ${{ matrix.os }}
timeout-minutes: 30
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Install deps (Ubuntu)
if: startsWith(matrix.os, 'ubuntu')
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
- name: Test (unit + integration, no perf)
run: scripts/test.sh CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
env:
CBM_SKIP_PERF: 1
test-windows:
if: ${{ inputs.skip_tests != true && always() && (needs.lint.result == 'success' || needs.lint.result == 'skipped') }}
needs: [lint]
runs-on: windows-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # v2
with:
msystem: CLANG64
path-type: inherit
install: >-
mingw-w64-clang-x86_64-clang
mingw-w64-clang-x86_64-compiler-rt
mingw-w64-clang-x86_64-zlib
make
- name: Test
shell: msys2 {0}
run: scripts/test.sh CC=clang CXX=clang++
# ── Step 3: Build binaries (standard + UI, all OS) ──────────
build-unix:
if: ${{ inputs.skip_builds != true && always() && (needs.test-unix.result == 'success' || needs.test-unix.result == 'skipped') && (needs.test-windows.result == 'success' || needs.test-windows.result == 'skipped') }}
needs: [test-unix, test-windows]
strategy:
matrix:
include:
- os: ubuntu-latest
goos: linux
goarch: amd64
cc: gcc
cxx: g++
- os: ubuntu-24.04-arm
goos: linux
goarch: arm64
cc: gcc
cxx: g++
- os: macos-14
goos: darwin
goarch: arm64
cc: cc
cxx: c++
- os: macos-15-intel
goos: darwin
goarch: amd64
cc: cc
cxx: c++
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Install deps (Ubuntu)
if: startsWith(matrix.os, 'ubuntu')
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: "22"
- name: Build standard binary
run: scripts/build.sh CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
- name: Ad-hoc sign macOS binary
if: startsWith(matrix.os, 'macos')
run: codesign --sign - --force build/c/codebase-memory-mcp
- name: Archive standard binary
run: |
cp LICENSE build/c/
tar -czf codebase-memory-mcp-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz \
-C build/c codebase-memory-mcp LICENSE
- name: Build UI binary
run: scripts/build.sh --with-ui CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
- name: Ad-hoc sign macOS UI binary
if: startsWith(matrix.os, 'macos')
run: codesign --sign - --force build/c/codebase-memory-mcp
- name: Frontend integrity scan (post-build dist/)
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
run: scripts/security-ui.sh
- name: Archive UI binary
run: |
cp LICENSE build/c/
tar -czf codebase-memory-mcp-ui-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz \
-C build/c codebase-memory-mcp LICENSE
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: binaries-${{ matrix.goos }}-${{ matrix.goarch }}
path: "*.tar.gz"
# ── Step 3b: Build portable Linux binaries (musl static) ────
build-linux-portable:
if: ${{ inputs.skip_builds != true && always() && (needs.test-unix.result == 'success' || needs.test-unix.result == 'skipped') && (needs.test-windows.result == 'success' || needs.test-windows.result == 'skipped') }}
needs: [test-unix, test-windows]
strategy:
matrix:
include:
- arch: amd64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
container:
image: alpine:3.21
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install musl build deps
run: apk add --no-cache build-base linux-headers zlib-dev zlib-static nodejs npm
- name: Build standard binary (static)
run: scripts/build.sh CC=gcc CXX=g++ STATIC=1
- name: Verify static linking
run: |
file build/c/codebase-memory-mcp | grep -q "statically linked"
echo "=== Confirmed: fully static binary ==="
- name: Archive standard binary
run: |
cp LICENSE build/c/
tar -czf codebase-memory-mcp-linux-${{ matrix.arch }}-portable.tar.gz \
-C build/c codebase-memory-mcp LICENSE
- name: Build UI binary (static)
run: scripts/build.sh --with-ui CC=gcc CXX=g++ STATIC=1
- name: Archive UI binary
run: |
cp LICENSE build/c/
tar -czf codebase-memory-mcp-ui-linux-${{ matrix.arch }}-portable.tar.gz \
-C build/c codebase-memory-mcp LICENSE
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: binaries-linux-${{ matrix.arch }}-portable
path: "*.tar.gz"
build-windows:
if: ${{ inputs.skip_builds != true && always() && (needs.test-unix.result == 'success' || needs.test-unix.result == 'skipped') && (needs.test-windows.result == 'success' || needs.test-windows.result == 'skipped') }}
needs: [test-unix, test-windows]
runs-on: windows-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # v2
with:
msystem: CLANG64
path-type: inherit
install: >-
mingw-w64-clang-x86_64-clang
mingw-w64-clang-x86_64-zlib
make
zip
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: "22"
- name: Build standard binary
shell: msys2 {0}
run: scripts/build.sh CC=clang CXX=clang++
- name: Archive standard binary
shell: msys2 {0}
run: |
BIN=build/c/codebase-memory-mcp
[ -f "${BIN}.exe" ] && BIN="${BIN}.exe"
cp "$BIN" codebase-memory-mcp.exe
zip codebase-memory-mcp-windows-amd64.zip codebase-memory-mcp.exe LICENSE
- name: Build UI binary
shell: msys2 {0}
run: scripts/build.sh --with-ui CC=clang CXX=clang++
- name: Archive UI binary
shell: msys2 {0}
run: |
BIN=build/c/codebase-memory-mcp
[ -f "${BIN}.exe" ] && BIN="${BIN}.exe"
cp "$BIN" codebase-memory-mcp-ui.exe
zip codebase-memory-mcp-ui-windows-amd64.zip codebase-memory-mcp-ui.exe LICENSE
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: binaries-windows-amd64
path: "*.zip"
# ── Step 4: Smoke test every binary ─────────────────────────
smoke-unix:
if: ${{ !cancelled() && inputs.skip_builds != true }}
needs: [build-unix]
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
goos: linux
goarch: amd64
- os: ubuntu-24.04-arm
goos: linux
goarch: arm64
- os: macos-14
goos: darwin
goarch: arm64
- os: macos-15-intel
goos: darwin
goarch: amd64
variant: [standard, ui]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: binaries-${{ matrix.goos }}-${{ matrix.goarch }}
- name: Extract binary
run: |
SUFFIX=${{ matrix.variant == 'ui' && '-ui' || '' }}
tar -xzf codebase-memory-mcp${SUFFIX}-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
chmod +x codebase-memory-mcp
- name: Start artifact server for E2E smoke tests
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
# Also serve under standard name so install.sh + update --standard work
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
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 (${{ matrix.variant }}, ${{ matrix.goos }}-${{ matrix.goarch }})
run: scripts/smoke-test.sh ./codebase-memory-mcp
env:
SMOKE_DOWNLOAD_URL: http://localhost:18080
- name: Binary string audit (${{ matrix.goos }}-${{ matrix.goarch }})
if: matrix.variant == 'standard'
run: scripts/security-strings.sh ./codebase-memory-mcp
- name: Install output audit (${{ matrix.goos }}-${{ matrix.goarch }})
if: matrix.variant == 'standard'
run: scripts/security-install.sh ./codebase-memory-mcp
- name: Network egress test (${{ matrix.goos }}-${{ matrix.goarch }})
if: matrix.variant == 'standard'
run: scripts/security-network.sh ./codebase-memory-mcp
- name: MCP robustness test
if: matrix.variant == 'standard' && matrix.goos == 'linux' && matrix.goarch == 'amd64'
run: scripts/security-fuzz.sh ./codebase-memory-mcp
- name: Fuzz testing (60s random input)
if: matrix.variant == 'standard' && matrix.goos == 'linux' && matrix.goarch == 'amd64'
run: scripts/security-fuzz-random.sh ./codebase-memory-mcp 60
- name: ClamAV scan (Linux)
if: matrix.variant == 'standard' && startsWith(matrix.os, 'ubuntu')
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq clamav > /dev/null 2>&1
sudo sed -i 's/^Example/#Example/' /etc/clamav/freshclam.conf 2>/dev/null || true
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
echo "=== ClamAV scan ==="
clamscan --no-summary ./codebase-memory-mcp
echo "=== ClamAV: clean ==="
- name: ClamAV scan (macOS)
if: matrix.variant == 'standard' && startsWith(matrix.os, 'macos')
run: |
brew install clamav > /dev/null 2>&1
CLAMAV_ETC=$(brew --prefix)/etc/clamav
if [ ! -f "$CLAMAV_ETC/freshclam.conf" ]; then
cp "$CLAMAV_ETC/freshclam.conf.sample" "$CLAMAV_ETC/freshclam.conf" 2>/dev/null || true
sed -i '' 's/^Example/#Example/' "$CLAMAV_ETC/freshclam.conf" 2>/dev/null || true
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, using bundled signatures"
echo "=== ClamAV scan (macOS) ==="
clamscan --no-summary ./codebase-memory-mcp
echo "=== ClamAV: clean ==="
smoke-windows:
if: ${{ !cancelled() && inputs.skip_builds != true }}
needs: [build-windows]
strategy:
fail-fast: false
matrix:
variant: [standard, ui]
runs-on: windows-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # v2
with:
msystem: CLANG64
path-type: inherit
install: >-
mingw-w64-clang-x86_64-python3
unzip
zip
coreutils
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: binaries-windows-amd64
- name: Extract binary
shell: msys2 {0}
run: |
SUFFIX=${{ matrix.variant == 'ui' && '-ui' || '' }}
unzip -o "codebase-memory-mcp${SUFFIX}-windows-amd64.zip"
[ -n "$SUFFIX" ] && cp "codebase-memory-mcp${SUFFIX}.exe" codebase-memory-mcp.exe || true
- name: Start artifact server for E2E smoke tests
shell: msys2 {0}
run: |
mkdir -p /tmp/smoke-server
cp codebase-memory-mcp.exe /tmp/smoke-server/codebase-memory-mcp.exe
SUFFIX=${{ matrix.variant == 'ui' && '-ui' || '' }}
cd /tmp/smoke-server
zip -q "codebase-memory-mcp${SUFFIX}-windows-amd64.zip" codebase-memory-mcp.exe
# Also serve under standard name
if [ -n "$SUFFIX" ]; then
cp "codebase-memory-mcp${SUFFIX}-windows-amd64.zip" "codebase-memory-mcp-windows-amd64.zip"
fi
sha256sum *.zip > checksums.txt
python3 -m http.server 18080 -d /tmp/smoke-server &
- name: Smoke test (${{ matrix.variant }}, windows-amd64)
shell: msys2 {0}
run: scripts/smoke-test.sh ./codebase-memory-mcp.exe
env:
SMOKE_DOWNLOAD_URL: http://localhost:18080
- name: Binary string audit (windows-amd64)
if: matrix.variant == 'standard'
shell: msys2 {0}
run: scripts/security-strings.sh ./codebase-memory-mcp.exe
- name: Install output audit (windows-amd64)
if: matrix.variant == 'standard'
shell: msys2 {0}
run: scripts/security-install.sh ./codebase-memory-mcp.exe
- name: Windows Defender scan
if: matrix.variant == 'standard'
shell: pwsh
run: |
Write-Host "=== Windows Defender scan (with ML heuristics) ==="
& "C:\Program Files\Windows Defender\MpCmdRun.exe" -SignatureUpdate 2>$null
$result = & "C:\Program Files\Windows Defender\MpCmdRun.exe" -Scan -ScanType 3 -File "$PWD\codebase-memory-mcp.exe" -DisableRemediation
Write-Host $result
if ($LASTEXITCODE -ne 0) {
Write-Host "BLOCKED: Windows Defender flagged the binary!"
exit 1
}
Write-Host "=== Windows Defender: clean ==="
# ── Step 4b: Smoke test portable Linux binaries ──────────────
smoke-linux-portable:
if: ${{ !cancelled() && inputs.skip_builds != true }}
needs: [build-linux-portable]
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
arch: amd64
- os: ubuntu-24.04-arm
arch: arm64
variant: [standard, ui]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: binaries-linux-${{ matrix.arch }}-portable
- name: Extract binary
run: |
SUFFIX=${{ matrix.variant == 'ui' && '-ui' || '' }}
tar -xzf codebase-memory-mcp${SUFFIX}-linux-${{ matrix.arch }}-portable.tar.gz
chmod +x codebase-memory-mcp
- name: Start artifact server for E2E smoke tests
run: |
mkdir -p /tmp/smoke-server
cp codebase-memory-mcp /tmp/smoke-server/
ARCH=${{ matrix.arch }}
SUFFIX=${{ matrix.variant == 'ui' && '-ui' || '' }}
tar -czf "/tmp/smoke-server/codebase-memory-mcp${SUFFIX}-linux-${ARCH}-portable.tar.gz" \
-C /tmp/smoke-server codebase-memory-mcp
# Also serve under standard name so install.sh + update --standard work
cp "/tmp/smoke-server/codebase-memory-mcp${SUFFIX}-linux-${ARCH}-portable.tar.gz" \
"/tmp/smoke-server/codebase-memory-mcp-linux-${ARCH}.tar.gz"
if [ -n "$SUFFIX" ]; then
cp "/tmp/smoke-server/codebase-memory-mcp${SUFFIX}-linux-${ARCH}-portable.tar.gz" \
"/tmp/smoke-server/codebase-memory-mcp${SUFFIX}-linux-${ARCH}.tar.gz"
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 (portable ${{ matrix.variant }}, linux-${{ matrix.arch }})
run: scripts/smoke-test.sh ./codebase-memory-mcp
env:
SMOKE_DOWNLOAD_URL: http://localhost:18080
- name: Binary string audit (portable linux-${{ matrix.arch }})
if: matrix.variant == 'standard'
run: scripts/security-strings.sh ./codebase-memory-mcp
- name: Install output audit (portable linux-${{ matrix.arch }})
if: matrix.variant == 'standard'
run: scripts/security-install.sh ./codebase-memory-mcp
- name: Network egress test (portable linux-${{ matrix.arch }})
if: matrix.variant == 'standard'
run: scripts/security-network.sh ./codebase-memory-mcp
- name: ClamAV scan (portable Linux)
if: matrix.variant == 'standard'
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq clamav > /dev/null 2>&1
sudo sed -i 's/^Example/#Example/' /etc/clamav/freshclam.conf 2>/dev/null || true
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
echo "=== ClamAV scan (portable) ==="
clamscan --no-summary ./codebase-memory-mcp
echo "=== ClamAV: clean ==="
# ── Step 6: Soak tests (after smoke, per-platform) ──────────
soak-quick:
if: ${{ !cancelled() && inputs.soak_level != 'none' && inputs.skip_builds != true }}
needs: [build-unix]
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
goos: linux
goarch: amd64
cc: gcc
cxx: g++
- os: ubuntu-24.04-arm
goos: linux
goarch: arm64
cc: gcc
cxx: g++
- os: macos-14
goos: darwin
goarch: arm64
cc: cc
cxx: c++
- os: macos-15-intel
goos: darwin
goarch: amd64
cc: cc
cxx: c++
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- 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 (release mode)
run: scripts/build.sh CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
- name: Quick soak (10 min)
run: scripts/soak-test.sh build/c/codebase-memory-mcp 10
- name: Upload soak metrics
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: soak-quick-${{ matrix.goos }}-${{ matrix.goarch }}
path: soak-results/
retention-days: 14
soak-quick-windows:
if: ${{ !cancelled() && inputs.soak_level != 'none' && inputs.skip_builds != true }}
needs: [build-windows]
runs-on: windows-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # 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 (release mode)
shell: msys2 {0}
run: scripts/build.sh CC=clang CXX=clang++
- name: Quick soak (10 min)
shell: msys2 {0}
run: |
BIN=build/c/codebase-memory-mcp
[ -f "${BIN}.exe" ] && BIN="${BIN}.exe"
scripts/soak-test.sh "$BIN" 10
- name: Upload soak metrics
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: soak-quick-windows-amd64
path: soak-results/
retention-days: 14
soak-asan:
if: ${{ !cancelled() && inputs.soak_level == 'full' && inputs.skip_builds != true }}
needs: [build-unix]
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
goos: linux
goarch: amd64
cc: gcc
cxx: g++
- os: ubuntu-24.04-arm
goos: linux
goarch: arm64
cc: gcc
cxx: g++
- os: macos-14
goos: darwin
goarch: arm64
cc: cc
cxx: c++
- os: macos-15-intel
goos: darwin
goarch: amd64
cc: cc
cxx: c++
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- 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 (ASan + LeakSanitizer)
run: |
SANITIZE="-fsanitize=address,undefined -fno-omit-frame-pointer"
scripts/build.sh CC=${{ matrix.cc }} CXX=${{ matrix.cxx }} EXTRA_CFLAGS="$SANITIZE" EXTRA_LDFLAGS="$SANITIZE"
- 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
- name: Upload ASan soak metrics
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: soak-asan-${{ matrix.goos }}-${{ matrix.goarch }}
path: soak-results/
retention-days: 14
soak-asan-windows:
if: ${{ !cancelled() && inputs.soak_level == 'full' && inputs.skip_builds != true }}
needs: [build-windows]
runs-on: windows-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # 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 (ASan, no LeakSan on Windows)
shell: msys2 {0}
run: |
SANITIZE="-fsanitize=address,undefined -fno-omit-frame-pointer"
scripts/build.sh CC=clang CXX=clang++ EXTRA_CFLAGS="$SANITIZE" EXTRA_LDFLAGS="$SANITIZE"
- name: ASan soak (15 min, no leak detection)
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
- name: Upload ASan soak metrics
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: soak-asan-windows-amd64
path: soak-results/
retention-days: 14
# ── Step 6c: Quick soak (portable Linux, musl static) ──────────
soak-quick-linux-portable:
if: ${{ !cancelled() && inputs.soak_level != 'none' && inputs.skip_builds != true }}
needs: [build-linux-portable]
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
container:
image: alpine:3.21
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install musl build deps
run: apk add --no-cache build-base linux-headers zlib-dev zlib-static python3 git nodejs npm
- name: Build (release mode, static)
run: scripts/build.sh CC=gcc CXX=g++ STATIC=1
- name: Quick soak (10 min)
run: scripts/soak-test.sh build/c/codebase-memory-mcp 10
- name: Upload soak metrics
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: soak-quick-linux-${{ matrix.arch }}-portable
path: soak-results/
retention-days: 14
# ── Step 6d: ASan soak (portable Linux, musl static) ──────────
soak-asan-linux-portable:
if: ${{ !cancelled() && inputs.soak_level == 'full' && inputs.skip_builds != true }}
needs: [build-linux-portable]
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
container:
image: alpine:3.21
timeout-minutes: 45
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install musl build deps
run: apk add --no-cache build-base linux-headers zlib-dev zlib-static python3 git nodejs npm
- name: Build (ASan + LeakSanitizer, static)
run: |
SANITIZE="-fsanitize=address,undefined -fno-omit-frame-pointer"
scripts/build.sh CC=gcc CXX=g++ STATIC=1 EXTRA_CFLAGS="$SANITIZE" EXTRA_LDFLAGS="$SANITIZE"
- 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
- name: Upload ASan soak metrics
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: soak-asan-linux-${{ matrix.arch }}-portable
path: soak-results/
retention-days: 14
if: ${{ inputs.skip_tests != true && !cancelled() && (needs.lint.result == 'success' || needs.lint.result == 'skipped') }}
uses: ./.github/workflows/_test.yml
with:
skip_perf: true
# ── Build all platforms ──────────────────────────────────────
build:
if: ${{ inputs.skip_builds != true && !cancelled() && (needs.test.result == 'success' || needs.test.result == 'skipped') }}
needs: [test]
uses: ./.github/workflows/_build.yml
# ── Smoke test every binary ─────────────────────────────────
smoke:
if: ${{ inputs.skip_builds != true && !cancelled() && needs.build.result == 'success' }}
needs: [build]
uses: ./.github/workflows/_smoke.yml
# ── Soak tests (optional) ───────────────────────────────────
soak:
if: ${{ inputs.soak_level != 'none' && !cancelled() && (needs.test.result == 'success' || needs.test.result == 'skipped') }}
needs: [test]
uses: ./.github/workflows/_soak.yml
with:
duration_minutes: 10
run_asan: ${{ inputs.soak_level == 'full' }}
+6 -186
View File
@@ -1,3 +1,4 @@
# Weekly soak test: 4h sustained load + ASan leak detection
name: Nightly Soak
on:
@@ -9,194 +10,13 @@ on:
description: 'Soak duration in minutes (default: 240 = 4h)'
type: number
default: 240
platforms:
description: 'Platforms to test'
type: choice
options: ['all', 'linux-only', 'macos-only']
default: 'all'
permissions:
contents: read
jobs:
soak-nightly:
runs-on: ${{ matrix.os }}
timeout-minutes: 330 # 5.5h safety cap (4h soak + build + analysis)
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
goos: linux
goarch: amd64
cc: gcc
cxx: g++
filter: all linux-only
- os: ubuntu-24.04-arm
goos: linux
goarch: arm64
cc: gcc
cxx: g++
filter: all linux-only
- os: macos-14
goos: darwin
goarch: arm64
cc: cc
cxx: c++
filter: all macos-only
steps:
- name: Check platform filter
if: ${{ !contains(matrix.filter, inputs.platforms || 'all') }}
run: |
echo "Skipping ${{ matrix.goos }}-${{ matrix.goarch }} (filter: ${{ inputs.platforms }})"
exit 0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- 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 (release mode)
run: scripts/build.sh CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
- name: Nightly soak (${{ inputs.duration_minutes || 240 }} min)
run: scripts/soak-test.sh build/c/codebase-memory-mcp ${{ inputs.duration_minutes || 240 }}
- name: Upload soak metrics
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: nightly-soak-${{ matrix.goos }}-${{ matrix.goarch }}
path: soak-results/
retention-days: 30
soak-nightly-asan:
runs-on: ${{ matrix.os }}
timeout-minutes: 120 # ASan runs shorter (60 min) due to 2-3x overhead
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
goos: linux
goarch: amd64
cc: gcc
cxx: g++
filter: all linux-only
- os: ubuntu-24.04-arm
goos: linux
goarch: arm64
cc: gcc
cxx: g++
filter: all linux-only
steps:
- name: Check platform filter
if: ${{ !contains(matrix.filter, inputs.platforms || 'all') }}
run: exit 0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Install deps
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 git
- name: Build (ASan + LeakSanitizer)
run: |
SANITIZE="-fsanitize=address,undefined -fno-omit-frame-pointer"
scripts/build.sh CC=${{ matrix.cc }} CXX=${{ matrix.cxx }} EXTRA_CFLAGS="$SANITIZE" EXTRA_LDFLAGS="$SANITIZE"
- name: ASan soak (60 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 60
- name: Upload ASan soak metrics
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: nightly-asan-${{ matrix.goos }}-${{ matrix.goarch }}
path: soak-results/
retention-days: 30
# ── Nightly soak: portable Linux (musl static) ──────────────
soak-nightly-portable:
runs-on: ${{ matrix.runner }}
container:
image: alpine:3.21
timeout-minutes: 330
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest
arch: amd64
filter: all linux-only
- runner: ubuntu-24.04-arm
arch: arm64
filter: all linux-only
steps:
- name: Check platform filter
if: ${{ !contains(matrix.filter, inputs.platforms || 'all') }}
run: exit 0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install musl build deps
run: apk add --no-cache build-base linux-headers zlib-dev zlib-static python3 git nodejs npm
- name: Build (release mode, static)
run: scripts/build.sh CC=gcc CXX=g++ STATIC=1
- name: Nightly soak (${{ inputs.duration_minutes || 240 }} min, portable)
run: scripts/soak-test.sh build/c/codebase-memory-mcp ${{ inputs.duration_minutes || 240 }}
- name: Upload soak metrics
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: nightly-soak-linux-${{ matrix.arch }}-portable
path: soak-results/
retention-days: 30
soak-nightly-asan-portable:
runs-on: ${{ matrix.runner }}
container:
image: alpine:3.21
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest
arch: amd64
filter: all linux-only
- runner: ubuntu-24.04-arm
arch: arm64
filter: all linux-only
steps:
- name: Check platform filter
if: ${{ !contains(matrix.filter, inputs.platforms || 'all') }}
run: exit 0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install musl build deps
run: apk add --no-cache build-base linux-headers zlib-dev zlib-static python3 git nodejs npm
- name: Build (ASan + LeakSanitizer, static)
run: |
SANITIZE="-fsanitize=address,undefined -fno-omit-frame-pointer"
scripts/build.sh CC=gcc CXX=g++ STATIC=1 EXTRA_CFLAGS="$SANITIZE" EXTRA_LDFLAGS="$SANITIZE"
- name: ASan soak (60 min, portable)
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 60
- name: Upload ASan soak metrics
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: nightly-asan-linux-${{ matrix.arch }}-portable
path: soak-results/
retention-days: 30
soak:
uses: ./.github/workflows/_soak.yml
with:
duration_minutes: ${{ inputs.duration_minutes || 240 }}
run_asan: true
File diff suppressed because it is too large Load Diff
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
# Wait for VirusTotal scans to complete and check results.
# Expects: VT_API_KEY, VT_ANALYSIS (comma-separated "file=URL" pairs)
set -euo pipefail
MIN_ENGINES=60
rm -f /tmp/vt_gate_fail
echo "=== Waiting for VirusTotal scans to fully complete ==="
echo "$VT_ANALYSIS" | tr ',' '\n' | while IFS= read -r entry; do
[ -z "$entry" ] && continue
FILE=$(echo "$entry" | cut -d'=' -f1)
URL=$(echo "$entry" | cut -d'=' -f2-)
BASENAME=$(basename "$FILE")
# Extract base64 analysis ID from URL
ANALYSIS_ID=$(echo "$URL" | sed -n 's|.*/file-analysis/\([^/]*\)/.*|\1|p')
if [ -z "$ANALYSIS_ID" ]; then
ANALYSIS_ID=$(echo "$URL" | grep -oE '[a-f0-9]{64}')
if [ -z "$ANALYSIS_ID" ]; then
echo "BLOCKED: Cannot parse VirusTotal URL: $URL"
echo "FAIL" >> /tmp/vt_gate_fail
continue
fi
fi
# Poll until completed (max 120 min)
SCAN_COMPLETE=false
for attempt in $(seq 1 720); do
RESULT=$(curl -sf --max-time 10 \
-H "x-apikey: $VT_API_KEY" \
"https://www.virustotal.com/api/v3/analyses/$ANALYSIS_ID" 2>/dev/null || echo "")
if [ -z "$RESULT" ]; then
echo " $BASENAME: waiting (attempt $attempt)..."
sleep 10
continue
fi
STATS=$(echo "$RESULT" | python3 -c "
import json, sys
d = json.loads(sys.stdin.read())
attrs = d.get('data', {}).get('attributes', {})
status = attrs.get('status', 'queued')
stats = attrs.get('stats', {})
malicious = stats.get('malicious', 0)
suspicious = stats.get('suspicious', 0)
undetected = stats.get('undetected', 0)
harmless = stats.get('harmless', 0)
total = sum(stats.values())
completed = malicious + suspicious + undetected + harmless
print(f'{status},{malicious},{suspicious},{completed},{total}')
" 2>/dev/null || echo "queued,0,0,0,0")
STATUS=$(echo "$STATS" | cut -d',' -f1)
MALICIOUS=$(echo "$STATS" | cut -d',' -f2)
SUSPICIOUS=$(echo "$STATS" | cut -d',' -f3)
COMPLETED=$(echo "$STATS" | cut -d',' -f4)
TOTAL=$(echo "$STATS" | cut -d',' -f5)
if [ "$STATUS" = "completed" ] && [ "$COMPLETED" -ge "$MIN_ENGINES" ]; then
SCAN_COMPLETE=true
if [ "$MALICIOUS" -gt 0 ] || [ "$SUSPICIOUS" -gt 0 ]; then
echo "BLOCKED: $BASENAME flagged ($MALICIOUS malicious, $SUSPICIOUS suspicious / $COMPLETED engines)"
echo " $URL"
echo "FAIL" >> /tmp/vt_gate_fail
else
echo "OK: $BASENAME clean ($COMPLETED engines, 0 detections)"
fi
break
fi
echo " $BASENAME: $COMPLETED/$TOTAL engines ($STATUS, attempt $attempt)..."
sleep 10
done
if [ "$SCAN_COMPLETE" != "true" ]; then
echo "WARNING: $BASENAME scan did not complete in time"
echo "FAIL" >> /tmp/vt_gate_fail
fi
done
if [ -f /tmp/vt_gate_fail ]; then
echo "BLOCKED: One or more VirusTotal checks failed"
exit 1
fi
echo "=== All VirusTotal scans passed ==="