mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 18:58:00 +00:00
740 lines
29 KiB
YAML
740 lines
29 KiB
YAML
name: Release
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
version:
|
|
description: "Release version (e.g. v0.8.0)"
|
|
required: true
|
|
type: string
|
|
release_notes:
|
|
description: "Release notes (optional — auto-generated from commits if empty)"
|
|
required: false
|
|
type: string
|
|
replace:
|
|
description: "Replace existing release if it exists"
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
|
|
permissions:
|
|
contents: write
|
|
id-token: write
|
|
attestations: write
|
|
|
|
jobs:
|
|
# ── Step 1: Lint (clang-format + cppcheck) ───────────────────
|
|
lint:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
|
|
- 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:
|
|
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 (checks for open alerts) ──────
|
|
codeql-gate:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Check for open code scanning alerts
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
ALERTS=$(gh api repos/${{ github.repository }}/code-scanning/alerts?state=open --jq 'length' 2>/dev/null || echo "0")
|
|
echo "Open code scanning alerts: $ALERTS"
|
|
if [ "$ALERTS" -gt 0 ]; then
|
|
echo "BLOCKED: $ALERTS open code scanning alert(s) found."
|
|
echo "Fix them before releasing: 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:
|
|
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 }}
|
|
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
|
|
run: scripts/test.sh CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
|
|
|
|
test-windows:
|
|
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:
|
|
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@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
with:
|
|
node-version: "22"
|
|
|
|
- name: Build standard binary
|
|
run: scripts/build.sh --version ${{ inputs.version }} CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
|
|
|
|
- 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 --version ${{ inputs.version }} CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
|
|
|
|
- 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"
|
|
|
|
build-windows:
|
|
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@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
with:
|
|
node-version: "22"
|
|
|
|
- name: Build standard binary
|
|
shell: msys2 {0}
|
|
run: scripts/build.sh --version ${{ 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
|
|
|
|
- name: Build UI binary
|
|
shell: msys2 {0}
|
|
run: scripts/build.sh --with-ui --version ${{ 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-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:
|
|
needs: [build-unix]
|
|
strategy:
|
|
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: Smoke test (${{ matrix.variant }}, ${{ matrix.goos }}-${{ matrix.goarch }})
|
|
run: scripts/smoke-test.sh ./codebase-memory-mcp
|
|
|
|
- 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
|
|
|
|
# Native platform antivirus scan
|
|
- 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
|
|
# Ensure freshclam config has DatabaseMirror set
|
|
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
|
|
# Download signatures (--no-warnings suppresses X509 store errors on macOS)
|
|
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:
|
|
needs: [build-windows]
|
|
strategy:
|
|
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
|
|
|
|
- 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: Smoke test (${{ matrix.variant }}, windows-amd64)
|
|
shell: msys2 {0}
|
|
run: scripts/smoke-test.sh ./codebase-memory-mcp.exe
|
|
|
|
- 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
|
|
|
|
# Windows Defender scan (includes ML heuristics — catches what VirusTotal misses)
|
|
- name: Windows Defender scan
|
|
if: matrix.variant == 'standard'
|
|
shell: pwsh
|
|
run: |
|
|
Write-Host "=== Windows Defender scan (with ML heuristics) ==="
|
|
# Update definitions first
|
|
& "C:\Program Files\Windows Defender\MpCmdRun.exe" -SignatureUpdate 2>$null
|
|
# Full scan of the binary
|
|
$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!"
|
|
Write-Host "Exit code: $LASTEXITCODE"
|
|
exit 1
|
|
}
|
|
Write-Host "=== Windows Defender: clean ==="
|
|
|
|
# ── Step 5: Create DRAFT release (not public yet) ─────────────
|
|
release-draft:
|
|
needs: [smoke-unix, smoke-windows, security-static, codeql-gate]
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
id-token: write
|
|
attestations: write
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
|
|
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
|
with:
|
|
merge-multiple: true
|
|
|
|
- name: List artifacts
|
|
run: ls -la *.tar.gz *.zip
|
|
|
|
- name: Generate checksums
|
|
run: sha256sum *.tar.gz *.zip > checksums.txt
|
|
|
|
# ── Artifact attestations (SLSA provenance) ──────────────
|
|
- name: Attest build provenance (tar.gz)
|
|
uses: actions/attest-build-provenance@96b4a1ef7235a096b17240c259729fdd70c83d45 # v2
|
|
with:
|
|
subject-path: '*.tar.gz'
|
|
|
|
- name: Attest build provenance (zip)
|
|
uses: actions/attest-build-provenance@96b4a1ef7235a096b17240c259729fdd70c83d45 # v2
|
|
with:
|
|
subject-path: '*.zip'
|
|
|
|
- name: Attest build provenance (checksums)
|
|
uses: actions/attest-build-provenance@96b4a1ef7235a096b17240c259729fdd70c83d45 # v2
|
|
with:
|
|
subject-path: 'checksums.txt'
|
|
|
|
# ── SBOM generation (SPDX format) ──────────────────────────
|
|
- name: Generate SBOM
|
|
run: |
|
|
python3 -c "
|
|
import json, uuid
|
|
sbom = {
|
|
'spdxVersion': 'SPDX-2.3',
|
|
'dataLicense': 'CC0-1.0',
|
|
'SPDXID': 'SPDXRef-DOCUMENT',
|
|
'name': 'codebase-memory-mcp-${{ inputs.version }}',
|
|
'documentNamespace': 'https://github.com/DeusData/codebase-memory-mcp/releases/${{ inputs.version }}',
|
|
'creationInfo': {
|
|
'created': '$(date -u +%Y-%m-%dT%H:%M:%SZ)',
|
|
'creators': ['Tool: codebase-memory-mcp-release-pipeline']
|
|
},
|
|
'packages': [
|
|
{'SPDXID': 'SPDXRef-Package-sqlite3', 'name': 'sqlite3', 'versionInfo': '3.49.1', 'downloadLocation': 'https://sqlite.org', 'filesAnalyzed': False},
|
|
{'SPDXID': 'SPDXRef-Package-yyjson', 'name': 'yyjson', 'versionInfo': '0.10.0', 'downloadLocation': 'https://github.com/ibireme/yyjson', 'filesAnalyzed': False},
|
|
{'SPDXID': 'SPDXRef-Package-mongoose', 'name': 'mongoose', 'versionInfo': '7.16', 'downloadLocation': 'https://github.com/cesanta/mongoose', 'filesAnalyzed': False},
|
|
{'SPDXID': 'SPDXRef-Package-mimalloc', 'name': 'mimalloc', 'versionInfo': '2.1.7', 'downloadLocation': 'https://github.com/microsoft/mimalloc', 'filesAnalyzed': False},
|
|
{'SPDXID': 'SPDXRef-Package-xxhash', 'name': 'xxhash', 'versionInfo': '0.8.2', 'downloadLocation': 'https://github.com/Cyan4973/xxHash', 'filesAnalyzed': False},
|
|
{'SPDXID': 'SPDXRef-Package-tre', 'name': 'tre', 'versionInfo': '0.8.0', 'downloadLocation': 'https://github.com/laurikari/tre', 'filesAnalyzed': False},
|
|
{'SPDXID': 'SPDXRef-Package-tree-sitter', 'name': 'tree-sitter', 'versionInfo': '0.24.4', 'downloadLocation': 'https://github.com/tree-sitter/tree-sitter', 'filesAnalyzed': False}
|
|
]
|
|
}
|
|
json.dump(sbom, open('sbom.json', 'w'), indent=2)
|
|
"
|
|
|
|
- name: Attest SBOM
|
|
uses: actions/attest-sbom@10926c72720ffc3f7b666661c8e55b1344e2a365 # v2
|
|
with:
|
|
subject-path: '*.tar.gz'
|
|
sbom-path: 'sbom.json'
|
|
|
|
# ── Sigstore cosign signing ──────────────────────────────
|
|
- name: Install cosign
|
|
uses: sigstore/cosign-installer@f713795cb21599bc4e5c4b58cbad1da852d7eeb9 # v3
|
|
|
|
- name: Sign release artifacts with cosign
|
|
run: |
|
|
for f in *.tar.gz *.zip checksums.txt; do
|
|
cosign sign-blob --yes --bundle "${f}.bundle" "$f"
|
|
done
|
|
|
|
# ── Create DRAFT release (not visible to users yet) ──────
|
|
- name: Delete existing release
|
|
if: ${{ inputs.replace }}
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
VERSION: ${{ inputs.version }}
|
|
run: gh release delete "$VERSION" --yes --cleanup-tag || true
|
|
|
|
- name: Create tag
|
|
env:
|
|
VERSION: ${{ inputs.version }}
|
|
run: |
|
|
git tag -f "$VERSION"
|
|
git push origin "$VERSION" --force
|
|
|
|
- uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
|
|
with:
|
|
tag_name: ${{ inputs.version }}
|
|
draft: true
|
|
files: |
|
|
*.tar.gz
|
|
*.zip
|
|
checksums.txt
|
|
sbom.json
|
|
*.bundle
|
|
body: ${{ inputs.release_notes || '' }}
|
|
generate_release_notes: ${{ inputs.release_notes == '' }}
|
|
|
|
# ── Step 6: Verify draft release ─────────────────────────────
|
|
# Scans binaries with VirusTotal, runs OpenSSF Scorecard.
|
|
# If verification passes, appends results and publishes.
|
|
# If it fails, the draft stays unpublished.
|
|
verify:
|
|
needs: [release-draft]
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
with:
|
|
persist-credentials: false
|
|
|
|
# ── VirusTotal scan ──────────────────────────────────────
|
|
# Extract raw binaries from archives before scanning.
|
|
# VirusTotal may not unpack archives >3MB, so we scan the
|
|
# actual executables that users will run.
|
|
- name: Download and extract release binaries
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
VERSION: ${{ inputs.version }}
|
|
run: |
|
|
mkdir -p assets binaries
|
|
gh release download "$VERSION" --dir assets --repo "$GITHUB_REPOSITORY" --pattern '*.tar.gz' --pattern '*.zip'
|
|
ls -la assets/
|
|
|
|
# Extract binaries from archives for scanning
|
|
for f in assets/*.tar.gz; do
|
|
NAME=$(basename "$f" .tar.gz)
|
|
tar -xzf "$f" -C binaries/ 2>/dev/null || true
|
|
# Rename to include platform for identification
|
|
if [ -f binaries/codebase-memory-mcp ]; then
|
|
mv binaries/codebase-memory-mcp "binaries/${NAME}"
|
|
fi
|
|
done
|
|
for f in assets/*.zip; do
|
|
NAME=$(basename "$f" .zip)
|
|
unzip -o "$f" -d binaries/ 2>/dev/null || true
|
|
if [ -f binaries/codebase-memory-mcp.exe ]; then
|
|
mv binaries/codebase-memory-mcp.exe "binaries/${NAME}.exe"
|
|
fi
|
|
done
|
|
echo "=== Extracted binaries for scanning ==="
|
|
ls -la binaries/
|
|
|
|
- name: Scan extracted binaries with VirusTotal
|
|
uses: crazy-max/ghaction-virustotal@d34968c958ae283fe976efed637081b9f9dcf74f # v4
|
|
id: virustotal
|
|
with:
|
|
vt_api_key: ${{ secrets.VIRUS_TOTAL_SCANNER_API_KEY }}
|
|
files: |
|
|
binaries/*
|
|
|
|
# ── Wait for ALL VirusTotal engines to complete, then check ──
|
|
# The action outputs comma-separated "file=URL" pairs.
|
|
# URLs are /gui/file-analysis/<base64_id>/detection — we extract the
|
|
# base64 analysis ID and poll /api/v3/analyses/<id> until completed.
|
|
- name: Check VirusTotal scan results (wait for 100% completion)
|
|
env:
|
|
VT_API_KEY: ${{ secrets.VIRUS_TOTAL_SCANNER_API_KEY }}
|
|
VT_ANALYSIS: ${{ steps.virustotal.outputs.analysis }}
|
|
run: |
|
|
echo "=== Waiting for VirusTotal scans to fully complete ==="
|
|
MIN_ENGINES=60
|
|
rm -f /tmp/vt_gate_fail
|
|
|
|
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: /gui/file-analysis/<ID>/detection
|
|
ANALYSIS_ID=$(echo "$URL" | sed -n 's|.*/file-analysis/\([^/]*\)/.*|\1|p')
|
|
if [ -z "$ANALYSIS_ID" ]; then
|
|
echo "WARNING: Could not extract analysis ID from $URL"
|
|
# Try SHA256 fallback (older action versions use /gui/file/<sha256>)
|
|
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 /api/v3/analyses/<id> until status=completed (max 20 min)
|
|
SCAN_COMPLETE=false
|
|
for attempt in $(seq 1 120); 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" ]; then
|
|
echo "$BASENAME: $MALICIOUS malicious, $SUSPICIOUS suspicious ($COMPLETED completed, $TOTAL total engines)"
|
|
|
|
if [ "$MALICIOUS" -gt 0 ] || [ "$SUSPICIOUS" -gt 0 ]; then
|
|
echo "BLOCKED: $BASENAME flagged! See $URL"
|
|
echo "FAIL" >> /tmp/vt_gate_fail
|
|
fi
|
|
SCAN_COMPLETE=true
|
|
break
|
|
fi
|
|
|
|
echo " $BASENAME: $STATUS (attempt $attempt)..."
|
|
sleep 10
|
|
done
|
|
|
|
if [ "$SCAN_COMPLETE" != "true" ]; then
|
|
echo "BLOCKED: $BASENAME scan did not complete within 20 minutes!"
|
|
echo "FAIL" >> /tmp/vt_gate_fail
|
|
fi
|
|
done
|
|
|
|
if [ -f /tmp/vt_gate_fail ]; then
|
|
FAIL_COUNT=$(wc -l < /tmp/vt_gate_fail | tr -d ' ')
|
|
echo ""
|
|
echo "=== VIRUSTOTAL GATE FAILED ==="
|
|
echo "$FAIL_COUNT binary(ies) flagged or scan incomplete."
|
|
echo "Draft release will NOT be published. Investigate before retrying."
|
|
exit 1
|
|
fi
|
|
|
|
echo "=== All binaries clean (all engines completed) ==="
|
|
|
|
# ── OpenSSF Scorecard gate ──────────────────────────────────
|
|
# Fetch public score and block release if repo health degrades below threshold.
|
|
- name: OpenSSF Scorecard gate (minimum 4.0)
|
|
run: |
|
|
SCORE=$(curl -sf "https://api.scorecard.dev/projects/github.com/DeusData/codebase-memory-mcp" 2>/dev/null \
|
|
| python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('score',0))" 2>/dev/null \
|
|
|| echo "0")
|
|
echo "OpenSSF Scorecard: $SCORE/10"
|
|
if python3 -c "exit(0 if float('$SCORE') >= 4.0 else 1)" 2>/dev/null; then
|
|
echo "=== Scorecard gate passed (>= 4.0) ==="
|
|
else
|
|
echo "BLOCKED: Scorecard $SCORE/10 is below minimum 4.0"
|
|
echo "Check https://scorecard.dev/viewer/?uri=github.com/DeusData/codebase-memory-mcp"
|
|
exit 1
|
|
fi
|
|
|
|
# ── Append results + publish ─────────────────────────────
|
|
- name: Append security verification and publish release
|
|
env:
|
|
VT_ANALYSIS: ${{ steps.virustotal.outputs.analysis }}
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
VERSION: ${{ inputs.version }}
|
|
run: |
|
|
echo "=== Building security verification report ==="
|
|
|
|
REPORT=$'---\n\n### Security Verification\n\n'
|
|
REPORT+=$'All release binaries have been independently verified:\n\n'
|
|
|
|
# VirusTotal results (comma-separated "file=URL" pairs)
|
|
REPORT+=$'**VirusTotal** — scanned by 70+ antivirus engines:\n\n'
|
|
REPORT+=$'| Binary | Scan |\n|--------|------|\n'
|
|
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")
|
|
echo "| $BASENAME | [View Report]($URL) |"
|
|
done >> /tmp/vt_table
|
|
if [ -f /tmp/vt_table ]; then
|
|
REPORT+=$(cat /tmp/vt_table)$'\n'
|
|
rm -f /tmp/vt_table
|
|
fi
|
|
|
|
# Build provenance
|
|
REPORT+=$'**Build Provenance (SLSA)** — cryptographic proof each binary was built by GitHub Actions from this repo:\n'
|
|
REPORT+=$'```\ngh attestation verify <downloaded-file> --repo DeusData/codebase-memory-mcp\n```\n\n'
|
|
|
|
# Cosign
|
|
REPORT+=$'**Sigstore cosign** — keyless signature verification:\n'
|
|
REPORT+=$'```\ncosign verify-blob --bundle <file>.bundle <file>\n```\n\n'
|
|
|
|
# Native AV scans
|
|
REPORT+=$'**Native antivirus scans** — all binaries passed these scans before this release was created (any detection would have blocked the release):\n'
|
|
REPORT+=$'- Windows: Windows Defender with ML heuristics (the same engine end users run)\n'
|
|
REPORT+=$'- Linux: ClamAV with daily signature updates\n'
|
|
REPORT+=$'- macOS: ClamAV with daily signature updates\n\n'
|
|
|
|
# SBOM
|
|
REPORT+=$'**SBOM** — Software Bill of Materials (`sbom.json`) lists all vendored dependencies.\n\n'
|
|
|
|
REPORT+=$'See [SECURITY.md](https://github.com/DeusData/codebase-memory-mcp/blob/main/SECURITY.md) for full details.\n'
|
|
|
|
# Append to release notes
|
|
EXISTING=$(gh release view "$VERSION" --json body --jq '.body' --repo "$GITHUB_REPOSITORY")
|
|
printf '%s\n\n%s\n' "$EXISTING" "$REPORT" | gh release edit "$VERSION" --notes-file - --repo "$GITHUB_REPOSITORY"
|
|
|
|
# ── Publish: promote draft to public release ─────────
|
|
gh release edit "$VERSION" --draft=false --repo "$GITHUB_REPOSITORY"
|
|
|
|
echo "=== Release verified and published ==="
|