mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-27 06:53:11 +08:00
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:
@@ -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"
|
||||
@@ -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) ==="
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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' }}
|
||||
|
||||
@@ -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
|
||||
|
||||
+45
-961
File diff suppressed because it is too large
Load Diff
Executable
+88
@@ -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 ==="
|
||||
Reference in New Issue
Block a user