Release #68
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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@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@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@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 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@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@v4 | |
| - uses: msys2/setup-msys2@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@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@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: | | |
| tar -czf codebase-memory-mcp-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz \ | |
| -C build/c codebase-memory-mcp | |
| - 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: | | |
| tar -czf codebase-memory-mcp-ui-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz \ | |
| -C build/c codebase-memory-mcp | |
| - uses: actions/upload-artifact@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@v4 | |
| - uses: msys2/setup-msys2@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@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 | |
| - 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 | |
| - uses: actions/upload-artifact@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@v4 | |
| - uses: actions/download-artifact@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 | |
| # Create freshclam config if missing | |
| 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 | |
| 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@v4 | |
| - uses: msys2/setup-msys2@v2 | |
| with: | |
| msystem: CLANG64 | |
| path-type: inherit | |
| install: >- | |
| mingw-w64-clang-x86_64-python3 | |
| unzip | |
| - uses: actions/download-artifact@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] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/download-artifact@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@v2 | |
| with: | |
| subject-path: '*.tar.gz' | |
| - name: Attest build provenance (zip) | |
| uses: actions/attest-build-provenance@v2 | |
| with: | |
| subject-path: '*.zip' | |
| - name: Attest build provenance (checksums) | |
| uses: actions/attest-build-provenance@v2 | |
| with: | |
| subject-path: 'checksums.txt' | |
| # ── SBOM generation ────────────────────────────────────── | |
| - name: Generate SBOM | |
| run: | | |
| cat > sbom.json <<SBOMEOF | |
| {"bomFormat":"CycloneDX","specVersion":"1.4","version":1,"metadata":{"component":{"type":"application","name":"codebase-memory-mcp","version":"${{ inputs.version }}"}},"components":[{"type":"library","name":"sqlite3","version":"3.49.1","description":"Vendored SQLite amalgamation"},{"type":"library","name":"yyjson","version":"0.10.0","description":"Fast JSON parser"},{"type":"library","name":"mongoose","version":"7.16","description":"Embedded HTTP server"},{"type":"library","name":"mimalloc","version":"2.1.7","description":"Memory allocator"},{"type":"library","name":"xxhash","version":"0.8.2","description":"Fast hash function"},{"type":"library","name":"tre","version":"0.8.0","description":"POSIX regex (Windows)"},{"type":"library","name":"tree-sitter","version":"0.24.4","description":"AST parser runtime (64 grammars)"}]} | |
| SBOMEOF | |
| # Trim leading whitespace from heredoc | |
| python3 -c "import json; d=json.load(open('sbom.json')); json.dump(d,open('sbom.json','w'),indent=2)" | |
| - name: Attest SBOM | |
| uses: actions/attest-sbom@v2 | |
| with: | |
| subject-path: '*.tar.gz' | |
| sbom-path: 'sbom.json' | |
| # ── Sigstore cosign signing ────────────────────────────── | |
| - name: Install cosign | |
| uses: sigstore/cosign-installer@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@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 | |
| security-events: write | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false | |
| # ── VirusTotal scan ────────────────────────────────────── | |
| - name: Download draft release binaries | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ inputs.version }} | |
| run: | | |
| mkdir -p assets | |
| gh release download "$VERSION" --dir assets --repo "$GITHUB_REPOSITORY" --pattern '*.tar.gz' --pattern '*.zip' | |
| ls -la assets/ | |
| - name: Scan all binaries with VirusTotal | |
| uses: crazy-max/ghaction-virustotal@v4 | |
| id: virustotal | |
| with: | |
| vt_api_key: ${{ secrets.VIRUS_TOTAL_SCANNER_API_KEY }} | |
| files: | | |
| assets/*.tar.gz | |
| assets/*.zip | |
| # ── Wait for ALL VirusTotal engines to complete, then check ── | |
| # The action outputs comma-separated "file=URL" pairs. | |
| # We wait until every engine has reported (not just the first few), | |
| # then check for any malicious or suspicious detection. | |
| # Minimum 60 engines required — below that we consider the scan incomplete. | |
| - 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 | |
| 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") | |
| SHA256=$(echo "$URL" | grep -oE '[a-f0-9]{64}') | |
| if [ -z "$SHA256" ]; then | |
| echo "WARNING: Could not extract SHA256 from $URL — skipping" | |
| continue | |
| fi | |
| # Poll until ALL engines have completed (max 20 minutes per binary) | |
| 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/files/$SHA256" 2>/dev/null || echo "") | |
| if [ -z "$RESULT" ]; then | |
| echo " $BASENAME: waiting for file to be processed (attempt $attempt)..." | |
| sleep 10 | |
| continue | |
| fi | |
| # Parse all stats in one python call | |
| STATS=$(echo "$RESULT" | python3 -c " | |
| import json, sys | |
| d = json.loads(sys.stdin.read()) | |
| stats = d.get('data', {}).get('attributes', {}).get('last_analysis_stats', {}) | |
| malicious = stats.get('malicious', 0) | |
| suspicious = stats.get('suspicious', 0) | |
| undetected = stats.get('undetected', 0) | |
| harmless = stats.get('harmless', 0) | |
| failure = stats.get('failure', 0) | |
| timeout = stats.get('timeout', 0) | |
| unsupported = stats.get('type-unsupported', 0) | |
| total = sum(stats.values()) | |
| # 'confirmed-timeout' may also exist | |
| completed = malicious + suspicious + undetected + harmless | |
| print(f'{malicious},{suspicious},{completed},{total}') | |
| " 2>/dev/null || echo "0,0,0,0") | |
| MALICIOUS=$(echo "$STATS" | cut -d',' -f1) | |
| SUSPICIOUS=$(echo "$STATS" | cut -d',' -f2) | |
| COMPLETED=$(echo "$STATS" | cut -d',' -f3) | |
| TOTAL=$(echo "$STATS" | cut -d',' -f4) | |
| if [ "$TOTAL" -ge "$MIN_ENGINES" ] && [ "$COMPLETED" -ge "$MIN_ENGINES" ]; 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: $COMPLETED/$TOTAL engines completed (attempt $attempt, need $MIN_ENGINES)..." | |
| 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 ──────────────────────────────────── | |
| - name: Run OpenSSF Scorecard | |
| uses: ossf/scorecard-action@v2 | |
| id: scorecard | |
| with: | |
| results_file: scorecard.sarif | |
| results_format: sarif | |
| publish_results: true | |
| - name: Upload Scorecard SARIF | |
| uses: github/codeql-action/upload-sarif@v3 | |
| with: | |
| sarif_file: scorecard.sarif | |
| - name: Extract Scorecard score | |
| id: score | |
| run: | | |
| SCORE=$(python3 -c " | |
| import json, sys | |
| with open('scorecard.sarif') as f: | |
| d = json.load(f) | |
| props = d.get('runs', [{}])[0].get('tool', {}).get('driver', {}).get('properties', {}) | |
| print(props.get('score', 'N/A')) | |
| " 2>/dev/null || echo "N/A") | |
| echo "score=$SCORE" >> "$GITHUB_OUTPUT" | |
| echo "OpenSSF Scorecard: $SCORE/10" | |
| # ── Append results + publish ───────────────────────────── | |
| - name: Append security verification and publish release | |
| env: | |
| VT_ANALYSIS: ${{ steps.virustotal.outputs.analysis }} | |
| SCORECARD_SCORE: ${{ steps.score.outputs.score }} | |
| 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 | |
| # OpenSSF Scorecard | |
| REPORT+=$'\n**OpenSSF Scorecard** — repository security health: **'"$SCORECARD_SCORE"$'/10**\n' | |
| REPORT+=$'[View detailed scorecard](https://scorecard.dev/viewer/?uri=github.com/DeusData/codebase-memory-mcp)\n\n' | |
| # 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 ===" |