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>
63 lines
2.1 KiB
Python
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())
|