mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
feat(update): let the ref updater see the addon declarations
collect_entries walked roles/*/meta/services.yml only, so a repository and ref declared in an addon file was invisible to it. That mattered once84a4b04f0made meta/addons the one place a Discourse plugin is pinned: the pins would have left the updater's view entirely. The other updater cannot stand in, utils/update/docker.py reaches version: through iter_role_images and skips any service without an image:.e317f08ac9built the addon contract with its own catalog-driven SPOT in utils/update/addons.py and left repository.py untouched without saying why, so this widens the ref path rather than replacing the catalog one; an addon can be maintained by either. The per-file work moves into _entries_of_file so services.yml and every meta/addons/*.yml share it. Addon entries carry an ("addons", <stem>) prefix on their entity path, which keeps them distinguishable from a service entry holding the same ref. The line index, the semver filter and the per-file "# nocheck: repository-version" suppression apply unchanged. Two tests cover it: an addon ref is collected beside a service ref with its own path and config_path, and a suppressed addon ref yields nothing. Against this repository the extension currently collects nothing, because every addon ref is a branch and is_semver rejects those; the tests are what prove the walk rather than the absence of output. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
84a4b04f05
commit
e4141cb826
2 files changed
+101
-31
No files matched your search
@@ -9,6 +9,7 @@ from utils.update.repository import (
|
||||
RepositoryRefEntry,
|
||||
RepositoryRefUpdate,
|
||||
apply_updates,
|
||||
collect_entries,
|
||||
suppressed_ref_lines,
|
||||
update_config_refs,
|
||||
walk_repo_ref_pairs,
|
||||
@@ -216,5 +217,50 @@ class TestApplyUpdates(unittest.TestCase):
|
||||
self.assertEqual(read_text(str(config_b)), " ref: v2.1.0\n")
|
||||
|
||||
|
||||
class TestCollectEntriesCoversAddons(unittest.TestCase):
|
||||
def _role(self, root: Path) -> Path:
|
||||
role = root / "roles" / "web-app-example"
|
||||
(role / "meta" / "addons").mkdir(parents=True)
|
||||
return role
|
||||
|
||||
def test_addon_ref_is_collected_beside_the_service_ref(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
root = Path(tmpdir)
|
||||
role = self._role(root)
|
||||
(role / "meta" / "services.yml").write_text(
|
||||
"app:\n"
|
||||
" repository: https://example.test/app.git\n"
|
||||
" ref: v1.0.0\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(role / "meta" / "addons" / "plug.yml").write_text(
|
||||
"config:\n"
|
||||
" repository: https://example.test/plug.git\n"
|
||||
" ref: v2.0.0\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
by_ref = {entry.ref: entry for entry in collect_entries(root)}
|
||||
|
||||
self.assertEqual(set(by_ref), {"v1.0.0", "v2.0.0"})
|
||||
addon = by_ref["v2.0.0"]
|
||||
self.assertEqual(addon.entity_path[:2], ("addons", "plug"))
|
||||
self.assertEqual(addon.config_path.name, "plug.yml")
|
||||
|
||||
def test_addon_ref_honours_its_own_suppression_marker(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
root = Path(tmpdir)
|
||||
role = self._role(root)
|
||||
(role / "meta" / "addons" / "plug.yml").write_text(
|
||||
"config:\n"
|
||||
" repository: https://example.test/plug.git\n"
|
||||
" # nocheck: repository-version\n"
|
||||
" ref: v2.0.0\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
self.assertEqual(collect_entries(root), [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+55
-31
@@ -30,7 +30,7 @@ from typing import TYPE_CHECKING
|
||||
from utils.annotations.suppress import is_suppressed_at
|
||||
from utils.cache.files import read_text
|
||||
from utils.cache.yaml import load_yaml_any
|
||||
from utils.roles.mapping import ROLE_FILE_META_SERVICES
|
||||
from utils.roles.mapping import ROLE_DIR_META_ADDONS, ROLE_FILE_META_SERVICES
|
||||
from utils.update.base import (
|
||||
is_semver,
|
||||
latest_semver,
|
||||
@@ -148,43 +148,67 @@ def suppressed_ref_lines(config_path: Path) -> set[int]:
|
||||
return suppressed
|
||||
|
||||
|
||||
def _entries_of_file(
|
||||
role_name: str, config_path: Path, entity_prefix: tuple[str, ...]
|
||||
) -> list[RepositoryRefEntry]:
|
||||
"""Return the unsuppressed semver ``(repository, ref)`` entries of one
|
||||
declaration file.
|
||||
|
||||
Args:
|
||||
role_name: role the file belongs to, carried into each entry.
|
||||
config_path: YAML file to read.
|
||||
entity_prefix: path segments prepended to the entity path, so an
|
||||
addon's entries stay distinguishable from a service's.
|
||||
"""
|
||||
data = load_yaml_any(str(config_path), default_if_missing=None)
|
||||
if not isinstance(data, dict):
|
||||
return []
|
||||
|
||||
ref_line_index: dict[str, list[int]] = {}
|
||||
for line_no, ref_value in _ref_lines(read_text(str(config_path)).splitlines()):
|
||||
ref_line_index.setdefault(ref_value, []).append(line_no)
|
||||
|
||||
suppressed = suppressed_ref_lines(config_path)
|
||||
entries: list[RepositoryRefEntry] = []
|
||||
for entity_path, repo, ref in walk_repo_ref_pairs(data, entity_prefix):
|
||||
if not is_semver(ref):
|
||||
continue
|
||||
candidates = ref_line_index.get(ref) or []
|
||||
if not candidates:
|
||||
continue
|
||||
line_no = candidates.pop(0)
|
||||
if line_no in suppressed:
|
||||
continue
|
||||
entries.append(
|
||||
RepositoryRefEntry(
|
||||
role=role_name,
|
||||
entity_path=entity_path,
|
||||
repository=repo,
|
||||
ref=ref,
|
||||
config_path=config_path,
|
||||
line=line_no,
|
||||
)
|
||||
)
|
||||
return entries
|
||||
|
||||
|
||||
def collect_entries(repo_root: Path) -> list[RepositoryRefEntry]:
|
||||
"""Return every semver-checkable ``(repository, ref)`` declaration
|
||||
under ``roles/*/meta/services.yml`` whose ref is not suppressed."""
|
||||
under ``roles/*/meta/services.yml`` and ``roles/*/meta/addons/*.yml``
|
||||
whose ref is not suppressed."""
|
||||
roles_root = repo_root / "roles"
|
||||
entries: list[RepositoryRefEntry] = []
|
||||
for role_dir in sorted(p for p in roles_root.iterdir() if p.is_dir()):
|
||||
services_path = role_dir / ROLE_FILE_META_SERVICES
|
||||
if not services_path.is_file():
|
||||
continue
|
||||
data = load_yaml_any(str(services_path), default_if_missing=None)
|
||||
if not isinstance(data, dict):
|
||||
continue
|
||||
if services_path.is_file():
|
||||
entries += _entries_of_file(role_dir.name, services_path, ())
|
||||
|
||||
raw = read_text(str(services_path))
|
||||
ref_line_index: dict[str, list[int]] = {}
|
||||
for line_no, ref_value in _ref_lines(raw.splitlines()):
|
||||
ref_line_index.setdefault(ref_value, []).append(line_no)
|
||||
|
||||
suppressed = suppressed_ref_lines(services_path)
|
||||
for entity_path, repo, ref in walk_repo_ref_pairs(data, ()):
|
||||
if not is_semver(ref):
|
||||
continue
|
||||
candidates = ref_line_index.get(ref) or []
|
||||
if not candidates:
|
||||
continue
|
||||
line_no = candidates.pop(0)
|
||||
if line_no in suppressed:
|
||||
continue
|
||||
entries.append(
|
||||
RepositoryRefEntry(
|
||||
role=role_dir.name,
|
||||
entity_path=entity_path,
|
||||
repository=repo,
|
||||
ref=ref,
|
||||
config_path=services_path,
|
||||
line=line_no,
|
||||
)
|
||||
addons_dir = role_dir / ROLE_DIR_META_ADDONS
|
||||
if not addons_dir.is_dir():
|
||||
continue
|
||||
for addon_path in sorted(addons_dir.glob("*.yml")):
|
||||
entries += _entries_of_file(
|
||||
role_dir.name, addon_path, ("addons", addon_path.stem)
|
||||
)
|
||||
return entries
|
||||
|
||||
|
||||
Reference in new issue
Block a user