Bundle third-party notices into release archives; extend release tooling

Release archives now carry THIRD_PARTY_NOTICES.md, generated by
scripts/gen-third-party-notices.sh from THIRD_PARTY.md, the grammar
manifest, and the per-component license texts; the Homebrew formula
and AUR PKGBUILD install it alongside the binary. The SBOM gains
per-component license metadata, corrected versions, and the previously
missing vendored libraries. The security workflow gains a
vendored-license scan with an explicit allow-list policy, and the
release workflow exposes a skip_perf input for releases that do not
touch pipeline logic.
This commit is contained in:
Martin Vogel committed 2026-06-12 02:17:39 +02:00
1 parent 3ee9d5d112
commit 487f3f945b
10 files changed
+245 -20

No files matched your search

+12 -6
View File
@@ -79,8 +79,9 @@ jobs:
GOARCH: ${{ matrix.goarch }}
run: |
cp LICENSE install.sh build/c/
scripts/gen-third-party-notices.sh build/c/THIRD_PARTY_NOTICES.md
tar -czf "codebase-memory-mcp-${GOOS}-${GOARCH}.tar.gz" \
-C build/c codebase-memory-mcp LICENSE install.sh
-C build/c codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md
- name: Build UI binary
env:
@@ -108,8 +109,9 @@ jobs:
GOARCH: ${{ matrix.goarch }}
run: |
cp LICENSE install.sh build/c/
scripts/gen-third-party-notices.sh build/c/THIRD_PARTY_NOTICES.md
tar -czf "codebase-memory-mcp-ui-${GOOS}-${GOARCH}.tar.gz" \
-C build/c codebase-memory-mcp LICENSE install.sh
-C build/c codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
@@ -153,7 +155,8 @@ jobs:
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 LICENSE install.ps1
scripts/gen-third-party-notices.sh THIRD_PARTY_NOTICES.md
zip codebase-memory-mcp-windows-amd64.zip codebase-memory-mcp.exe LICENSE install.ps1 THIRD_PARTY_NOTICES.md
- name: Build UI binary
shell: msys2 {0}
@@ -172,7 +175,8 @@ jobs:
BIN=build/c/codebase-memory-mcp
[ -f "${BIN}.exe" ] && BIN="${BIN}.exe"
cp "$BIN" codebase-memory-mcp.exe
zip codebase-memory-mcp-ui-windows-amd64.zip codebase-memory-mcp.exe LICENSE install.ps1
scripts/gen-third-party-notices.sh THIRD_PARTY_NOTICES.md
zip codebase-memory-mcp-ui-windows-amd64.zip codebase-memory-mcp.exe LICENSE install.ps1 THIRD_PARTY_NOTICES.md
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
@@ -222,8 +226,9 @@ jobs:
ARCH: ${{ matrix.arch }}
run: |
cp LICENSE install.sh build/c/
scripts/gen-third-party-notices.sh build/c/THIRD_PARTY_NOTICES.md
tar -czf "codebase-memory-mcp-linux-${ARCH}-portable.tar.gz" \
-C build/c codebase-memory-mcp LICENSE install.sh
-C build/c codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md
- name: Build UI binary (static)
env:
@@ -240,8 +245,9 @@ jobs:
ARCH: ${{ matrix.arch }}
run: |
cp LICENSE install.sh build/c/
scripts/gen-third-party-notices.sh build/c/THIRD_PARTY_NOTICES.md
tar -czf "codebase-memory-mcp-ui-linux-${ARCH}-portable.tar.gz" \
-C build/c codebase-memory-mcp LICENSE install.sh
-C build/c codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
+10
View File
@@ -22,6 +22,16 @@ jobs:
- name: "Layer 8: Vendored dependency integrity"
run: scripts/security-vendored.sh
license-gate:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Install ScanCode Toolkit
run: pipx install scancode-toolkit
- name: "License compliance gate (one finding fails)"
run: scripts/license-gate.sh
codeql-gate:
runs-on: ubuntu-latest
timeout-minutes: 50
+19 -8
View File
@@ -26,6 +26,11 @@ on:
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
permissions:
contents: read
@@ -45,7 +50,7 @@ jobs:
needs: [lint]
uses: ./.github/workflows/_test.yml
with:
skip_perf: false
skip_perf: ${{ inputs.skip_perf }}
# ── 3. Build all platforms ──────────────────────────────────────
build:
@@ -111,13 +116,19 @@ jobs:
'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}
{'SPDXID': 'SPDXRef-Package-sqlite3', 'name': 'sqlite3', 'versionInfo': '3.51.3', 'licenseDeclared': 'blessing', 'downloadLocation': 'https://sqlite.org', 'filesAnalyzed': False},
{'SPDXID': 'SPDXRef-Package-yyjson', 'name': 'yyjson', 'versionInfo': '0.12.0', 'licenseDeclared': 'MIT', 'downloadLocation': 'https://github.com/ibireme/yyjson', 'filesAnalyzed': False},
{'SPDXID': 'SPDXRef-Package-mimalloc', 'name': 'mimalloc', 'versionInfo': '3.3.2', 'licenseDeclared': 'MIT', 'downloadLocation': 'https://github.com/microsoft/mimalloc', 'filesAnalyzed': False},
{'SPDXID': 'SPDXRef-Package-xxhash', 'name': 'xxhash', 'versionInfo': '0.8.3', 'licenseDeclared': 'BSD-2-Clause', 'downloadLocation': 'https://github.com/Cyan4973/xxHash', 'filesAnalyzed': False},
{'SPDXID': 'SPDXRef-Package-tre', 'name': 'tre', 'versionInfo': '0.8.0', 'licenseDeclared': 'BSD-2-Clause', 'downloadLocation': 'https://github.com/laurikari/tre', 'filesAnalyzed': False},
{'SPDXID': 'SPDXRef-Package-tree-sitter', 'name': 'tree-sitter', 'versionInfo': '0.24.4', 'licenseDeclared': 'MIT', 'downloadLocation': 'https://github.com/tree-sitter/tree-sitter', 'filesAnalyzed': False},
{'SPDXID': 'SPDXRef-Package-lz4', 'name': 'lz4', 'versionInfo': '1.10.0', 'licenseDeclared': 'BSD-2-Clause', 'downloadLocation': 'https://github.com/lz4/lz4', 'filesAnalyzed': False},
{'SPDXID': 'SPDXRef-Package-zstd', 'name': 'zstd', 'versionInfo': '1.5.7', 'licenseDeclared': 'BSD-3-Clause', 'downloadLocation': 'https://github.com/facebook/zstd', 'filesAnalyzed': False},
{'SPDXID': 'SPDXRef-Package-simplecpp', 'name': 'simplecpp', 'versionInfo': '1.x', 'licenseDeclared': '0BSD', 'downloadLocation': 'https://github.com/danmar/simplecpp', 'filesAnalyzed': False},
{'SPDXID': 'SPDXRef-Package-verstable', 'name': 'verstable', 'versionInfo': '2.2.1', 'licenseDeclared': 'MIT', 'downloadLocation': 'https://github.com/JacksonAllan/Verstable', 'filesAnalyzed': False},
{'SPDXID': 'SPDXRef-Package-wyhash', 'name': 'wyhash', 'versionInfo': 'final-4.3', 'licenseDeclared': 'Unlicense', 'downloadLocation': 'https://github.com/wangyi-fudan/wyhash', 'filesAnalyzed': False},
{'SPDXID': 'SPDXRef-Package-nomic-embed-code', 'name': 'nomic-embed-code-token-embeddings', 'versionInfo': '1.0', 'licenseDeclared': 'Apache-2.0', 'downloadLocation': 'https://huggingface.co/nomic-ai/nomic-embed-code', 'filesAnalyzed': False, 'comment': 'Derived int8 token embeddings; see vendored/nomic/NOTICE'},
{'SPDXID': 'SPDXRef-Package-tree-sitter-grammars', 'name': 'tree-sitter-grammars-aggregate', 'versionInfo': '158-grammars', 'licenseDeclared': 'MIT', 'downloadLocation': 'NOASSERTION', 'filesAnalyzed': False, 'comment': 'Aggregate of 158 vendored tree-sitter grammars. Predominantly MIT; exceptions: clojure CC0-1.0, jinja2 + just Apache-2.0, first-party grammars (c) DeusData MIT. Per-grammar provenance: internal/cbm/vendored/grammars/MANIFEST.md; full texts ship in THIRD_PARTY_NOTICES.md inside each archive.'}
]
}
json.dump(sbom, open('sbom.json', 'w'), indent=2)
+5
View File
@@ -20,4 +20,9 @@ package() {
"${pkgdir}/usr/bin/codebase-memory-mcp"
install -Dm644 "${srcdir}/LICENSE" \
"${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
# Third-party attribution bundle (present in archives since v0.8.1)
if [ -f "${srcdir}/THIRD_PARTY_NOTICES.md" ]; then
install -Dm644 "${srcdir}/THIRD_PARTY_NOTICES.md" \
"${pkgdir}/usr/share/licenses/${pkgname}/THIRD_PARTY_NOTICES.md"
fi
}
@@ -28,6 +28,8 @@ class CodebaseMemoryMcp < Formula
def install
bin.install "codebase-memory-mcp"
# Third-party attribution bundle (present in archives since v0.8.1)
doc.install "THIRD_PARTY_NOTICES.md" if File.exist?("THIRD_PARTY_NOTICES.md")
end
def caveats
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -euo pipefail
# Generates the third-party notices bundle shipped inside every release
# archive: THIRD_PARTY.md + the grammar provenance manifest + the verbatim
# license/notice text of every vendored component (both vendored trees).
# Deterministic output (sorted file order).
#
# Usage: scripts/gen-third-party-notices.sh [output-path]
# default output: build/THIRD_PARTY_NOTICES.md
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
OUT="${1:-$ROOT/build/THIRD_PARTY_NOTICES.md}"
mkdir -p "$(dirname "$OUT")"
{
echo "# Third-Party Notices"
echo
echo "This file accompanies the codebase-memory-mcp binary distribution."
echo "It aggregates THIRD_PARTY.md, the vendored grammar provenance"
echo "manifest, and the verbatim license / notice texts of every vendored"
echo "component, satisfying binary-redistribution notice requirements"
echo "(MIT, BSD, Apache-2.0)."
echo
echo "---"
echo
cat "$ROOT/THIRD_PARTY.md"
echo
echo "---"
echo
cat "$ROOT/internal/cbm/vendored/grammars/MANIFEST.md"
find "$ROOT/vendored" "$ROOT/internal/cbm/vendored" -type f \
\( -iname 'LICENSE*' -o -iname 'COPYING*' -o -iname 'NOTICE*' -o -iname 'UNLICENSE*' \) \
| LC_ALL=C sort | while IFS= read -r f; do
rel="${f#"$ROOT"/}"
echo
echo "==============================================================="
echo " $rel"
echo "==============================================================="
echo
cat "$f"
done
} > "$OUT"
BYTES=$(wc -c < "$OUT" | tr -d ' ')
echo "wrote $OUT (${BYTES} bytes)"
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""License-gate policy check over a ScanCode Toolkit JSON scan.
Usage: license-gate-check.py <scan.json> <license-policy.json>
Fails (exit 1) if ANY scanned file carries a detected license expression
containing an SPDX id outside the policy allow-list — one finding is enough.
"""
import json
import re
import sys
def main():
scan_path, policy_path = sys.argv[1], sys.argv[2]
with open(policy_path) as fh:
policy = json.load(fh)
allowed = {x.lower() for x in policy["allowed_spdx_ids"]}
ignored_paths = tuple(policy.get("ignored_paths", []))
skip_tokens = {"and", "or", "with", ""}
with open(scan_path) as fh:
scan = json.load(fh)
violations = []
checked = 0
for f in scan.get("files", []):
path = f.get("path", "?")
if f.get("type") != "file":
continue
if ignored_paths and path.startswith(ignored_paths):
continue
expr = f.get("detected_license_expression_spdx")
if not expr:
continue
checked += 1
for tok in re.split(r"[\s()]+", expr):
if tok.lower() in skip_tokens:
continue
if tok.lower() not in allowed:
violations.append((path, expr, tok))
break
if violations:
print("BLOCKED: %d file(s) with non-allow-listed license detections:" % len(violations))
for path, expr, tok in violations[:25]:
print(" %s: '%s' (offending id: %s)" % (path, expr, tok))
sys.exit(1)
print("OK: %d detection(s), all allow-listed" % checked)
if __name__ == "__main__":
main()
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
set -euo pipefail
# CI license-compliance gate (runs in the security workflow for both dry
# runs and releases). Two layers:
# 1) Structural: every vendored component directory must physically carry
# a license file.
# 2) ScanCode Toolkit license detection over every vendored license text
# and all first-party sources; ONE detection outside
# scripts/license-policy.json fails the gate (see license-gate-check.py).
#
# Grammar parser bodies (generated parser.c, no headers) are excluded from
# the ScanCode pass for runtime; their per-directory license files ARE
# scanned, and layer 1 guarantees each grammar carries one.
#
# Requires: scancode (pipx install scancode-toolkit), python3.
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
echo "=== License gate 1/2: structural coverage ==="
MISS=0
for d in vendored/*/ \
internal/cbm/vendored/lz4 internal/cbm/vendored/simplecpp \
internal/cbm/vendored/ts_runtime internal/cbm/vendored/verstable \
internal/cbm/vendored/wyhash internal/cbm/vendored/zstd \
internal/cbm/vendored/common internal/cbm/vendored/common/tree_sitter \
internal/cbm/vendored/grammars/*/; do
[ -d "$d" ] || continue
if ! ls "$d" | grep -qiE '^(LICENSE|LICENCE|COPYING|UNLICENSE|NOTICE)'; then
echo "BLOCKED: no license file in $d"
MISS=1
fi
done
if [ $MISS -ne 0 ]; then
echo "=== LICENSE GATE FAILED (structural) ==="
exit 1
fi
echo "OK: every vendored component directory carries a license file"
echo "=== License gate 2/2: ScanCode detection ==="
if ! command -v scancode &>/dev/null; then
echo "FAIL: scancode not installed (pipx install scancode-toolkit)"
exit 1
fi
STAGE="$(mktemp -d)"
trap 'rm -rf "$STAGE"' EXIT
# Inputs: all vendored license/notice texts + all first-party sources.
find vendored internal/cbm/vendored -type f \
\( -iname 'LICENSE*' -o -iname 'COPYING*' -o -iname 'NOTICE*' -o -iname 'UNLICENSE*' \) \
> "$STAGE/files.txt"
find src pkg scripts -type f \
\( -name '*.c' -o -name '*.h' -o -name '*.sh' -o -name '*.js' \
-o -name '*.py' -o -name '*.rb' -o -name '*.toml' -o -name '*.json' \) \
>> "$STAGE/files.txt"
mkdir -p "$STAGE/tree"
tar cf - -T "$STAGE/files.txt" | tar xf - -C "$STAGE/tree"
scancode --license --quiet --processes 2 --json-pp "$STAGE/scan.json" "$STAGE/tree" \
> "$STAGE/scancode.log" 2>&1 || {
echo "FAIL: scancode run failed:"
tail -20 "$STAGE/scancode.log"
exit 1
}
python3 scripts/license-gate-check.py "$STAGE/scan.json" scripts/license-policy.json
echo "=== License gate passed ==="
+27
View File
@@ -0,0 +1,27 @@
{
"_comment": "License allow-list for the CI license gate (scripts/license-gate.sh). A single detected SPDX id outside this list fails the pipeline. Additions to this list are deliberate, reviewed changes — never add copyleft (GPL/LGPL/AGPL/EPL/SSPL) without a maintainer decision.",
"allowed_spdx_ids": [
"MIT",
"MIT-0",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"0BSD",
"ISC",
"Zlib",
"Unlicense",
"CC0-1.0",
"blessing",
"LLVM-exception",
"LicenseRef-scancode-public-domain",
"LicenseRef-scancode-public-domain-disclaimer"
],
"_ignored_paths_comment": "Path prefixes (relative to the staged scan tree) excluded from the gate. Use ONLY for documented false positives. Justifications: the license tooling itself (gate scripts + this policy file) necessarily names prohibited licenses; gen-third-party-notices.sh echoes license terminology; src/discover/discover.c contains a license-FILENAME classification list (LICENSE-MIT, LICENSE-APACHE, ...) for file discovery, which ScanCode reads as license references.",
"ignored_paths": [
"tree/scripts/license-policy.json",
"tree/scripts/license-gate-check.py",
"tree/scripts/license-gate.sh",
"tree/scripts/gen-third-party-notices.sh",
"tree/src/discover/discover.c"
]
}
-6
View File
@@ -7,10 +7,4 @@ source code. In place of a legal notice, the source carries this blessing:
> May you find forgiveness for yourself and forgive others.
> May you share freely, never taking more than you give.
All of the code and documentation in SQLite has been dedicated to the
public domain by the authors. Anyone is free to copy, modify, publish,
use, compile, sell, or distribute the original SQLite code, either in
source code form or as a compiled binary, for any purpose, commercial or
non-commercial, and by any means.
See <https://www.sqlite.org/copyright.html> for the authoritative statement.