mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 11:46:35 +00:00
Upstreams that ship several channels in parallel put the channel first: LiteLLM publishes 'main-v1.77.7-stable' next to 'main-v1.77.7-nightly', Jitsi ships 'stable-9646'. The grammar only allowed a leading 'v', so both shapes parsed as nothing at all, sat at depth 0 next to 'latest', and the bump job never looked at them. The numeric core now takes an optional leading channel, and the channel folds into the flavor the same way the patch letter already does. Everything that is not the number is family; only the number is order. That is what keeps '-stable' away from '-nightly' and 'stable-9646' away from a bare '9646', which would leave the channel and change what the role deploys. The change is additive. Only a tag starting with letters followed by a dash can match the new group, and every such tag failed the grammar outright before, so no tag that parses today parses differently. The regression case pins the four shapes that already worked, and the new cases pin the two directions that must not happen along with the tags that must stay refused: 'act-latest', 'bookworm-slim', 'main-latest' and 'buildx-stable-1' carry a channel but no version. Two pins in this repository become orderable as a result: the four Jitsi components and the LiteLLM gateway. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
90 lines
3.1 KiB
Python
90 lines
3.1 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
|
|
|
|
_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"])
|