Files
deusdata--codebase-memory-mcp/scripts/package-release.sh
Martin Vogel b6a5d2c35b feat(release): ship MCPB bundles and publish them to the MCP Registry (#1246)
Every release now carries .mcpb one-click-install bundles alongside the
archives, and the MCP Registry entry lists them with per-file sha256:

- package-release.sh (canonical) builds codebase-memory-mcp-<target>.mcpb
  for darwin/windows and the STATIC linux builds — manifest.json + the same
  staged (stripped, gated) binary + LICENSE + THIRD_PARTY_NOTICES.md. The
  glibc-dynamic linux targets stay archive-only: a dynamic binary defeats
  the one-click promise.
- _build.yml / release-draft: bundles flow through provenance attestation,
  checksums.txt, cosign signing and the release asset list; checksums.txt
  is also preserved as a same-run artifact for the registry job.
- verify: the canonical scan matrix grows to 14 containers; MCPB manifests
  are validated (parse, binary server, entry_point member, command binds
  the entry point). Bundle binaries dedupe to the archive scan objects, so
  the VT gate gains only the three distinct manifest.json files.
- publish-mcp-registry: gen-mcpb-registry-entries.sh appends one mcpb
  package entry per bundle (release-asset URL + fileSha256 from the
  attested checksums) to server.json before mcp-publisher runs.
  Idempotent; a checksums file without bundles is a hard failure.
- contracts: Step 0o pins the bundle shape at its producer on every leg,
  Step 0p pins the registry entries against the live server.json, and the
  extractor contract covers the 14-container matrix incl. broken-manifest
  fail-closed cases. The linux test image gains zip for the packager.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-11 17:01:24 +02:00

271 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# package-release.sh — THE canonical release-archive step. Every venue that
# turns built binaries into a release archive (release/_build.yml, the local
# artifact-flow smoke lane) runs this file; workflows provide only
# checkout/toolchain/upload around it. Archive names and contents are defined
# HERE, nowhere else, so a local artifact smoke provably exercises the same
# bytes-layout the release publishes.
#
# This script ARCHIVES what scripts/build.sh already produced; it never builds
# or synthesizes another executable.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
usage() {
cat <<'EOF'
Usage: scripts/package-release.sh <goos> <goarch> [--out-dir DIR] [VAR=VAL ...]
The canonical release-archive step: identical in the release build and the
local artifact-flow smoke lane.
goos linux | darwin | windows
goarch arch label used verbatim in the archive name (amd64, arm64,
arm64-portable, ...)
--out-dir where to place the archive (default: repository root).
Make passthrough (VAR=VAL, forwarded to the build):
CC= CXX= compiler override, e.g. CC=clang CXX=clang++.
Environment:
BUILD_DIR build tree to archive from (default build/c).
VERSION release version stamped into the MCPB manifest (v-prefix
accepted; defaults to 0.0.0-dev outside a release build).
Archive contents (defined here, canonical) — ONE executable, no sidecars:
unix: codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md (.tar.gz)
windows: codebase-memory-mcp.exe LICENSE install.ps1 THIRD_PARTY_NOTICES.md (.zip)
MCPB bundle (.mcpb, a zip) — darwin/windows targets plus the STATIC linux
builds; same staged binary, for MCP-Registry one-click-install hosts:
manifest.json server/codebase-memory-mcp[.exe] server/LICENSE
server/THIRD_PARTY_NOTICES.md
Only one build variant ships: the binary carries the graph UI and the agent
integration templates inside itself, so an extracted archive is immediately
complete — no adjacent data file has to resolve for `install` to work.
EOF
}
GOOS=""
GOARCH=""
OUT_DIR="$ROOT"
MAKE_ARGS=()
expect_value=""
for arg in "$@"; do
case "$expect_value" in
out-dir) OUT_DIR="$arg"; expect_value=""; continue ;;
esac
case "$arg" in
-h | --help) usage; exit 0 ;;
--out-dir) expect_value="out-dir" ;;
--out-dir=*) OUT_DIR="${arg#--out-dir=}" ;;
-*)
echo "package-release: unknown option '$arg'. Please consult --help." >&2
exit 2
;;
*=*) MAKE_ARGS+=("$arg") ;;
*)
if [ -z "$GOOS" ]; then GOOS="$arg"
elif [ -z "$GOARCH" ]; then GOARCH="$arg"
else
echo "package-release: unexpected argument '$arg'. Please consult --help." >&2
exit 2
fi
;;
esac
done
[ -n "$GOOS" ] && [ -n "$GOARCH" ] || { usage >&2; exit 2; }
case "$GOOS" in
linux | darwin | windows) ;;
*) echo "package-release: goos must be linux, darwin or windows." >&2; exit 2 ;;
esac
[ -n "$expect_value" ] && { echo "package-release: --$expect_value needs a value." >&2; exit 2; }
BUILD_DIR="${BUILD_DIR:-build/c}"
OUT_DIR="$(mkdir -p "$OUT_DIR" && cd "$OUT_DIR" && pwd)"
NAME="codebase-memory-mcp-${GOOS}-${GOARCH}"
# Ship every release binary stripped. Production already builds without -g, but
# the linker still keeps a ~536 KB .symtab, so releases carried their full
# symbol table to users: bigger downloads and a free map of the internals, with
# nothing gained. Nothing symbolizes at runtime (mem_profile.c is not in the
# production build and never calls backtrace_symbols), so this costs no
# diagnostics.
#
# A historical unstripped linux-amd64 artifact was the sole Microsoft detection
# in release run 30398064336, while related stripped artifacts later scanned
# clean. That is useful release evidence but not controlled feature attribution:
# engine state and other bytes can differ between observations. Independently
# of the opaque verdict, stripping removes an unnecessary symbol surface and
# does not change program behavior.
#
# macOS is ad-hoc signed by the build workflow BEFORE this script runs, and
# stripping invalidates that signature, so Mach-O is re-signed here. Skipping
# the re-sign ships a binary the kernel refuses to exec.
strip_release_binary() {
local binary="$1"
[ -f "$binary" ] || return 0
# The right flags differ per format, and the WRONG ones fail silently in
# the dangerous direction. Measured on the flagged darwin-arm64 artifact:
#
# llvm-strip --strip-all 373 symbols scanned CLEAN
# strip (no flags) 378 symbols equivalent
# strip -x -S 4058 symbols the state VirusTotal FLAGGED
# strip -X / -u -r 4058 symbols likewise
#
# Apple's strip returns success for `-x -S`, so a helper that just tries
# candidates until one exits 0 would quietly reship the flagged binary.
# GNU/LLVM `--strip-all` is not even accepted by Apple's strip, which is why
# generalising it to every platform broke the macOS build -- loudly, which
# was the lucky outcome.
#
# So: --strip-all where it is understood, plain `strip` for Mach-O, and a
# hard error when no candidate can do the job. Never a weaker fallback.
local stripped=""
for tool in "${STRIP:-}" llvm-strip strip; do
[ -n "$tool" ] || continue
command -v "$tool" >/dev/null 2>&1 || continue
if "$tool" --strip-all "$binary" 2>/dev/null; then
stripped="$tool --strip-all"
elif [ "$GOOS" = "darwin" ] && "$tool" "$binary" 2>/dev/null; then
stripped="$tool"
fi
[ -n "$stripped" ] && break
done
if [ -z "$stripped" ]; then
echo "package-release: no working strip for $binary" >&2
return 1
fi
if [ "$GOOS" = "darwin" ]; then
command -v codesign >/dev/null 2>&1 &&
codesign --sign - --force "$binary" 2>/dev/null
fi
echo "=== package-release: stripped $(basename "$binary") ==="
return 0
}
if [ "$GOOS" = "windows" ]; then
# Windows ships one executable, exactly like every other runtime set. There is no
# launcher stub: a small unsigned PE whose entire job is to verify and
# execute another binary adds loader-like behavior and another artifact to
# audit. Historical launcher builds received Microsoft Wacatac verdicts,
# but that observation does not identify a stable feature or establish
# causation. Self-update — the launcher's whole reason to exist — moves OUT
# of the running process into install.ps1: Windows' executable lock only
# blocks a process from replacing ITSELF.
PAYLOAD="$BUILD_DIR/codebase-memory-mcp"
[ -f "${PAYLOAD}.exe" ] && PAYLOAD="${PAYLOAD}.exe"
[ -f "$PAYLOAD" ] || { echo "package-release: build first; missing $PAYLOAD" >&2; exit 2; }
STAGED_BINARY_NAME="codebase-memory-mcp.exe"
INSTALLER="install.ps1"
else
PAYLOAD="$BUILD_DIR/codebase-memory-mcp"
[ -f "$PAYLOAD" ] || { echo "package-release: build first; missing $PAYLOAD" >&2; exit 2; }
STAGED_BINARY_NAME="codebase-memory-mcp"
INSTALLER="install.sh"
fi
# Work from one owner-private directory for every target. The binary is copied
# here BEFORE strip/re-sign and the archive is created from this same directory,
# so the executable that validates the adjacent sidecars is byte-for-byte the
# executable users receive. Keeping the stage beside the build also avoids a
# system /tmp mounted noexec: the validation probe must actually execute.
PACK_DIR="$(mktemp -d "$BUILD_DIR/.cbm-package.XXXXXX")"
trap 'rm -rf "$PACK_DIR"' EXIT
STAGED_BINARY="$PACK_DIR/$STAGED_BINARY_NAME"
cp "$PAYLOAD" "$STAGED_BINARY"
strip_release_binary "$STAGED_BINARY" || exit 2
# Gate the artifact AFTER strip/re-sign: this is the final executable image and
# no later step may mutate it.
bash scripts/ci/check-binary-composition.sh "$STAGED_BINARY" || exit 2
cp LICENSE "$INSTALLER" "$PACK_DIR/"
scripts/gen-third-party-notices.sh "$PACK_DIR/THIRD_PARTY_NOTICES.md"
if [ "$GOOS" = "windows" ]; then
(
cd "$PACK_DIR"
rm -f "$OUT_DIR/$NAME.zip"
zip -q "$OUT_DIR/$NAME.zip" \
codebase-memory-mcp.exe LICENSE install.ps1 THIRD_PARTY_NOTICES.md
)
echo "=== package-release: $OUT_DIR/$NAME.zip ==="
else
# BSD tar otherwise materializes macOS extended attributes as hidden
# AppleDouble `._*` members, violating the exact four-file inventory.
COPYFILE_DISABLE=1 tar -czf "$OUT_DIR/$NAME.tar.gz" -C "$PACK_DIR" \
codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md
echo "=== package-release: $OUT_DIR/$NAME.tar.gz ==="
fi
# ── MCPB bundle (registryType "mcpb" in the MCP Registry) ─────────────────
# Repackages the SAME staged binary the archive above ships — already
# stripped, re-signed and composition-gated — so the VirusTotal scan set
# dedupes the executable to the archive's object; only manifest.json is a
# new scan member. The installer script is deliberately absent: an MCPB
# host manages install/update itself.
build_mcpb_bundle() {
local out="$OUT_DIR/$NAME.mcpb"
local stage="$PACK_DIR/.mcpb-stage"
local entry="server/$STAGED_BINARY_NAME"
local platform
case "$GOOS" in
darwin) platform="darwin" ;;
windows) platform="win32" ;;
linux) platform="linux" ;;
esac
command -v zip >/dev/null 2>&1 || {
echo "package-release: zip is required to build $NAME.mcpb" >&2
return 1
}
mkdir -p "$stage/server"
cp "$STAGED_BINARY" "$stage/$entry"
cp "$PACK_DIR/LICENSE" "$PACK_DIR/THIRD_PARTY_NOTICES.md" "$stage/server/"
local mcpb_version="${VERSION:-0.0.0-dev}"
mcpb_version="${mcpb_version#v}"
# ${__dirname} is the MCPB host's substitution variable, not shell —
# hence the escapes. Hosts append .exe themselves where needed, but the
# manifest names the actual member so non-normalizing hosts also work.
cat >"$stage/manifest.json" <<EOF
{
"manifest_version": "0.3",
"name": "codebase-memory-mcp",
"display_name": "Codebase Memory",
"version": "$mcpb_version",
"description": "Codebase knowledge graph for AI agents — 159 languages, sub-ms queries, 99% fewer tokens.",
"author": { "name": "DeusData", "url": "https://github.com/DeusData" },
"repository": { "type": "git", "url": "https://github.com/DeusData/codebase-memory-mcp" },
"homepage": "https://deusdata.github.io/codebase-memory-mcp/",
"license": "MIT",
"server": {
"type": "binary",
"entry_point": "$entry",
"mcp_config": {
"command": "\${__dirname}/$entry",
"args": []
}
},
"compatibility": {
"platforms": ["$platform"]
}
}
EOF
rm -f "$out"
(
cd "$stage"
zip -q -X "$out" \
manifest.json "$entry" server/LICENSE server/THIRD_PARTY_NOTICES.md
)
echo "=== package-release: $out ==="
}
# MCPB eligibility: every darwin/windows target, but only the STATIC linux
# builds — a glibc-dynamic binary defeats the one-click-install promise.
case "$GOOS/$GOARCH" in
darwin/* | windows/* | linux/*-portable) build_mcpb_bundle || exit 2 ;;
esac