mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
The version-source resolver orders upstream versions by semver. A pin that is not one, such as `latest` or a branch name, has no depth, so no candidate ever matches it and the declaration never moves anything. Nothing reported that: the watched-pins lint only looks at values that already are a semver. test_version_keys now fails on an `update:` block whose pinned key does not hold a semver. A monitored addon the resolver does not read is held to the same rule as soon as it pins a `version`, because the catalog check skips a non-semver pin just as silently. An addon without a `version` keeps tracking the app default and is left alone. The Stalwart README no longer asks for `webui_version` to be bumped by hand next to the image tag. Its `update:` block names the upstream tags, the server itself follows `releases/latest` of the bundle, and the resolver plans v1.0.5 to v1.0.11 for it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
91 lines
3.4 KiB
Python
91 lines
3.4 KiB
Python
"""A version only lives behind a key that says it is one.
|
|
|
|
``version`` is the tag of the entity's own image, ``<what>_version`` any other
|
|
upstream version it pins, and ``ref`` the git ref of the repository updater.
|
|
A semver behind ``release``, ``tag`` or a name invented once is invisible to
|
|
the three updaters and to everyone reading the file, which is how pins go
|
|
stale unnoticed.
|
|
|
|
``update:`` blocks are checked too: a block naming a key the entity does not
|
|
carry, a type the resolver does not know, or a pin that is not a semver moves
|
|
nothing and says nothing. The updaters order upstream versions by semver, so a
|
|
moving tag behind an ``update:`` block, or as the ``version`` of a monitored
|
|
addon, fails.
|
|
|
|
Keys that hold a dotted number without naming a version are listed in
|
|
``_NOT_VERSIONS``: the container's ``cpus`` and ``bond`` shares and the
|
|
``upstream_host`` address.
|
|
|
|
Per-line opt-out: ``# nocheck: version-key`` above the pin, with a reason.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import unittest
|
|
|
|
from utils.annotations.suppress import is_suppressed_at
|
|
from utils.cache.files import read_text
|
|
from utils.cache.yaml import load_yaml
|
|
from utils.roles.mapping import ROLE_FILE_META_SERVICES
|
|
from utils.update.base import is_semver
|
|
from utils.update.source import invalid_declarations
|
|
|
|
from . import PROJECT_ROOT
|
|
|
|
_RULE = "version-key"
|
|
_VALID_KEY = re.compile(r"^(version|[a-z0-9_]+_version|ref)$")
|
|
_NOT_VERSIONS = frozenset({"cpus", "bond", "upstream_host"})
|
|
|
|
|
|
def _findings() -> list[str]:
|
|
findings: list[str] = []
|
|
for config_path in sorted(
|
|
(PROJECT_ROOT / "roles").glob(f"*/{ROLE_FILE_META_SERVICES}")
|
|
):
|
|
role = config_path.parts[-3]
|
|
lines = read_text(str(config_path)).splitlines()
|
|
for entity, config in (load_yaml(str(config_path)) or {}).items():
|
|
if not isinstance(config, dict):
|
|
continue
|
|
for key, value in config.items():
|
|
name, text = str(key), str(value)
|
|
if name in _NOT_VERSIONS or _VALID_KEY.match(name):
|
|
continue
|
|
if "." not in text or not is_semver(text):
|
|
continue
|
|
number = next(
|
|
(
|
|
index
|
|
for index, line in enumerate(lines, start=1)
|
|
if re.match(rf"^\s+{re.escape(name)}\s*:", line)
|
|
),
|
|
0,
|
|
)
|
|
if number and is_suppressed_at(lines, number, _RULE):
|
|
continue
|
|
findings.append(f"- {role}/{entity}.{name} = {text}")
|
|
return sorted(set(findings))
|
|
|
|
|
|
class TestVersionKeys(unittest.TestCase):
|
|
def test_a_version_sits_behind_version_or_its_suffix(self) -> None:
|
|
findings = _findings()
|
|
self.assertEqual(
|
|
findings,
|
|
[],
|
|
"These values look like a version but their key does not say so. "
|
|
"Name the key `version` for the entity's own image tag, "
|
|
"`<what>_version` for any other upstream version, `<what>_tag` for "
|
|
f"a locally built image, or mark it `# nocheck: {_RULE}` with a "
|
|
"reason:\n" + "\n".join(findings),
|
|
)
|
|
|
|
def test_every_declared_update_source_can_move_a_semver_pin(self) -> None:
|
|
problems = invalid_declarations(PROJECT_ROOT)
|
|
self.assertEqual(problems, [], "\n".join(problems))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|