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.
71 lines
2.2 KiB
Python
71 lines
2.2 KiB
Python
import re
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
|
|
def normalize_version(raw: Optional[str]) -> Optional[str]:
|
|
if not raw:
|
|
return None
|
|
return raw.strip().lstrip("v")
|
|
|
|
|
|
def parse_version(value: str) -> Optional[Dict[str, Any]]:
|
|
match = re.match(
|
|
r"^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$",
|
|
value,
|
|
)
|
|
if not match:
|
|
return None
|
|
major, minor, patch, prerelease, _build = match.groups()
|
|
prerelease_parts: Optional[List[Tuple]] = None
|
|
if prerelease:
|
|
prerelease_parts = []
|
|
for part in prerelease.split("."):
|
|
if part.isdigit():
|
|
prerelease_parts.append((True, int(part)))
|
|
else:
|
|
prerelease_parts.append((False, part))
|
|
return {
|
|
"major": int(major),
|
|
"minor": int(minor),
|
|
"patch": int(patch),
|
|
"prerelease": prerelease_parts,
|
|
}
|
|
|
|
|
|
def _compare_prerelease(a: Optional[list], b: Optional[list]) -> int:
|
|
if a is None and b is None:
|
|
return 0
|
|
if a is None:
|
|
return -1
|
|
if b is None:
|
|
return 1
|
|
for (a_is_num, a_val), (b_is_num, b_val) in zip(a, b):
|
|
if a_is_num and b_is_num:
|
|
if a_val != b_val:
|
|
return 1 if a_val > b_val else -1
|
|
elif a_is_num != b_is_num:
|
|
return -1 if a_is_num else 1
|
|
else:
|
|
if a_val != b_val:
|
|
return 1 if a_val > b_val else -1
|
|
if len(a) == len(b):
|
|
return 0
|
|
return 1 if len(a) > len(b) else -1
|
|
|
|
|
|
def compare_versions(current: Optional[str], latest: Optional[str]) -> Optional[int]:
|
|
current_norm = normalize_version(current)
|
|
latest_norm = normalize_version(latest)
|
|
if not current_norm or not latest_norm:
|
|
return None
|
|
current_v = parse_version(current_norm)
|
|
latest_v = parse_version(latest_norm)
|
|
if current_v is None or latest_v is None:
|
|
return None
|
|
c = (current_v["major"], current_v["minor"], current_v["patch"])
|
|
l = (latest_v["major"], latest_v["minor"], latest_v["patch"])
|
|
if l > c:
|
|
return 1
|
|
if l < c:
|
|
return -1
|
|
return _compare_prerelease(current_v["prerelease"], latest_v["prerelease"])
|