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
71
update_monitor/comparator.py
Normal file
71
update_monitor/comparator.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
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"])
|
||||
Loading…
Add table
Add a link
Reference in a new issue