Skip to content

Commit 487f3f9

Browse files
committed
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.
1 parent 3ee9d5d commit 487f3f9

10 files changed

Lines changed: 245 additions & 20 deletions

File tree

.github/workflows/_build.yml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,9 @@ jobs:
7979
GOARCH: ${{ matrix.goarch }}
8080
run: |
8181
cp LICENSE install.sh build/c/
82+
scripts/gen-third-party-notices.sh build/c/THIRD_PARTY_NOTICES.md
8283
tar -czf "codebase-memory-mcp-${GOOS}-${GOARCH}.tar.gz" \
83-
-C build/c codebase-memory-mcp LICENSE install.sh
84+
-C build/c codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md
8485
8586
- name: Build UI binary
8687
env:
@@ -108,8 +109,9 @@ jobs:
108109
GOARCH: ${{ matrix.goarch }}
109110
run: |
110111
cp LICENSE install.sh build/c/
112+
scripts/gen-third-party-notices.sh build/c/THIRD_PARTY_NOTICES.md
111113
tar -czf "codebase-memory-mcp-ui-${GOOS}-${GOARCH}.tar.gz" \
112-
-C build/c codebase-memory-mcp LICENSE install.sh
114+
-C build/c codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md
113115
114116
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
115117
with:
@@ -153,7 +155,8 @@ jobs:
153155
BIN=build/c/codebase-memory-mcp
154156
[ -f "${BIN}.exe" ] && BIN="${BIN}.exe"
155157
cp "$BIN" codebase-memory-mcp.exe
156-
zip codebase-memory-mcp-windows-amd64.zip codebase-memory-mcp.exe LICENSE install.ps1
158+
scripts/gen-third-party-notices.sh THIRD_PARTY_NOTICES.md
159+
zip codebase-memory-mcp-windows-amd64.zip codebase-memory-mcp.exe LICENSE install.ps1 THIRD_PARTY_NOTICES.md
157160
158161
- name: Build UI binary
159162
shell: msys2 {0}
@@ -172,7 +175,8 @@ jobs:
172175
BIN=build/c/codebase-memory-mcp
173176
[ -f "${BIN}.exe" ] && BIN="${BIN}.exe"
174177
cp "$BIN" codebase-memory-mcp.exe
175-
zip codebase-memory-mcp-ui-windows-amd64.zip codebase-memory-mcp.exe LICENSE install.ps1
178+
scripts/gen-third-party-notices.sh THIRD_PARTY_NOTICES.md
179+
zip codebase-memory-mcp-ui-windows-amd64.zip codebase-memory-mcp.exe LICENSE install.ps1 THIRD_PARTY_NOTICES.md
176180
177181
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
178182
with:
@@ -222,8 +226,9 @@ jobs:
222226
ARCH: ${{ matrix.arch }}
223227
run: |
224228
cp LICENSE install.sh build/c/
229+
scripts/gen-third-party-notices.sh build/c/THIRD_PARTY_NOTICES.md
225230
tar -czf "codebase-memory-mcp-linux-${ARCH}-portable.tar.gz" \
226-
-C build/c codebase-memory-mcp LICENSE install.sh
231+
-C build/c codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md
227232
228233
- name: Build UI binary (static)
229234
env:
@@ -240,8 +245,9 @@ jobs:
240245
ARCH: ${{ matrix.arch }}
241246
run: |
242247
cp LICENSE install.sh build/c/
248+
scripts/gen-third-party-notices.sh build/c/THIRD_PARTY_NOTICES.md
243249
tar -czf "codebase-memory-mcp-ui-linux-${ARCH}-portable.tar.gz" \
244-
-C build/c codebase-memory-mcp LICENSE install.sh
250+
-C build/c codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md
245251
246252
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
247253
with:

.github/workflows/_security.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ jobs:
2222
- name: "Layer 8: Vendored dependency integrity"
2323
run: scripts/security-vendored.sh
2424

25+
license-gate:
26+
runs-on: ubuntu-latest
27+
timeout-minutes: 15
28+
steps:
29+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
30+
- name: Install ScanCode Toolkit
31+
run: pipx install scancode-toolkit
32+
- name: "License compliance gate (one finding fails)"
33+
run: scripts/license-gate.sh
34+
2535
codeql-gate:
2636
runs-on: ubuntu-latest
2737
timeout-minutes: 50

.github/workflows/release.yml

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ on:
2626
type: choice
2727
options: ['full', 'quick', 'none']
2828
default: 'quick'
29+
skip_perf:
30+
description: "Skip performance tests (use when no pipeline logic changed)"
31+
required: false
32+
type: boolean
33+
default: false
2934

3035
permissions:
3136
contents: read
@@ -45,7 +50,7 @@ jobs:
4550
needs: [lint]
4651
uses: ./.github/workflows/_test.yml
4752
with:
48-
skip_perf: false
53+
skip_perf: ${{ inputs.skip_perf }}
4954

5055
# ── 3. Build all platforms ──────────────────────────────────────
5156
build:
@@ -111,13 +116,19 @@ jobs:
111116
'creators': ['Tool: codebase-memory-mcp-release-pipeline']
112117
},
113118
'packages': [
114-
{'SPDXID': 'SPDXRef-Package-sqlite3', 'name': 'sqlite3', 'versionInfo': '3.49.1', 'downloadLocation': 'https://sqlite.org', 'filesAnalyzed': False},
115-
{'SPDXID': 'SPDXRef-Package-yyjson', 'name': 'yyjson', 'versionInfo': '0.10.0', 'downloadLocation': 'https://github.com/ibireme/yyjson', 'filesAnalyzed': False},
116-
{'SPDXID': 'SPDXRef-Package-mongoose', 'name': 'mongoose', 'versionInfo': '7.16', 'downloadLocation': 'https://github.com/cesanta/mongoose', 'filesAnalyzed': False},
117-
{'SPDXID': 'SPDXRef-Package-mimalloc', 'name': 'mimalloc', 'versionInfo': '2.1.7', 'downloadLocation': 'https://github.com/microsoft/mimalloc', 'filesAnalyzed': False},
118-
{'SPDXID': 'SPDXRef-Package-xxhash', 'name': 'xxhash', 'versionInfo': '0.8.2', 'downloadLocation': 'https://github.com/Cyan4973/xxHash', 'filesAnalyzed': False},
119-
{'SPDXID': 'SPDXRef-Package-tre', 'name': 'tre', 'versionInfo': '0.8.0', 'downloadLocation': 'https://github.com/laurikari/tre', 'filesAnalyzed': False},
120-
{'SPDXID': 'SPDXRef-Package-tree-sitter', 'name': 'tree-sitter', 'versionInfo': '0.24.4', 'downloadLocation': 'https://github.com/tree-sitter/tree-sitter', 'filesAnalyzed': False}
119+
{'SPDXID': 'SPDXRef-Package-sqlite3', 'name': 'sqlite3', 'versionInfo': '3.51.3', 'licenseDeclared': 'blessing', 'downloadLocation': 'https://sqlite.org', 'filesAnalyzed': False},
120+
{'SPDXID': 'SPDXRef-Package-yyjson', 'name': 'yyjson', 'versionInfo': '0.12.0', 'licenseDeclared': 'MIT', 'downloadLocation': 'https://github.com/ibireme/yyjson', 'filesAnalyzed': False},
121+
{'SPDXID': 'SPDXRef-Package-mimalloc', 'name': 'mimalloc', 'versionInfo': '3.3.2', 'licenseDeclared': 'MIT', 'downloadLocation': 'https://github.com/microsoft/mimalloc', 'filesAnalyzed': False},
122+
{'SPDXID': 'SPDXRef-Package-xxhash', 'name': 'xxhash', 'versionInfo': '0.8.3', 'licenseDeclared': 'BSD-2-Clause', 'downloadLocation': 'https://github.com/Cyan4973/xxHash', 'filesAnalyzed': False},
123+
{'SPDXID': 'SPDXRef-Package-tre', 'name': 'tre', 'versionInfo': '0.8.0', 'licenseDeclared': 'BSD-2-Clause', 'downloadLocation': 'https://github.com/laurikari/tre', 'filesAnalyzed': False},
124+
{'SPDXID': 'SPDXRef-Package-tree-sitter', 'name': 'tree-sitter', 'versionInfo': '0.24.4', 'licenseDeclared': 'MIT', 'downloadLocation': 'https://github.com/tree-sitter/tree-sitter', 'filesAnalyzed': False},
125+
{'SPDXID': 'SPDXRef-Package-lz4', 'name': 'lz4', 'versionInfo': '1.10.0', 'licenseDeclared': 'BSD-2-Clause', 'downloadLocation': 'https://github.com/lz4/lz4', 'filesAnalyzed': False},
126+
{'SPDXID': 'SPDXRef-Package-zstd', 'name': 'zstd', 'versionInfo': '1.5.7', 'licenseDeclared': 'BSD-3-Clause', 'downloadLocation': 'https://github.com/facebook/zstd', 'filesAnalyzed': False},
127+
{'SPDXID': 'SPDXRef-Package-simplecpp', 'name': 'simplecpp', 'versionInfo': '1.x', 'licenseDeclared': '0BSD', 'downloadLocation': 'https://github.com/danmar/simplecpp', 'filesAnalyzed': False},
128+
{'SPDXID': 'SPDXRef-Package-verstable', 'name': 'verstable', 'versionInfo': '2.2.1', 'licenseDeclared': 'MIT', 'downloadLocation': 'https://github.com/JacksonAllan/Verstable', 'filesAnalyzed': False},
129+
{'SPDXID': 'SPDXRef-Package-wyhash', 'name': 'wyhash', 'versionInfo': 'final-4.3', 'licenseDeclared': 'Unlicense', 'downloadLocation': 'https://github.com/wangyi-fudan/wyhash', 'filesAnalyzed': False},
130+
{'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'},
131+
{'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.'}
121132
]
122133
}
123134
json.dump(sbom, open('sbom.json', 'w'), indent=2)

pkg/aur/PKGBUILD

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,9 @@ package() {
2020
"${pkgdir}/usr/bin/codebase-memory-mcp"
2121
install -Dm644 "${srcdir}/LICENSE" \
2222
"${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
23+
# Third-party attribution bundle (present in archives since v0.8.1)
24+
if [ -f "${srcdir}/THIRD_PARTY_NOTICES.md" ]; then
25+
install -Dm644 "${srcdir}/THIRD_PARTY_NOTICES.md" \
26+
"${pkgdir}/usr/share/licenses/${pkgname}/THIRD_PARTY_NOTICES.md"
27+
fi
2328
}

pkg/homebrew/Formula/codebase-memory-mcp.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ class CodebaseMemoryMcp < Formula
2828

2929
def install
3030
bin.install "codebase-memory-mcp"
31+
# Third-party attribution bundle (present in archives since v0.8.1)
32+
doc.install "THIRD_PARTY_NOTICES.md" if File.exist?("THIRD_PARTY_NOTICES.md")
3133
end
3234

3335
def caveats

scripts/gen-third-party-notices.sh

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Generates the third-party notices bundle shipped inside every release
5+
# archive: THIRD_PARTY.md + the grammar provenance manifest + the verbatim
6+
# license/notice text of every vendored component (both vendored trees).
7+
# Deterministic output (sorted file order).
8+
#
9+
# Usage: scripts/gen-third-party-notices.sh [output-path]
10+
# default output: build/THIRD_PARTY_NOTICES.md
11+
12+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
13+
OUT="${1:-$ROOT/build/THIRD_PARTY_NOTICES.md}"
14+
mkdir -p "$(dirname "$OUT")"
15+
16+
{
17+
echo "# Third-Party Notices"
18+
echo
19+
echo "This file accompanies the codebase-memory-mcp binary distribution."
20+
echo "It aggregates THIRD_PARTY.md, the vendored grammar provenance"
21+
echo "manifest, and the verbatim license / notice texts of every vendored"
22+
echo "component, satisfying binary-redistribution notice requirements"
23+
echo "(MIT, BSD, Apache-2.0)."
24+
echo
25+
echo "---"
26+
echo
27+
cat "$ROOT/THIRD_PARTY.md"
28+
echo
29+
echo "---"
30+
echo
31+
cat "$ROOT/internal/cbm/vendored/grammars/MANIFEST.md"
32+
33+
find "$ROOT/vendored" "$ROOT/internal/cbm/vendored" -type f \
34+
\( -iname 'LICENSE*' -o -iname 'COPYING*' -o -iname 'NOTICE*' -o -iname 'UNLICENSE*' \) \
35+
| LC_ALL=C sort | while IFS= read -r f; do
36+
rel="${f#"$ROOT"/}"
37+
echo
38+
echo "==============================================================="
39+
echo " $rel"
40+
echo "==============================================================="
41+
echo
42+
cat "$f"
43+
done
44+
} > "$OUT"
45+
46+
BYTES=$(wc -c < "$OUT" | tr -d ' ')
47+
echo "wrote $OUT (${BYTES} bytes)"

scripts/license-gate-check.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env python3
2+
"""License-gate policy check over a ScanCode Toolkit JSON scan.
3+
4+
Usage: license-gate-check.py <scan.json> <license-policy.json>
5+
6+
Fails (exit 1) if ANY scanned file carries a detected license expression
7+
containing an SPDX id outside the policy allow-list — one finding is enough.
8+
"""
9+
import json
10+
import re
11+
import sys
12+
13+
14+
def main():
15+
scan_path, policy_path = sys.argv[1], sys.argv[2]
16+
with open(policy_path) as fh:
17+
policy = json.load(fh)
18+
allowed = {x.lower() for x in policy["allowed_spdx_ids"]}
19+
ignored_paths = tuple(policy.get("ignored_paths", []))
20+
skip_tokens = {"and", "or", "with", ""}
21+
22+
with open(scan_path) as fh:
23+
scan = json.load(fh)
24+
25+
violations = []
26+
checked = 0
27+
for f in scan.get("files", []):
28+
path = f.get("path", "?")
29+
if f.get("type") != "file":
30+
continue
31+
if ignored_paths and path.startswith(ignored_paths):
32+
continue
33+
expr = f.get("detected_license_expression_spdx")
34+
if not expr:
35+
continue
36+
checked += 1
37+
for tok in re.split(r"[\s()]+", expr):
38+
if tok.lower() in skip_tokens:
39+
continue
40+
if tok.lower() not in allowed:
41+
violations.append((path, expr, tok))
42+
break
43+
44+
if violations:
45+
print("BLOCKED: %d file(s) with non-allow-listed license detections:" % len(violations))
46+
for path, expr, tok in violations[:25]:
47+
print(" %s: '%s' (offending id: %s)" % (path, expr, tok))
48+
sys.exit(1)
49+
print("OK: %d detection(s), all allow-listed" % checked)
50+
51+
52+
if __name__ == "__main__":
53+
main()

scripts/license-gate.sh

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# CI license-compliance gate (runs in the security workflow for both dry
5+
# runs and releases). Two layers:
6+
# 1) Structural: every vendored component directory must physically carry
7+
# a license file.
8+
# 2) ScanCode Toolkit license detection over every vendored license text
9+
# and all first-party sources; ONE detection outside
10+
# scripts/license-policy.json fails the gate (see license-gate-check.py).
11+
#
12+
# Grammar parser bodies (generated parser.c, no headers) are excluded from
13+
# the ScanCode pass for runtime; their per-directory license files ARE
14+
# scanned, and layer 1 guarantees each grammar carries one.
15+
#
16+
# Requires: scancode (pipx install scancode-toolkit), python3.
17+
18+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
19+
cd "$ROOT"
20+
21+
echo "=== License gate 1/2: structural coverage ==="
22+
MISS=0
23+
for d in vendored/*/ \
24+
internal/cbm/vendored/lz4 internal/cbm/vendored/simplecpp \
25+
internal/cbm/vendored/ts_runtime internal/cbm/vendored/verstable \
26+
internal/cbm/vendored/wyhash internal/cbm/vendored/zstd \
27+
internal/cbm/vendored/common internal/cbm/vendored/common/tree_sitter \
28+
internal/cbm/vendored/grammars/*/; do
29+
[ -d "$d" ] || continue
30+
if ! ls "$d" | grep -qiE '^(LICENSE|LICENCE|COPYING|UNLICENSE|NOTICE)'; then
31+
echo "BLOCKED: no license file in $d"
32+
MISS=1
33+
fi
34+
done
35+
if [ $MISS -ne 0 ]; then
36+
echo "=== LICENSE GATE FAILED (structural) ==="
37+
exit 1
38+
fi
39+
echo "OK: every vendored component directory carries a license file"
40+
41+
echo "=== License gate 2/2: ScanCode detection ==="
42+
if ! command -v scancode &>/dev/null; then
43+
echo "FAIL: scancode not installed (pipx install scancode-toolkit)"
44+
exit 1
45+
fi
46+
47+
STAGE="$(mktemp -d)"
48+
trap 'rm -rf "$STAGE"' EXIT
49+
50+
# Inputs: all vendored license/notice texts + all first-party sources.
51+
find vendored internal/cbm/vendored -type f \
52+
\( -iname 'LICENSE*' -o -iname 'COPYING*' -o -iname 'NOTICE*' -o -iname 'UNLICENSE*' \) \
53+
> "$STAGE/files.txt"
54+
find src pkg scripts -type f \
55+
\( -name '*.c' -o -name '*.h' -o -name '*.sh' -o -name '*.js' \
56+
-o -name '*.py' -o -name '*.rb' -o -name '*.toml' -o -name '*.json' \) \
57+
>> "$STAGE/files.txt"
58+
59+
mkdir -p "$STAGE/tree"
60+
tar cf - -T "$STAGE/files.txt" | tar xf - -C "$STAGE/tree"
61+
62+
scancode --license --quiet --processes 2 --json-pp "$STAGE/scan.json" "$STAGE/tree" \
63+
> "$STAGE/scancode.log" 2>&1 || {
64+
echo "FAIL: scancode run failed:"
65+
tail -20 "$STAGE/scancode.log"
66+
exit 1
67+
}
68+
69+
python3 scripts/license-gate-check.py "$STAGE/scan.json" scripts/license-policy.json
70+
echo "=== License gate passed ==="

scripts/license-policy.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"_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.",
3+
"allowed_spdx_ids": [
4+
"MIT",
5+
"MIT-0",
6+
"Apache-2.0",
7+
"BSD-2-Clause",
8+
"BSD-3-Clause",
9+
"0BSD",
10+
"ISC",
11+
"Zlib",
12+
"Unlicense",
13+
"CC0-1.0",
14+
"blessing",
15+
"LLVM-exception",
16+
"LicenseRef-scancode-public-domain",
17+
"LicenseRef-scancode-public-domain-disclaimer"
18+
],
19+
"_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.",
20+
"ignored_paths": [
21+
"tree/scripts/license-policy.json",
22+
"tree/scripts/license-gate-check.py",
23+
"tree/scripts/license-gate.sh",
24+
"tree/scripts/gen-third-party-notices.sh",
25+
"tree/src/discover/discover.c"
26+
]
27+
}

vendored/sqlite3/LICENSE.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,4 @@ source code. In place of a legal notice, the source carries this blessing:
77
> May you find forgiveness for yourself and forgive others.
88
> May you share freely, never taking more than you give.
99
10-
All of the code and documentation in SQLite has been dedicated to the
11-
public domain by the authors. Anyone is free to copy, modify, publish,
12-
use, compile, sell, or distribute the original SQLite code, either in
13-
source code form or as a compiled binary, for any purpose, commercial or
14-
non-commercial, and by any means.
15-
1610
See <https://www.sqlite.org/copyright.html> for the authoritative statement.

0 commit comments

Comments
 (0)