mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-06 15:49:59 +00:00
The pre-release ML false-positive tolerance (single-engine Microsoft "!ml" verdicts downgradable with Defender endpoint evidence, #1340) is reverted by owner decision: cbm does not ship binaries carrying a VirusTotal detection, demonstrably false or not. A "trojan" badge on a release asset is a reputation cost the project is not willing to price in, however good the accompanying evidence. The gate returns to its original form: any detection, by any engine, on any artifact, on any version blocks the release. The endpoint verification tool and the evidence side-channel are removed with it; the notes renderer keeps its extracted-script form but only ever states a verified "0 detections". False positives are resolved upstream instead: verify the bytes on a real Defender endpoint, submit a Microsoft false-positive report for the exact hashes, wait for the detection to clear, then RE-RUN the failed verify job -- which does not rebuild, so the cleared hashes are the shipped hashes. tests/test_vt_gate_zero_tolerance_contract.sh pins the decision: clean passes; 1 malicious and 1 suspicious each block across stable, -rc., -pre and -alpha versions; plus a tripwire for the specific reverted evidence mechanism returning. Loosening this gate again has to consciously delete that contract. The skip_tests dispatch input and the script-extracted notes step survive the revert -- both are orthogonal to gating policy. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
394 lines
16 KiB
YAML
394 lines
16 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'
|
|
skip_perf:
|
|
description: "Skip performance tests (use when no pipeline logic changed)"
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
skip_tests:
|
|
description: "Skip the test phase entirely (re-release of an already test-green tree; build/smoke/soak/verify still run)"
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
|
|
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) ────────────
|
|
# skip_tests exists for RE-releases of a tree whose tests are already green:
|
|
# when only packaging/gating changed and the previous run proved the suites,
|
|
# re-running ~2h of tests adds no information. It skips ONLY this phase —
|
|
# build, smoke, soak and verify always run against the fresh artifacts.
|
|
test:
|
|
if: ${{ !inputs.skip_tests }}
|
|
needs: [lint]
|
|
uses: ./.github/workflows/_test.yml
|
|
with:
|
|
skip_perf: ${{ inputs.skip_perf }}
|
|
broad_platforms: true
|
|
shard_suites: true
|
|
|
|
# ── 3. Build all platforms ──────────────────────────────────────
|
|
# !cancelled() && !failure(): run when `test` is deliberately skipped, but
|
|
# never when lint or test actually failed.
|
|
build:
|
|
if: ${{ !cancelled() && !failure() }}
|
|
needs: [test]
|
|
permissions:
|
|
contents: read
|
|
id-token: write
|
|
attestations: write
|
|
uses: ./.github/workflows/_build.yml
|
|
with:
|
|
version: ${{ inputs.version }}
|
|
attest: true
|
|
|
|
# ── 4. Smoke test every binary ──────────────────────────────────
|
|
smoke:
|
|
needs: [build]
|
|
uses: ./.github/workflows/_smoke.yml
|
|
with:
|
|
broad_platforms: true
|
|
|
|
# ── 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
|
|
- 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 checksum provenance
|
|
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
|
|
with:
|
|
subject-path: checksums.txt
|
|
|
|
# SBOM content lives in the canonical scripts/ci entry, not inline YAML
|
|
# (venue-parity contract): vendored versions are reviewable there.
|
|
- name: Generate SBOM
|
|
run: python3 scripts/ci/generate-sbom.py "${{ inputs.version }}"
|
|
|
|
- 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@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
|
|
|
- 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 }}
|
|
# Tag the DISPATCHED sha, not the checkout HEAD: the artifacts were
|
|
# built from github.sha, and a commit pushed to main mid-run must not
|
|
# move the release tag. (A floating HEAD also broke the 0.8.0 release:
|
|
# HEAD had become a commit touching .github/workflows/, and the App
|
|
# token may not create refs pointing at workflow-modifying commits.)
|
|
run: |
|
|
git tag -f "$VERSION" "$GITHUB_SHA"
|
|
git push origin "$VERSION" --force
|
|
|
|
- uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v2
|
|
with:
|
|
tag_name: ${{ inputs.version }}
|
|
draft: true
|
|
# Semver prerelease (v0.9.1-rc.1): never the "latest" release on GitHub.
|
|
prerelease: ${{ contains(inputs.version, '-') }}
|
|
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]
|
|
# Explicit condition: a skipped ancestor (soak with soak_level=none) must
|
|
# not skip the publish chain — only real failures/cancellations block it.
|
|
if: ${{ !cancelled() && !failure() && needs.release-draft.result == 'success' }}
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
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 files
|
|
run: |
|
|
# Audit every file that ships in the release archives — binaries,
|
|
# install scripts, LICENSE, and any future companion files.
|
|
# security-strings.sh detects file type and applies binary-only
|
|
# rules (URL allowlist, dangerous-command detection) only to real
|
|
# binaries; for shell scripts it still runs credential and base64
|
|
# pattern audits.
|
|
for f in binaries/*; do
|
|
[ -f "$f" ] || continue
|
|
echo "--- Auditing: $(basename "$f") ---"
|
|
scripts/security-strings.sh "$f"
|
|
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/*
|
|
|
|
# ZERO tolerance: any detection on any artifact blocks the release.
|
|
# False positives are resolved upstream (Microsoft false-positive
|
|
# submission for the exact hashes, then re-run this job — no rebuild),
|
|
# never by loosening this gate.
|
|
- 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
|
|
|
|
# Renders per-binary sha256 + VT links into the notes; only reachable
|
|
# after the zero-tolerance gate passed, so "0 detections" is verified.
|
|
- name: Append VirusTotal scan links to release notes
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
VERSION: ${{ inputs.version }}
|
|
run: scripts/ci/append-vt-notes.sh
|
|
|
|
# ── 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.
|
|
#
|
|
# Runs against the still-DRAFT GitHub release. If publish fails, the
|
|
# release stays in draft so we can re-run without a half-shipped state.
|
|
publish-registries:
|
|
needs: [verify]
|
|
if: ${{ !cancelled() && !failure() && needs.verify.result == 'success' }}
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
id-token: write # for npm provenance
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
|
|
# The packaged versions MUST match the dispatched release version —
|
|
# the 0.8.0 release failed here because pkg/npm still carried the
|
|
# previous release's hand-pinned version and npm refuses to publish
|
|
# over an existing version. Inject the dispatch input so a forgotten
|
|
# manual bump can never fail the pipeline again. (server.json needs
|
|
# no injection: publish-mcp-registry syncs it from pkg/npm.)
|
|
- name: Sync packaging versions to the release version
|
|
env:
|
|
RELEASE_VERSION: ${{ inputs.version }}
|
|
run: |
|
|
V="${RELEASE_VERSION#v}"
|
|
jq --arg v "$V" '.version = $v' pkg/npm/package.json > pkg/npm/package.tmp
|
|
mv pkg/npm/package.tmp pkg/npm/package.json
|
|
sed -i "s/^version = \".*\"/version = \"$V\"/" pkg/pypi/pyproject.toml
|
|
grep -q "\"version\": \"$V\"" pkg/npm/package.json
|
|
grep -q "^version = \"$V\"" pkg/pypi/pyproject.toml
|
|
echo "packaging synced to $V"
|
|
|
|
# ── npm ──────────────────────────────────────────────────
|
|
- name: Setup Node
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # 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 }}
|
|
RELEASE_VERSION: ${{ inputs.version }}
|
|
# A prerelease must never become npm `latest` — plain `npm install`
|
|
# would serve the RC to everyone. Publish prereleases under the
|
|
# `next` dist-tag; testers opt in via `npm i codebase-memory-mcp@next`.
|
|
run: |
|
|
if [[ "$RELEASE_VERSION" == *-* ]]; then
|
|
npm publish --access public --provenance --tag next
|
|
else
|
|
npm publish --access public --provenance
|
|
fi
|
|
|
|
# ── PyPI ─────────────────────────────────────────────────
|
|
- name: Setup Python
|
|
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
with:
|
|
python-version: '3.12'
|
|
|
|
- name: Build PyPI distribution
|
|
working-directory: pkg/pypi
|
|
run: |
|
|
# Pin to specific versions (typosquatting / supply-chain mitigation).
|
|
# OSSF scorecard prefers --require-hashes, but that needs full
|
|
# transitive deps; version pinning is the practical compromise.
|
|
python -m pip install --upgrade 'build==1.3.0' 'twine==6.2.0'
|
|
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/*
|
|
|
|
# ── 8b. Publish server.json to the official MCP Registry ──────
|
|
# Runs after npm + PyPI so the registry can verify package ownership
|
|
# (npm: `mcpName` field in package.json; PyPI: `mcp-name:` marker in
|
|
# the package README). Authenticates with GitHub Actions OIDC — no
|
|
# token, no interactive device flow. server.json's version is synced
|
|
# from the just-published npm package so it always matches the release.
|
|
#
|
|
# Intentionally does NOT gate publish-final: the binary release is the
|
|
# product, the registry entry is metadata. A registry-preview outage
|
|
# must never block shipping. Re-run this job alone to retry — it does
|
|
# not touch npm/PyPI, so retries are safe.
|
|
publish-mcp-registry:
|
|
needs: [publish-registries]
|
|
# Skipped for prereleases: the MCP Registry has no channel concept, so an
|
|
# RC would present itself as the current version to every registry consumer.
|
|
if: ${{ !cancelled() && !failure() && needs.publish-registries.result == 'success' && !contains(inputs.version, '-') }}
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
id-token: write # GitHub OIDC auth to the MCP Registry
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
|
|
- name: Sync server.json version to the released package
|
|
env:
|
|
# The dispatch input is the authoritative release version —
|
|
# pkg/npm/package.json can lag behind it in the dispatched commit.
|
|
RELEASE_VERSION: ${{ inputs.version }}
|
|
run: |
|
|
VERSION="${RELEASE_VERSION#v}"
|
|
jq --arg v "$VERSION" '.version = $v | (.packages[].version) = $v' \
|
|
server.json > server.tmp && mv server.tmp server.json
|
|
echo "server.json pinned to $VERSION"
|
|
cat server.json
|
|
|
|
- name: Install mcp-publisher
|
|
run: |
|
|
curl -fsSL "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
|
|
|
|
- name: Authenticate to MCP Registry (GitHub OIDC)
|
|
run: ./mcp-publisher login github-oidc
|
|
|
|
- name: Publish server.json to MCP Registry
|
|
run: ./mcp-publisher publish
|
|
|
|
# ── 9. Atomic un-draft (only after npm + PyPI succeed) ────────
|
|
# The GitHub release stays in DRAFT until both registries publish.
|
|
# If anything upstream fails, the draft can be deleted and the run
|
|
# re-tried with replace=true — no half-shipped state visible to users.
|
|
publish-final:
|
|
needs: [verify, publish-registries]
|
|
if: ${{ !cancelled() && !failure() && needs.verify.result == 'success' && needs.publish-registries.result == 'success' }}
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
- name: Un-draft GitHub release
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
VERSION: ${{ inputs.version }}
|
|
run: gh release edit "$VERSION" --draft=false --repo "$GITHUB_REPOSITORY"
|