The monolithic check_updates.py has been split into the update_monitor/ package with separate modules for fetching, extraction, comparison, classification, and notification. This makes the codebase easier to navigate and test. The flat services.yaml is replaced by per-service directories under services/<name>/config.yaml, co-locating config with update scripts and AI review examples. When --auto-update is enabled, services with an auto-update.sh script get their release notes classified by a local Ollama model (using few-shot examples from review-examples/) to decide whether the update can be applied automatically or needs manual admin intervention. Results are reported in a single consolidated Matrix message. Immich is the first service with auto-update configured, using 7 real release note examples to train the classifier.
131 lines
5.5 KiB
Python
131 lines
5.5 KiB
Python
import argparse
|
|
import sys
|
|
|
|
from .checker import build_summary, check_service
|
|
from .classifier import Classifier
|
|
from .comparator import compare_versions
|
|
from .loader import load_dotenv, load_services_dir
|
|
from .notifier import send_matrix_message, send_uptime_kuma_ping
|
|
from .updater import run_update_script
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Check for webservice updates")
|
|
parser.add_argument("--services", default="services", help="Path to services directory")
|
|
parser.add_argument("--all", action="store_true", help="Show all services")
|
|
parser.add_argument("--timeout", type=float, default=10.0, help="HTTP timeout in seconds")
|
|
parser.add_argument("--user-agent", default="service-update-alerts/1.0", help="HTTP user agent")
|
|
parser.add_argument("--auto-update", action="store_true", help="Automatically update services when safe")
|
|
parser.add_argument("--classify-timeout", type=float, default=300.0, help="Timeout for Ollama classification in seconds")
|
|
parser.add_argument("--script-timeout", type=float, default=300.0, help="Timeout for auto-update scripts in seconds")
|
|
args = parser.parse_args()
|
|
|
|
load_dotenv()
|
|
|
|
try:
|
|
services = load_services_dir(args.services)
|
|
except Exception as exc:
|
|
print(f"Failed to load services: {exc}", file=sys.stderr)
|
|
return 1
|
|
|
|
results = [check_service(svc, args.timeout, args.user_agent) for svc in services.values()]
|
|
|
|
output_lines = []
|
|
for result in sorted(results, key=lambda r: r["name"].lower()):
|
|
comparison = compare_versions(result["current"], result["latest"])
|
|
has_update = comparison == 1
|
|
if not args.all and not has_update:
|
|
continue
|
|
current = result["current"] or "unknown"
|
|
latest = result["latest"] or "unknown"
|
|
notes = []
|
|
if comparison is None and result["current"] and result["latest"]:
|
|
notes.append("unparseable")
|
|
if result["current_error"]:
|
|
notes.append("current error")
|
|
if result["latest_error"]:
|
|
notes.append("latest error")
|
|
suffix = f" [{' '.join(notes)}]" if notes else ""
|
|
output_lines.append(f"{result['name']}: {current} -> {latest}{suffix}")
|
|
|
|
if output_lines:
|
|
print("\n".join(output_lines))
|
|
|
|
for result in sorted(results, key=lambda r: r["name"].lower()):
|
|
if result["current_error"]:
|
|
print(f"{result['name']}: current version error: {result['current_error']}", file=sys.stderr)
|
|
if result["latest_error"]:
|
|
print(f"{result['name']}: latest version error: {result['latest_error']}", file=sys.stderr)
|
|
|
|
# Build one consolidated Matrix message
|
|
matrix_sections = []
|
|
|
|
if args.auto_update:
|
|
classifier = Classifier()
|
|
auto_updated = []
|
|
manual_needed = []
|
|
update_pending = []
|
|
|
|
for result in results:
|
|
if compare_versions(result["current"], result["latest"]) != 1:
|
|
continue
|
|
service = services[result["name"]]
|
|
current = result["current"] or "unknown"
|
|
latest = result["latest"] or "unknown"
|
|
|
|
script = service.service_dir / "auto-update.sh" if service.service_dir else None
|
|
if not script or not script.exists():
|
|
update_pending.append(f"- {service.name}: {current} -> {latest}")
|
|
continue
|
|
|
|
classification = classifier.classify(
|
|
service.service_dir,
|
|
result.get("release_notes"),
|
|
current,
|
|
latest,
|
|
timeout=args.classify_timeout,
|
|
)
|
|
if classification["action"] == "auto":
|
|
script_result = run_update_script(script, current, latest, timeout=args.script_timeout)
|
|
if script_result["success"]:
|
|
msg = f"- {service.name}: {current} -> {latest}\n Reason: {classification['reason']}"
|
|
if script_result["output"]:
|
|
msg += f"\n Output: {script_result['output']}"
|
|
auto_updated.append(msg)
|
|
else:
|
|
msg = f"- {service.name}: {current} -> {latest}\n Error: {script_result['error']}"
|
|
manual_needed.append(msg)
|
|
else:
|
|
msg = f"- {service.name}: {current} -> {latest}\n Reason: {classification['reason']}"
|
|
manual_needed.append(msg)
|
|
|
|
if auto_updated:
|
|
matrix_sections.append("Auto-updated:\n" + "\n".join(auto_updated))
|
|
if manual_needed:
|
|
matrix_sections.append("Manual update needed:\n" + "\n".join(manual_needed))
|
|
if update_pending:
|
|
matrix_sections.append("Updates available (no auto-update configured):\n" + "\n".join(update_pending))
|
|
else:
|
|
summary, should_notify = build_summary(results)
|
|
if should_notify:
|
|
matrix_sections.append(summary)
|
|
|
|
errors = [r for r in results if r["current_error"] or r["latest_error"]]
|
|
if errors:
|
|
error_lines = []
|
|
for r in errors:
|
|
if r["current_error"]:
|
|
error_lines.append(f"- {r['name']}: current version error")
|
|
if r["latest_error"]:
|
|
error_lines.append(f"- {r['name']}: latest version error")
|
|
matrix_sections.append("Errors:\n" + "\n".join(error_lines))
|
|
|
|
if matrix_sections:
|
|
send_matrix_message("\n\n".join(matrix_sections), args.timeout)
|
|
|
|
send_uptime_kuma_ping(args.timeout)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|