feat: Add AI-driven auto-update & restructure into package
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.
This commit is contained in:
parent
f52ff222f0
commit
f55f3d8e91
36 changed files with 2612 additions and 821 deletions
86
update_monitor/checker.py
Normal file
86
update_monitor/checker.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import json
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from .comparator import compare_versions
|
||||
from .extractor import build_upstream_fallback, extract_version
|
||||
from .fetcher import authenticate_service, build_headers, fetch_response, fetch_socket_info
|
||||
from .models import ServiceConfig
|
||||
|
||||
|
||||
def check_service(service: ServiceConfig, timeout: float, user_agent: str) -> Dict[str, Any]:
|
||||
result: Dict[str, Any] = {
|
||||
"name": service.name,
|
||||
"current": None,
|
||||
"latest": None,
|
||||
"current_error": None,
|
||||
"latest_error": None,
|
||||
"upstream_url": service.upstream_latest_version_url,
|
||||
"release_notes": None,
|
||||
}
|
||||
|
||||
if service.current_version_url and service.current_version_extract:
|
||||
try:
|
||||
headers = build_headers(service.current_version_headers)
|
||||
if service.login:
|
||||
login_cookie = authenticate_service(service, timeout, user_agent)
|
||||
if login_cookie:
|
||||
headers.setdefault("Cookie", login_cookie)
|
||||
body, response_headers = fetch_response(service.current_version_url, timeout, user_agent, headers)
|
||||
result["current"] = extract_version(body, service.current_version_extract, response_headers)
|
||||
except Exception as exc:
|
||||
result["current_error"] = str(exc)
|
||||
elif service.login_socket:
|
||||
try:
|
||||
result["current"] = fetch_socket_info(service, timeout)
|
||||
except Exception as exc:
|
||||
result["current_error"] = str(exc)
|
||||
|
||||
if service.upstream_latest_version_url and service.upstream_latest_extract:
|
||||
headers = build_headers(service.upstream_latest_headers)
|
||||
try:
|
||||
body, response_headers = fetch_response(
|
||||
service.upstream_latest_version_url, timeout, user_agent, headers
|
||||
)
|
||||
result["latest"] = extract_version(body, service.upstream_latest_extract, response_headers)
|
||||
if result["latest"] is None:
|
||||
raise ValueError("Latest version extraction returned empty")
|
||||
try:
|
||||
parsed = json.loads(body)
|
||||
if isinstance(parsed, dict):
|
||||
result["release_notes"] = parsed.get("body") or None
|
||||
except (json.JSONDecodeError, AttributeError):
|
||||
pass
|
||||
except Exception as exc:
|
||||
fallback = build_upstream_fallback(service.upstream_latest_version_url)
|
||||
if fallback:
|
||||
try:
|
||||
body, response_headers = fetch_response(fallback["url"], timeout, user_agent, headers)
|
||||
result["latest"] = extract_version(body, fallback["extract"], response_headers)
|
||||
except Exception as fallback_exc:
|
||||
result["latest_error"] = str(fallback_exc)
|
||||
else:
|
||||
result["latest_error"] = str(exc)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def build_summary(results: List[Dict[str, Any]]) -> Tuple[str, bool]:
|
||||
updates = [r for r in results if compare_versions(r["current"], r["latest"]) == 1]
|
||||
errors = [r for r in results if r["current_error"] or r["latest_error"]]
|
||||
|
||||
lines = []
|
||||
if updates:
|
||||
lines.append("Updates available:")
|
||||
for r in updates:
|
||||
lines.append(f"- {r['name']}: {r['current'] or 'unknown'} -> {r['latest'] or 'unknown'}")
|
||||
if errors:
|
||||
lines.append("Errors:")
|
||||
for r in errors:
|
||||
if r["current_error"]:
|
||||
lines.append(f"- {r['name']}: current version error")
|
||||
if r["latest_error"]:
|
||||
lines.append(f"- {r['name']}: latest version error")
|
||||
|
||||
if not lines:
|
||||
return "All services up to date.", False
|
||||
return "\n".join(lines), True
|
||||
Loading…
Add table
Add a link
Reference in a new issue