Scaffolds the CommunityOS monorepo per the implementation plan: 8 brand-neutral product modules (community_theme_base, community_membership, event_qr_ticketing, community_school, community_classifieds, community_benefits, community_interac, community_portal) plus the tncsc_deployment client layer, each with an App-Store-ready manifest, LGPL-3 license, and empty security/data/demo/tests/views scaffolding. Adds deploy/docker-compose.yml (Odoo 19 CE + Postgres 16), CI workflow that installs all modules with --test-enable, and scripts/check_brand_leak.py + check_manifests.py enforcing the no-client-identity-in-product-code and manifest-completeness rules. Verified locally: all 9 modules install clean on a fresh Odoo 19 database, and the brand-leak check correctly fails when a client term is added to a product module and passes once removed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
102 lines
3.1 KiB
Python
102 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Fail if a client-specific term appears inside a product (community_*) module.
|
|
|
|
Deployment modules (matching *_deployment) are exempt, since client identity
|
|
is expected to live there. Run from the repo root:
|
|
|
|
python scripts/check_brand_leak.py
|
|
"""
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
ADDONS_DIR = REPO_ROOT / "addons"
|
|
|
|
BANNED_WORDS = [
|
|
"tncsc",
|
|
"tamil nadu",
|
|
"tamil nadu cultural society",
|
|
]
|
|
|
|
BANNED_PATTERNS = [
|
|
re.compile(r"\b" + re.escape(word).replace(r"\ ", r"\s+") + r"\b", re.IGNORECASE)
|
|
for word in BANNED_WORDS
|
|
]
|
|
|
|
# Any email/domain that identifies the specific client deployment.
|
|
BANNED_DOMAIN_PATTERN = re.compile(r"\btncsc\.(ca|org|com)\b", re.IGNORECASE)
|
|
|
|
SCAN_EXTENSIONS = {".py", ".xml", ".csv", ".js", ".rst", ".md", ".html", ".txt"}
|
|
|
|
# Never scan the plan itself, or this checker's own word list.
|
|
EXCLUDE_FILENAMES = {"check_brand_leak.py"}
|
|
|
|
|
|
def is_deployment_module(module_dir: Path) -> bool:
|
|
return module_dir.name.endswith("_deployment")
|
|
|
|
|
|
def is_product_module(module_dir: Path) -> bool:
|
|
return module_dir.is_dir() and not module_dir.name.startswith(".")
|
|
|
|
|
|
def iter_product_files():
|
|
if not ADDONS_DIR.exists():
|
|
return
|
|
for module_dir in sorted(ADDONS_DIR.iterdir()):
|
|
if not is_product_module(module_dir) or is_deployment_module(module_dir):
|
|
continue
|
|
for path in module_dir.rglob("*"):
|
|
if not path.is_file():
|
|
continue
|
|
if path.name in EXCLUDE_FILENAMES:
|
|
continue
|
|
if path.suffix.lower() not in SCAN_EXTENSIONS:
|
|
continue
|
|
yield path
|
|
|
|
|
|
def scan_file(path: Path):
|
|
"""Return a list of (line_no, line_text, matched_term) violations."""
|
|
violations = []
|
|
try:
|
|
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
except OSError:
|
|
return violations
|
|
|
|
for line_no, line in enumerate(text.splitlines(), start=1):
|
|
for pattern in BANNED_PATTERNS:
|
|
if pattern.search(line):
|
|
violations.append((line_no, line.strip(), pattern.pattern))
|
|
if BANNED_DOMAIN_PATTERN.search(line):
|
|
violations.append((line_no, line.strip(), BANNED_DOMAIN_PATTERN.pattern))
|
|
return violations
|
|
|
|
|
|
def main() -> int:
|
|
if not ADDONS_DIR.exists():
|
|
print(f"No addons/ directory found at {ADDONS_DIR}; nothing to check.")
|
|
return 0
|
|
|
|
any_violation = False
|
|
for path in iter_product_files():
|
|
for line_no, line_text, term in scan_file(path):
|
|
any_violation = True
|
|
rel_path = path.relative_to(REPO_ROOT)
|
|
print(f"BRAND LEAK: {rel_path}:{line_no}: matched /{term}/ -> {line_text}")
|
|
|
|
if any_violation:
|
|
print(
|
|
"\nOne or more community_* (product) modules reference client-specific "
|
|
"identity. Move this content into the appropriate *_deployment module."
|
|
)
|
|
return 1
|
|
|
|
print("Brand-leak check passed: no client-specific terms found in product modules.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|