Release #65
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 | |
| - 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 VirusTotal results and check for detections ── | |
| # The action outputs comma-separated "file=URL" pairs. | |
| # URLs are https://www.virustotal.com/gui/file/<sha256> permalinks. | |
| # We extract the SHA256, query the API for scan stats, and gate on detections. | |
| - name: Check VirusTotal scan results | |
| env: | |
| VT_API_KEY: ${{ secrets.VIRUS_TOTAL_SCANNER_API_KEY }} | |
| VT_ANALYSIS: ${{ steps.virustotal.outputs.analysis }} | |
| run: | | |
| echo "=== Checking VirusTotal scan results ===" | |
| DETECTIONS=0 | |
| # Split comma-separated output into newlines | |
| 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 SHA256 hash from URL (last path segment of /gui/file/<hash>) | |
| 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 the file report until last_analysis_results are available | |
| for attempt in $(seq 1 30); 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 scan (attempt $attempt/30)..." | |
| sleep 10 | |
| continue | |
| fi | |
| MALICIOUS=$(echo "$RESULT" | python3 -c " | |
| import json, sys | |
| d = json.loads(sys.stdin.read()) | |
| stats = d.get('data', {}).get('attributes', {}).get('last_analysis_stats', {}) | |
| print(stats.get('malicious', 0)) | |
| " 2>/dev/null || echo "0") | |
| SUSPICIOUS=$(echo "$RESULT" | python3 -c " | |
| import json, sys | |
| d = json.loads(sys.stdin.read()) | |
| stats = d.get('data', {}).get('attributes', {}).get('last_analysis_stats', {}) | |
| print(stats.get('suspicious', 0)) | |
| " 2>/dev/null || echo "0") | |
| TOTAL=$(echo "$RESULT" | python3 -c " | |
| import json, sys | |
| d = json.loads(sys.stdin.read()) | |
| stats = d.get('data', {}).get('attributes', {}).get('last_analysis_stats', {}) | |
| print(sum(stats.values())) | |
| " 2>/dev/null || echo "0") | |
| if [ "$TOTAL" -gt 0 ]; then | |
| echo "$BASENAME: $MALICIOUS malicious, $SUSPICIOUS suspicious (of $TOTAL engines)" | |
| if [ "$MALICIOUS" -gt 0 ] || [ "$SUSPICIOUS" -gt 0 ]; then | |
| echo "BLOCKED: $BASENAME flagged! See $URL" | |
| echo "FAIL" >> /tmp/vt_gate_fail | |
| fi | |
| break | |
| fi | |
| echo " $BASENAME: waiting for scan results (attempt $attempt/30)..." | |
| sleep 10 | |
| done | |
| 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 as malicious or suspicious." | |
| echo "Draft release will NOT be published. Investigate before retrying." | |
| exit 1 | |
| fi | |
| echo "=== All binaries clean ===" | |
| # ── 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** — every binary scanned during CI build:\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 ===" |