Files
computer-playbook/utils/update/base.py
T
kevinveenbirkenbachandClaude Opus 5.5 89b95d751c fix(update): stop bumping pins of eol roles
The update bot opened a version PR for web-app-jira (#821) although the role is end of life: none of the updaters looked at the lifecycle. utils.update.base.is_maintained now answers that question once, and the collectors for image versions, declared version sources, pip pins, repository refs and the addon check skip a role whose lifecycle is eol.

The lint that requires every semver pin to be watched shares that scope, otherwise it would report the pins of the eol roles as unwatched. Every other stage, deprecated included, stays in the update scope.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 01:09:10 +02:00

108 lines
3.7 KiB
Python

"""Semver primitives shared by every version-bump backend.
A "version" here is a tag of the shape
``(<channel>-)?v?<numeric-semver><patch>?(-<flavor>)?``, where the numeric part
has 1 to 5 dot-separated components, the optional leading ``<channel>-`` is a
release line written before the number (e.g. the LiteLLM tag
``main-v1.77.7-stable`` or the Jitsi tag ``stable-9646``), the optional
``<patch>`` is a vendor counter written as a letter and a number (e.g. the
Checkmk tag ``2.4.0p32``), and the optional ``-<flavor>`` suffix is an opaque
discriminator (e.g. the Docker Official Image tag ``5.4.5-php8.3-apache`` or
the npm-style ``1.2.3-alpha``).
Upgrade candidates MUST share the same depth (1 to 5 components) AND the same
flavor as the current tag, so that ``5.4.5-php8.3-apache`` never silently bumps
to ``5.4.6-php8.4-apache`` (different runtime) or to ``5.4.6`` (different
depth). Everything that is not the number folds into the flavor: the leading
channel, the trailing suffix and the patch letter. That is what keeps
``main-v1.77.7-stable`` away from ``main-v1.77.7-nightly``, ``stable-9646``
away from a bare ``9646``, and ``2.4.0p32`` ordering against ``2.4.0p33``
rather than against the four-component ``2.4.0.32``. Only the patch number
joins the ordering key, so ``p9`` sorts below ``p10``.
"""
from __future__ import annotations
import os
import re
from typing import TYPE_CHECKING
from utils.roles.meta_lookup import get_role_lifecycle
if TYPE_CHECKING:
from pathlib import Path
UNMAINTAINED_LIFECYCLES = frozenset({"eol"})
_SEMVER_CORE = r"v?\d+(?:\.\d+){0,4}"
_VERSIONED_TAG_RE = re.compile(
rf"^(?P<channel>[A-Za-z][A-Za-z0-9]*-)?(?P<semver>{_SEMVER_CORE})"
rf"(?P<patch>[A-Za-z]\d+)?(?P<flavor>-\S+)?$"
)
def _parse_versioned_tag(tag: str) -> tuple[str, str, int | None] | None:
match = _VERSIONED_TAG_RE.match(str(tag).strip())
if match is None:
return None
patch = match.group("patch")
flavor = (
(match.group("channel") or "")
+ (match.group("flavor") or "")
+ (patch[0] if patch else "")
)
return match.group("semver"), flavor, int(patch[1:]) if patch else None
def is_semver(value: str) -> bool:
return _parse_versioned_tag(value) is not None
def version_key(tag: str) -> tuple[int, ...]:
parsed = _parse_versioned_tag(tag)
if parsed is None:
return (0,) * 4
semver, _flavor, patch = parsed
parts = tuple(int(part) for part in semver.lstrip("v").split("."))
padded = parts + (0,) * (4 - len(parts))
return padded if patch is None else (*padded, patch)
def version_depth(tag: str) -> int:
parsed = _parse_versioned_tag(tag)
if parsed is None:
return 0
semver, _flavor, _patch = parsed
return len(semver.lstrip("v").split("."))
def version_flavor(tag: str) -> str:
"""Return the ``-<flavor>`` suffix of a versioned tag, or "" when none."""
parsed = _parse_versioned_tag(tag)
return parsed[1] if parsed else ""
def latest_semver(tags: list[str], depth: int, flavor: str = "") -> str | None:
candidates = [
tag
for tag in tags
if is_semver(tag)
and version_depth(tag) == depth
and version_flavor(tag) == flavor
]
return max(candidates, key=version_key, default=None)
def resolve_max_fetch_workers() -> int:
return int(os.environ["INFINITO_WORKER_FETCH"])
def is_maintained(roles_root: Path, role: str) -> bool:
"""Return whether the version-bump backends watch a role.
Args:
roles_root: the ``roles/`` directory that holds the role.
role: role name, the directory name under ``roles_root``.
"""
lifecycle = get_role_lifecycle(roles_root / role, role_name=role)
return lifecycle not in UNMAINTAINED_LIFECYCLES