Files
Assetmanager/tools/validate_release.py

149 lines
5.3 KiB
Python
Executable File

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