#!/usr/bin/env python3 """Lightweight release checks for AssetManager. The checks are intentionally read-only and do not require a running database. A failure exits with status 1 so the script can be used in CI or a release job. """ from __future__ import annotations import compileall import importlib.util import os from pathlib import Path import subprocess import sys ROOT = Path(__file__).resolve().parents[1] APP_DIR = ROOT / "app" def fail(message: str) -> None: print(f"[FAIL] {message}") raise SystemExit(1) def ok(message: str) -> None: print(f"[ OK ] {message}") def check_version() -> None: path = APP_DIR / "version.py" spec = importlib.util.spec_from_file_location("assetmanager_release_version", path) if spec is None or spec.loader is None: fail("app/version.py could not be loaded") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) app_version = getattr(module, "APP_VERSION", None) conventional = getattr(module, "__version__", None) if not isinstance(app_version, str) or not app_version.strip(): fail("APP_VERSION is missing or empty") if conventional != app_version: fail("__version__ must be an alias of APP_VERSION") version_file = (ROOT / "VERSION").read_text(encoding="utf-8").strip() if version_file != app_version: fail(f"VERSION ({version_file}) and app/version.py ({app_version}) differ") readme = (ROOT / "README.md").read_text(encoding="utf-8") if app_version not in readme: fail(f"README.md does not contain current version {app_version}") ok(f"Version exports are valid ({app_version})") def check_compile() -> None: if not compileall.compile_dir(str(APP_DIR), quiet=1, force=True): fail("Python compilation failed") ok("Python sources compile") def check_translations() -> None: script = ROOT / "tools" / "check_translations.py" result = subprocess.run([sys.executable, str(script)], cwd=ROOT, text=True) if result.returncode: fail("Translation check failed") ok("Translation check passed") def check_templates() -> None: try: from jinja2 import Environment, FileSystemLoader except ImportError as exc: fail(f"Jinja2 is unavailable: {exc}") template_dir = APP_DIR / "templates" env = Environment(loader=FileSystemLoader(str(template_dir))) failures: list[str] = [] for path in sorted(template_dir.rglob("*.html")): name = path.relative_to(template_dir).as_posix() try: env.get_template(name) except Exception as exc: # syntax/load validation failures.append(f"{name}: {exc}") if failures: fail("Template validation failed:\n " + "\n ".join(failures)) ok("Templates load without syntax errors") def check_job_definition_frontend() -> None: form_path = APP_DIR / "templates" / "job_definition_form.html" content = form_path.read_text(encoding="utf-8") required_frontend = [ 'id="insert-job-test-script"', 'id="job-parameters-json"', 'id="add-job-parameter"', 'job-enabled-top', 'jobdefs.own_script', ] missing = [item for item in required_frontend if item not in content] if missing: fail("Job-definition frontend validation failed; missing: " + ", ".join(missing)) template_dir = APP_DIR / "templates" / "jobs" required_templates = { "powershell_wrapper.ps1": ["{{JobId}}", "{{CallbackUrl}}", "Eigene Befehle hier einfügen"], "python_wrapper.py.txt": ["{{JobId}}", "{{CallbackUrl}}", "Eigene Befehle hier einfügen"], "bash_wrapper.sh": ["{{JobId}}", "{{CallbackUrl}}", "${status}", "${code}"], "cmd_wrapper.cmd": ["{{JobId}}", "{{CallbackUrl}}", "Eigene Befehle hier einfügen"], "powershell_test.txt": ["{{AssetName}}", "{{Hostname}}"], "python_test.txt": ["{{AssetName}}", "{{Hostname}}"], "bash_test.txt": ["{{AssetName}}", "{{Hostname}}"], "cmd_test.txt": ["{{AssetName}}", "{{Hostname}}"], } failures: list[str] = [] for filename, markers in required_templates.items(): path = template_dir / filename if not path.is_file(): failures.append(f"missing file: {filename}") continue template_content = path.read_text(encoding="utf-8") for marker in markers: if marker not in template_content: failures.append(f"{filename}: missing {marker}") if failures: fail("Job script template validation failed:\n " + "\n ".join(failures)) ok("Job-definition backend generator safeguards are present") def check_fastapi_import() -> None: # Import only. Do not start Uvicorn and do not connect to external services. sys.path.insert(0, str(ROOT)) os.environ.setdefault("DATABASE_URL", "sqlite+pysqlite:///:memory:") try: from app.main import app # noqa: F401 except Exception as exc: fail(f"FastAPI application import failed: {type(exc).__name__}: {exc}") ok("FastAPI application imports") def main() -> None: print("AssetManager release validation") check_version() check_compile() check_translations() check_templates() check_job_definition_frontend() check_fastapi_import() print("All release checks passed.") if __name__ == "__main__": main()