mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Add publish-registries job to release.yml that runs after verify (release published, VirusTotal table appended). Publishes: - pkg/npm via 'npm publish --access public --provenance' - pkg/pypi via 'twine upload' (sdist + wheel from hatchling) Both wrappers fetch the binary at install time, so a version bump is the only per-release change needed. Bumped to 0.6.1. Requires NPM_TOKEN and PYPI_TOKEN repo secrets.
304 lines
12 KiB
YAML
304 lines
12 KiB
YAML
# Release pipeline: lint → test → build → smoke/soak → draft → verify → publish
|
|
#
|
|
# Security (security-static + codeql-gate) starts immediately and runs in
|
|
# parallel with lint/test/build/smoke/soak. It only blocks the final verify
|
|
# step — everything else proceeds independently.
|
|
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
|
|
soak_level:
|
|
description: 'Soak: full (quick+asan), quick (10min), none'
|
|
type: choice
|
|
options: ['full', 'quick', 'none']
|
|
default: 'quick'
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
# ── Security (starts immediately, blocks only verify) ───────────
|
|
security:
|
|
uses: ./.github/workflows/_security.yml
|
|
secrets: inherit
|
|
|
|
# ── 1. Lint (cppcheck + clang-format) ───────────────────────────
|
|
lint:
|
|
uses: ./.github/workflows/_lint.yml
|
|
|
|
# ── 2. Tests (all platforms, full suite for release) ────────────
|
|
test:
|
|
needs: [lint]
|
|
uses: ./.github/workflows/_test.yml
|
|
with:
|
|
skip_perf: false
|
|
|
|
# ── 3. Build all platforms ──────────────────────────────────────
|
|
build:
|
|
needs: [test]
|
|
uses: ./.github/workflows/_build.yml
|
|
with:
|
|
version: ${{ inputs.version }}
|
|
|
|
# ── 4. Smoke test every binary ──────────────────────────────────
|
|
smoke:
|
|
needs: [build]
|
|
uses: ./.github/workflows/_smoke.yml
|
|
|
|
# ── 5. Soak tests ──────────────────────────────────────────────
|
|
soak:
|
|
if: ${{ inputs.soak_level != 'none' }}
|
|
needs: [build]
|
|
uses: ./.github/workflows/_soak.yml
|
|
with:
|
|
duration_minutes: 10
|
|
run_asan: ${{ inputs.soak_level == 'full' }}
|
|
version: ${{ inputs.version }}
|
|
|
|
# ── 6. Create DRAFT release ────────────────────────────────────
|
|
release-draft:
|
|
needs: [smoke, soak]
|
|
if: ${{ !cancelled() && !failure() }}
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
id-token: write
|
|
attestations: write
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
merge-multiple: true
|
|
|
|
- name: List artifacts
|
|
run: ls -la *.tar.gz *.zip
|
|
|
|
- name: Generate checksums
|
|
run: sha256sum *.tar.gz *.zip > checksums.txt
|
|
|
|
- name: Attest build provenance
|
|
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
|
|
with:
|
|
subject-path: '*.tar.gz,*.zip,checksums.txt'
|
|
|
|
- name: Generate SBOM
|
|
run: |
|
|
python3 -c "
|
|
import json
|
|
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@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
|
|
with:
|
|
subject-path: '*.tar.gz'
|
|
sbom-path: 'sbom.json'
|
|
|
|
- name: Install cosign
|
|
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
|
|
|
- name: Sign artifacts
|
|
run: |
|
|
for f in *.tar.gz *.zip checksums.txt; do
|
|
cosign sign-blob --yes --bundle "${f}.bundle" "$f"
|
|
done
|
|
|
|
- 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 == '' }}
|
|
|
|
# ── 7. Verify + Publish (requires security gate) ───────────────
|
|
verify:
|
|
needs: [release-draft, security]
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- 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'
|
|
for f in assets/*.tar.gz; do
|
|
NAME=$(basename "$f" .tar.gz)
|
|
tar -xzf "$f" -C binaries/ 2>/dev/null || true
|
|
[ -f binaries/codebase-memory-mcp ] && mv binaries/codebase-memory-mcp "binaries/${NAME}"
|
|
done
|
|
for f in assets/*.zip; do
|
|
NAME=$(basename "$f" .zip)
|
|
unzip -o "$f" -d binaries/ 2>/dev/null || true
|
|
[ -f binaries/codebase-memory-mcp.exe ] && mv binaries/codebase-memory-mcp.exe "binaries/${NAME}.exe"
|
|
done
|
|
cp install.sh binaries/ 2>/dev/null || true
|
|
cp install.ps1 binaries/ 2>/dev/null || true
|
|
ls -la binaries/
|
|
|
|
- name: Security audits on all release binaries
|
|
run: |
|
|
for bin in binaries/codebase-memory-mcp*; do
|
|
[ -f "$bin" ] || continue
|
|
echo "--- Auditing: $(basename "$bin") ---"
|
|
scripts/security-strings.sh "$bin"
|
|
done
|
|
|
|
- name: VirusTotal scan
|
|
uses: crazy-max/ghaction-virustotal@936d8c5c00afe97d3d9a1af26d017cfdf26800a2 # v5.0.0
|
|
id: virustotal
|
|
with:
|
|
vt_api_key: ${{ secrets.VIRUS_TOTAL_SCANNER_API_KEY }}
|
|
files: binaries/*
|
|
|
|
- name: Wait for VirusTotal results
|
|
env:
|
|
VT_API_KEY: ${{ secrets.VIRUS_TOTAL_SCANNER_API_KEY }}
|
|
VT_ANALYSIS: ${{ steps.virustotal.outputs.analysis }}
|
|
run: scripts/ci/check-virustotal.sh
|
|
|
|
- name: Append VirusTotal scan links to release notes
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
VERSION: ${{ inputs.version }}
|
|
run: |
|
|
# Hash the extracted binaries (not the archives — VT indexes binary hashes)
|
|
TABLE="\n\n## Security Verification\n\n"
|
|
TABLE+="All release binaries scanned with 70+ antivirus engines — **0 detections**.\n\n"
|
|
TABLE+="| Binary | SHA-256 | VirusTotal |\n"
|
|
TABLE+="|--------|---------|------------|\n"
|
|
|
|
for bin in binaries/codebase-memory-mcp-*; do
|
|
[ -f "$bin" ] || continue
|
|
name=$(basename "$bin")
|
|
# Skip UI variants and non-binary files
|
|
echo "$name" | grep -qE \
|
|
'^codebase-memory-mcp-(linux|darwin|windows)-(amd64|arm64)(\.exe)?$' || continue
|
|
sha256=$(sha256sum "$bin" 2>/dev/null | awk '{print $1}' \
|
|
|| shasum -a 256 "$bin" | awk '{print $1}')
|
|
label=$(echo "$name" \
|
|
| sed 's/^codebase-memory-mcp-//' \
|
|
| sed 's/\.exe$//')
|
|
short="${sha256:0:20}..."
|
|
vt_url="https://www.virustotal.com/gui/file/${sha256}/detection"
|
|
TABLE+="| \`${label}\` | \`${short}\` | [0/72 ✅](${vt_url}) |\n"
|
|
done
|
|
|
|
CURRENT=$(gh release view "$VERSION" \
|
|
--json body --jq '.body // ""' --repo "$GITHUB_REPOSITORY")
|
|
printf '%s%b' "$CURRENT" "$TABLE" > /tmp/release_notes.md
|
|
gh release edit "$VERSION" \
|
|
--notes-file /tmp/release_notes.md --repo "$GITHUB_REPOSITORY"
|
|
|
|
- name: Publish release
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
VERSION: ${{ inputs.version }}
|
|
run: gh release edit "$VERSION" --draft=false --repo "$GITHUB_REPOSITORY"
|
|
|
|
# ── 8. Publish package wrappers (npm + PyPI) ──────────────────
|
|
# Wrappers in pkg/npm and pkg/pypi download the released binary at
|
|
# install time, so they only need a version bump (already in the repo
|
|
# at this point) — no per-release sha256 substitution.
|
|
publish-registries:
|
|
needs: [verify]
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
id-token: write # for npm provenance
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
# ── npm ──────────────────────────────────────────────────
|
|
- name: Setup Node
|
|
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v6.4.0
|
|
with:
|
|
node-version: '22'
|
|
registry-url: 'https://registry.npmjs.org'
|
|
|
|
- name: Publish to npm
|
|
working-directory: pkg/npm
|
|
env:
|
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
run: npm publish --access public --provenance
|
|
|
|
# ── PyPI ─────────────────────────────────────────────────
|
|
- name: Setup Python
|
|
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
with:
|
|
python-version: '3.12'
|
|
|
|
- name: Build PyPI distribution
|
|
working-directory: pkg/pypi
|
|
run: |
|
|
python -m pip install --upgrade build twine
|
|
python -m build
|
|
|
|
- name: Publish to PyPI
|
|
working-directory: pkg/pypi
|
|
env:
|
|
TWINE_USERNAME: __token__
|
|
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
|
|
run: twine upload --non-interactive dist/*
|