nixpkgs-plugin-update: prefer recent GitHub release tags

Use GitHub's recent-tag GraphQL query as an ordered source of candidate tags
and pick the first acceptable release tag from that list.

This keeps the generic git fallback unchanged while avoiding the older "largest
version wins" behavior on repositories whose newest maintained release has a
lower semantic version than an older historical tag.
This commit is contained in:
Austin Horstman
2026-04-19 10:04:57 -05:00
parent ef2ef8eb64
commit 65c0f77321
@@ -125,6 +125,14 @@ def select_latest_tag(
return None
def first_release_tag(tags: list[str]) -> str | None:
for tag in tags:
if normalize_release_version(tag) is not None:
return tag
return None
class Repo:
def __init__(self, uri: str, branch: str) -> None:
self.uri = uri
@@ -318,17 +326,6 @@ class RepoGitHub(Repo):
with urllib.request.urlopen(req, timeout=10) as response:
return json.load(response)
def _extract_commit_date(self, target: dict) -> datetime | None:
commit_date_str = None
if "committedDate" in target:
commit_date_str = target["committedDate"]
elif "target" in target and "committedDate" in target["target"]:
commit_date_str = target["target"]["committedDate"]
if commit_date_str:
return datetime.fromisoformat(commit_date_str.replace("Z", "+00:00"))
return None
@retry(urllib.error.URLError, tries=4, delay=3, backoff=2)
def get_latest_tag(self) -> str | None:
try:
@@ -341,23 +338,11 @@ class RepoGitHub(Repo):
return super().get_latest_tag()
query = """
query GetLatestVersionInfo($owner: String!, $name: String!) {
query GetRecentTags($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
refs(refPrefix: "refs/tags/", first: 5, orderBy: {field: TAG_COMMIT_DATE, direction: DESC}) {
refs(refPrefix: "refs/tags/", first: 20, orderBy: {field: TAG_COMMIT_DATE, direction: DESC}) {
nodes {
name
target {
... on Commit {
committedDate
}
... on Tag {
target {
... on Commit {
committedDate
}
}
}
}
}
}
}
@@ -377,64 +362,17 @@ class RepoGitHub(Repo):
)
return None
if "data" not in data or not data["data"]:
log.warning(
"No data in GraphQL response for %s/%s", self.owner, self.repo
)
return None
repo = data["data"]["repository"]
repo = data.get("data", {}).get("repository")
if not repo:
log.debug(
"Repository %s/%s not found or inaccessible", self.owner, self.repo
)
return None
valid_versions = []
invalid_tags = []
for ref_node in repo["refs"]["nodes"]:
tag_name = ref_node["name"]
commit_date = self._extract_commit_date(ref_node["target"])
if not commit_date:
continue
try:
version = parse_version(tag_name)
valid_versions.append((tag_name, version, commit_date))
except InvalidVersion:
invalid_tags.append((tag_name, None, commit_date))
def get_version(tag_tuple):
_, version, _ = tag_tuple
return version
def get_date(tag_tuple):
_, _, date = tag_tuple
return date or datetime.min
def get_max_versions(versions, sort_key):
return max(versions, key=sort_key, default=(None, None, None))
max_valid_tag, _, max_valid_date = get_max_versions(
valid_versions, get_version
)
max_invalid_tag, _, max_invalid_date = get_max_versions(
invalid_tags, get_date
)
if max_valid_tag and max_invalid_tag:
return (
max_invalid_tag
if (max_invalid_date or datetime.min)
> (max_valid_date or datetime.min)
else max_valid_tag
)
elif max_valid_tag:
return max_valid_tag
elif max_invalid_tag:
return max_invalid_tag
else:
recent_tags = [node["name"] for node in repo["refs"]["nodes"]]
if not recent_tags:
return None
latest_tag = first_release_tag(recent_tags)
return latest_tag if latest_tag is not None else recent_tags[0]
except Exception as e:
log.warning(
"Error fetching version info for %s/%s: %s",