mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
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:
1 parent
3ee9d5d112
commit
487f3f945b
10 files changed
+245
-20
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+47
@@ -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)"
|
||||
Executable
+53
@@ -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()
|
||||
Executable
+70
@@ -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 ==="
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user