#!/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())