#!/usr/bin/env python3 """Basic sanity checks for Cloudron packaging scaffolds.""" import json import os import pathlib import sys from typing import Dict, List ROOT = pathlib.Path(__file__).resolve().parents[1] EXPECTED_BASE = os.environ.get("CLOUDRON_BASE", "cloudron/base:5.0.0") def find_apps(apps_dir: pathlib.Path) -> List[pathlib.Path]: return sorted(p for p in apps_dir.iterdir() if p.is_dir()) def check_manifest(app_dir: pathlib.Path) -> List[str]: issues: List[str] = [] manifest = app_dir / "CloudronManifest.json" if not manifest.exists(): issues.append("missing CloudronManifest.json") return issues try: data = json.loads(manifest.read_text(encoding="utf-8")) except json.JSONDecodeError as exc: issues.append(f"manifest JSON invalid: {exc}") return issues for key in ("id", "title", "version"): if not data.get(key): issues.append(f"manifest missing {key}") tagline = data.get("tagline", "") description = data.get("description", "") if "TODO" in tagline: issues.append("manifest tagline still contains TODO placeholder") if "TODO" in description: issues.append("manifest description still contains TODO placeholder") return issues def check_dockerfile(app_dir: pathlib.Path) -> List[str]: issues: List[str] = [] dockerfile = app_dir / "Dockerfile" if not dockerfile.exists(): issues.append("missing Dockerfile") return issues first_from = None for line in dockerfile.read_text(encoding="utf-8").splitlines(): line = line.strip() if line.startswith("FROM "): first_from = line.split()[1] break if first_from != EXPECTED_BASE: issues.append(f"Dockerfile base image '{first_from}' != '{EXPECTED_BASE}'") return issues def check_start_script(app_dir: pathlib.Path) -> List[str]: issues: List[str] = [] start = app_dir / "start.sh" if not start.exists(): issues.append("missing start.sh") return issues mode = start.stat().st_mode if not mode & 0o111: issues.append("start.sh is not executable") if "Replace start.sh" in start.read_text(encoding="utf-8"): issues.append("start.sh still contains placeholder command") return issues def main() -> int: apps_dir = ROOT / "apps" if not apps_dir.exists(): print("No apps directory present", file=sys.stderr) return 1 failures = 0 for app_dir in find_apps(apps_dir): app_issues: List[str] = [] app_issues.extend(check_manifest(app_dir)) app_issues.extend(check_dockerfile(app_dir)) app_issues.extend(check_start_script(app_dir)) if app_issues: failures += 1 print(f"[FAIL] {app_dir.relative_to(ROOT)}") for issue in app_issues: print(f" - {issue}") if failures: print(f"\n{failures} app(s) require updates", file=sys.stderr) return 2 print("All apps passed lint checks") return 0 if __name__ == "__main__": sys.exit(main())