TNCSC_Odoo/scripts/check_manifests.py
metatroncubeswdev a848373429 feat(scaffold): Phase 0 - repo scaffold, Docker dev stack, CI guardrails
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>
2026-08-17 18:48:01 -04:00

63 lines
2.1 KiB
Python

#!/usr/bin/env python3
"""Validate that every product module manifest is App-Store-ready.
Checks community_* modules for the required keys (license, version, author,
summary, category, website, depends). *_deployment modules are exempt from
price/currency/images since they are never sold independently.
"""
import ast
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
ADDONS_DIR = REPO_ROOT / "addons"
REQUIRED_KEYS = ["name", "version", "author", "website", "license", "category", "summary", "depends"]
PRODUCT_ONLY_KEYS = ["price", "currency", "images"]
def load_manifest(path: Path) -> dict:
return ast.literal_eval(path.read_text(encoding="utf-8"))
def main() -> int:
if not ADDONS_DIR.exists():
print(f"No addons/ directory found at {ADDONS_DIR}; nothing to check.")
return 0
errors = []
for module_dir in sorted(ADDONS_DIR.iterdir()):
manifest_path = module_dir / "__manifest__.py"
if not manifest_path.exists():
continue
manifest = load_manifest(manifest_path)
is_deployment = module_dir.name.endswith("_deployment")
required = REQUIRED_KEYS + ([] if is_deployment else PRODUCT_ONLY_KEYS)
for key in required:
if key not in manifest or manifest[key] in (None, "", []):
errors.append(f"{module_dir.name}: missing or empty required manifest key '{key}'")
if not is_deployment and manifest.get("license") not in ("LGPL-3", "OPL-1"):
errors.append(
f"{module_dir.name}: license must be 'LGPL-3' (or 'OPL-1' for an "
f"intentionally paid-closed module), got {manifest.get('license')!r}"
)
if manifest.get("version") and not str(manifest["version"]).startswith("19.0."):
errors.append(f"{module_dir.name}: version should start with '19.0.', got {manifest.get('version')!r}")
if errors:
print("Manifest completeness check FAILED:\n")
for error in errors:
print(f" - {error}")
return 1
print("Manifest completeness check passed.")
return 0
if __name__ == "__main__":
sys.exit(main())