This commit is contained in:
2026-01-16 16:17:43 +00:00
parent d6347e7e59
commit 50587ffbbd

View File

@@ -6,12 +6,11 @@ import xml.etree.ElementTree as ET
from dataclasses import dataclass from dataclasses import dataclass
from urllib.parse import quote_plus, urlparse from urllib.parse import quote_plus, urlparse
from packaging.version import InvalidVersion, Version
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
# Home Assistant includes "packaging"
from packaging.version import InvalidVersion, Version
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
UA = "BahmcloudStore (Home Assistant)" UA = "BahmcloudStore (Home Assistant)"
@@ -112,13 +111,9 @@ def _semver_key(tag: str) -> Version | None:
return None return None
def _pick_highest_semver(candidates: list[str]) -> str | None: def _pick_highest_semver(tags: list[str]) -> str | None:
"""Pick highest semantic version from a list of tag strings.
If no semver parseable tags exist, return None.
"""
parsed: list[tuple[Version, str]] = [] parsed: list[tuple[Version, str]] = []
for t in candidates: for t in tags:
if not isinstance(t, str): if not isinstance(t, str):
continue continue
ts = t.strip() ts = t.strip()
@@ -130,7 +125,6 @@ def _pick_highest_semver(candidates: list[str]) -> str | None:
if not parsed: if not parsed:
return None return None
parsed.sort(key=lambda x: x[0], reverse=True) parsed.sort(key=lambda x: x[0], reverse=True)
return parsed[0][1] return parsed[0][1]
@@ -199,17 +193,16 @@ async def _github_latest_version_api(hass: HomeAssistant, owner: str, repo: str)
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
headers = {"Accept": "application/vnd.github+json", "User-Agent": UA} headers = {"Accept": "application/vnd.github+json", "User-Agent": UA}
# 1) Preferred: GitHub latest release endpoint (already good)
data, status = await _safe_json(session, f"https://api.github.com/repos/{owner}/{repo}/releases/latest", headers=headers) data, status = await _safe_json(session, f"https://api.github.com/repos/{owner}/{repo}/releases/latest", headers=headers)
if isinstance(data, dict) and data.get("tag_name"): if isinstance(data, dict) and data.get("tag_name"):
return str(data["tag_name"]), "release" return str(data["tag_name"]), "release"
# 2) If no releases: fetch multiple tags and select highest semver # No releases -> pick highest semver from many tags (instead of per_page=1)
if status == 404: if status == 404:
tags_data, _ = await _safe_json(session, f"https://api.github.com/repos/{owner}/{repo}/tags?per_page=100", headers=headers) data, _ = await _safe_json(session, f"https://api.github.com/repos/{owner}/{repo}/tags?per_page=100", headers=headers)
tags: list[str] = [] tags: list[str] = []
if isinstance(tags_data, list): if isinstance(data, list):
for t in tags_data: for t in data:
if isinstance(t, dict) and t.get("name"): if isinstance(t, dict) and t.get("name"):
tags.append(str(t["name"])) tags.append(str(t["name"]))
@@ -217,7 +210,7 @@ async def _github_latest_version_api(hass: HomeAssistant, owner: str, repo: str)
if best: if best:
return best, "tag" return best, "tag"
# fallback: keep old behavior (first tag, if any) # fallback: keep old behavior (first tag)
if tags: if tags:
return tags[0], "tag" return tags[0], "tag"
@@ -225,7 +218,6 @@ async def _github_latest_version_api(hass: HomeAssistant, owner: str, repo: str)
async def _github_latest_version(hass: HomeAssistant, owner: str, repo: str) -> tuple[str | None, str | None]: async def _github_latest_version(hass: HomeAssistant, owner: str, repo: str) -> tuple[str | None, str | None]:
# Redirect is very robust if releases exist
tag, src = await _github_latest_version_redirect(hass, owner, repo) tag, src = await _github_latest_version_redirect(hass, owner, repo)
if tag: if tag:
return tag, src return tag, src
@@ -240,26 +232,25 @@ async def _github_latest_version(hass: HomeAssistant, owner: str, repo: str) ->
async def _gitea_latest_version(hass: HomeAssistant, base: str, owner: str, repo: str) -> tuple[str | None, str | None]: async def _gitea_latest_version(hass: HomeAssistant, base: str, owner: str, repo: str) -> tuple[str | None, str | None]:
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
# Fetch multiple releases, pick highest semver by tag_name # releases: fetch multiple, pick highest semver (instead of limit=1)
rels, _ = await _safe_json(session, f"{base}/api/v1/repos/{owner}/{repo}/releases?limit=50") data, _ = await _safe_json(session, f"{base}/api/v1/repos/{owner}/{repo}/releases?limit=50")
release_tags: list[str] = [] rel_tags: list[str] = []
if isinstance(rels, list): if isinstance(data, list):
for r in rels: for r in data:
if isinstance(r, dict) and r.get("tag_name"): if isinstance(r, dict) and r.get("tag_name"):
release_tags.append(str(r["tag_name"])) rel_tags.append(str(r["tag_name"]))
best_rel = _pick_highest_semver(release_tags) best_rel = _pick_highest_semver(rel_tags)
if best_rel: if best_rel:
return best_rel, "release" return best_rel, "release"
if release_tags: if rel_tags:
# fallback: first release tag return rel_tags[0], "release"
return release_tags[0], "release"
# No releases: fetch multiple tags, pick highest semver # tags: fetch multiple, pick highest semver (instead of limit=1)
tags_data, _ = await _safe_json(session, f"{base}/api/v1/repos/{owner}/{repo}/tags?limit=50") data, _ = await _safe_json(session, f"{base}/api/v1/repos/{owner}/{repo}/tags?limit=50")
tags: list[str] = [] tags: list[str] = []
if isinstance(tags_data, list): if isinstance(data, list):
for t in tags_data: for t in data:
if isinstance(t, dict) and t.get("name"): if isinstance(t, dict) and t.get("name"):
tags.append(str(t["name"])) tags.append(str(t["name"]))
@@ -280,25 +271,25 @@ async def _gitlab_latest_version(
project = quote_plus(f"{owner}/{repo}") project = quote_plus(f"{owner}/{repo}")
# Fetch multiple releases, pick highest semver by tag_name # releases: fetch multiple, pick highest semver (instead of per_page=1)
rels, _ = await _safe_json(session, f"{base}/api/v4/projects/{project}/releases?per_page=50", headers=headers) data, _ = await _safe_json(session, f"{base}/api/v4/projects/{project}/releases?per_page=50", headers=headers)
release_tags: list[str] = [] rel_tags: list[str] = []
if isinstance(rels, list): if isinstance(data, list):
for r in rels: for r in data:
if isinstance(r, dict) and r.get("tag_name"): if isinstance(r, dict) and r.get("tag_name"):
release_tags.append(str(r["tag_name"])) rel_tags.append(str(r["tag_name"]))
best_rel = _pick_highest_semver(release_tags) best_rel = _pick_highest_semver(rel_tags)
if best_rel: if best_rel:
return best_rel, "release" return best_rel, "release"
if release_tags: if rel_tags:
return release_tags[0], "release" return rel_tags[0], "release"
# No releases: fetch multiple tags, pick highest semver # tags: fetch multiple, pick highest semver (instead of per_page=1)
tags_data, _ = await _safe_json(session, f"{base}/api/v4/projects/{project}/repository/tags?per_page=50", headers=headers) data, _ = await _safe_json(session, f"{base}/api/v4/projects/{project}/repository/tags?per_page=50", headers=headers)
tags: list[str] = [] tags: list[str] = []
if isinstance(tags_data, list): if isinstance(data, list):
for t in tags_data: for t in data:
if isinstance(t, dict) and t.get("name"): if isinstance(t, dict) and t.get("name"):
tags.append(str(t["name"])) tags.append(str(t["name"]))
@@ -308,7 +299,7 @@ async def _gitlab_latest_version(
if tags: if tags:
return tags[0], "tag" return tags[0], "tag"
# last fallback: atom (often newest by date, not semver) # atom fallback
atom, status = await _safe_text(session, f"{base}/{owner}/{repo}/-/tags?format=atom", headers=headers) atom, status = await _safe_text(session, f"{base}/{owner}/{repo}/-/tags?format=atom", headers=headers)
if status == 200 and atom: if status == 200 and atom:
try: try: