mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 19:01:54 +00:00
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>
282 lines
9.6 KiB
Python
282 lines
9.6 KiB
Python
from __future__ import annotations
|
|
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from utils.cache.files import read_text
|
|
from utils.roles.mapping import ROLE_DIR_META_ADDONS, ROLE_FILE_META_SERVICES
|
|
from utils.update.repository import (
|
|
RepositoryRefEntry,
|
|
RepositoryRefUpdate,
|
|
apply_updates,
|
|
collect_entries,
|
|
suppressed_ref_lines,
|
|
update_config_refs,
|
|
walk_repo_ref_pairs,
|
|
)
|
|
|
|
|
|
class TestWalkRepoRefPairs(unittest.TestCase):
|
|
def test_finds_top_level_entity(self) -> None:
|
|
data = {
|
|
"bookwyrm": {
|
|
"repository": "https://github.com/bookwyrm-social/bookwyrm.git",
|
|
"ref": "v0.8.5",
|
|
},
|
|
}
|
|
|
|
pairs = list(walk_repo_ref_pairs(data, ()))
|
|
|
|
self.assertEqual(
|
|
pairs,
|
|
[
|
|
(
|
|
("bookwyrm",),
|
|
"https://github.com/bookwyrm-social/bookwyrm.git",
|
|
"v0.8.5",
|
|
),
|
|
],
|
|
)
|
|
|
|
def test_descends_into_nested_plugin_map(self) -> None:
|
|
data = {
|
|
"discourse": {
|
|
"plugins": {
|
|
"discourse-ldap-auth": {
|
|
"repository": "https://github.com/jonmbake/discourse-ldap-auth.git",
|
|
"ref": "master",
|
|
},
|
|
"discourse-prometheus": {
|
|
"repository": "https://github.com/discourse/discourse-prometheus.git",
|
|
"ref": "v1.0.0",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
pairs = list(walk_repo_ref_pairs(data, ()))
|
|
entity_paths = {pair[0] for pair in pairs}
|
|
|
|
self.assertIn(("discourse", "plugins", "discourse-ldap-auth"), entity_paths)
|
|
self.assertIn(("discourse", "plugins", "discourse-prometheus"), entity_paths)
|
|
|
|
def test_skips_when_only_repository_or_only_ref(self) -> None:
|
|
data = {
|
|
"only-repo": {"repository": "https://example.invalid/x.git"},
|
|
"only-ref": {"ref": "v1.0.0"},
|
|
"both": {
|
|
"repository": "https://example.invalid/y.git",
|
|
"ref": "v2.0.0",
|
|
},
|
|
}
|
|
|
|
pairs = list(walk_repo_ref_pairs(data, ()))
|
|
|
|
self.assertEqual(len(pairs), 1)
|
|
self.assertEqual(pairs[0][0], ("both",))
|
|
|
|
def test_walks_lists(self) -> None:
|
|
data = {
|
|
"repos": [
|
|
{
|
|
"repository": "https://example.invalid/a.git",
|
|
"ref": "v1",
|
|
},
|
|
{
|
|
"repository": "https://example.invalid/b.git",
|
|
"ref": "v2",
|
|
},
|
|
],
|
|
}
|
|
|
|
pairs = list(walk_repo_ref_pairs(data, ()))
|
|
|
|
self.assertEqual(len(pairs), 2)
|
|
self.assertEqual(pairs[0][0], ("repos", "[0]"))
|
|
self.assertEqual(pairs[1][0], ("repos", "[1]"))
|
|
|
|
|
|
class TestUpdateConfigRefs(unittest.TestCase):
|
|
def test_rewrites_target_line_and_preserves_others(self) -> None:
|
|
original = """plugin-a:
|
|
repository: https://example.invalid/a.git
|
|
ref: v1.0.0
|
|
plugin-b:
|
|
repository: https://example.invalid/b.git
|
|
ref: v2.0.0
|
|
"""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
config_path = Path(tmpdir) / "services.yml"
|
|
config_path.write_text(original, encoding="utf-8")
|
|
|
|
changed = update_config_refs(config_path, {3: "v1.1.0"})
|
|
|
|
self.assertTrue(changed)
|
|
updated = read_text(str(config_path))
|
|
self.assertIn(" ref: v1.1.0\n", updated)
|
|
self.assertIn(" ref: v2.0.0\n", updated)
|
|
|
|
def test_preserves_quoting_and_trailing_comment(self) -> None:
|
|
original = ' ref: "v0.7.5" # pin\n'
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
config_path = Path(tmpdir) / "services.yml"
|
|
config_path.write_text(original, encoding="utf-8")
|
|
|
|
changed = update_config_refs(config_path, {1: "v0.8.5"})
|
|
|
|
self.assertTrue(changed)
|
|
self.assertEqual(
|
|
read_text(str(config_path)),
|
|
' ref: "v0.8.5" # pin\n',
|
|
)
|
|
|
|
def test_no_op_when_target_value_already_matches(self) -> None:
|
|
original = " ref: v1.0.0\n"
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
config_path = Path(tmpdir) / "services.yml"
|
|
config_path.write_text(original, encoding="utf-8")
|
|
|
|
changed = update_config_refs(config_path, {1: "v1.0.0"})
|
|
|
|
self.assertFalse(changed)
|
|
self.assertEqual(read_text(str(config_path)), original)
|
|
|
|
def test_skips_out_of_range_line_numbers(self) -> None:
|
|
original = " ref: v1.0.0\n"
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
config_path = Path(tmpdir) / "services.yml"
|
|
config_path.write_text(original, encoding="utf-8")
|
|
|
|
changed = update_config_refs(config_path, {99: "v2.0.0"})
|
|
|
|
self.assertFalse(changed)
|
|
self.assertEqual(read_text(str(config_path)), original)
|
|
|
|
|
|
class TestSuppressedRefLines(unittest.TestCase):
|
|
def test_recognises_line_above_marker(self) -> None:
|
|
content = """plugin-a:
|
|
repository: https://example.invalid/a.git
|
|
# nocheck: repository-version
|
|
ref: v1.0.0
|
|
plugin-b:
|
|
repository: https://example.invalid/b.git
|
|
ref: v2.0.0
|
|
"""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
config_path = Path(tmpdir) / "services.yml"
|
|
config_path.write_text(content, encoding="utf-8")
|
|
|
|
suppressed = suppressed_ref_lines(config_path)
|
|
|
|
self.assertEqual(suppressed, {4})
|
|
|
|
|
|
class TestApplyUpdates(unittest.TestCase):
|
|
def test_groups_updates_per_config_file(self) -> None:
|
|
file_a_content = " ref: v1.0.0\n"
|
|
file_b_content = " ref: v2.0.0\n"
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
config_a = Path(tmpdir) / "a" / "services.yml"
|
|
config_b = Path(tmpdir) / "b" / "services.yml"
|
|
config_a.parent.mkdir()
|
|
config_b.parent.mkdir()
|
|
config_a.write_text(file_a_content, encoding="utf-8")
|
|
config_b.write_text(file_b_content, encoding="utf-8")
|
|
|
|
updates = [
|
|
RepositoryRefUpdate(
|
|
entry=RepositoryRefEntry(
|
|
role="role-a",
|
|
entity_path=("plugin-a",),
|
|
repository="https://example.invalid/a.git",
|
|
ref="v1.0.0",
|
|
config_path=config_a,
|
|
line=1,
|
|
),
|
|
latest="v1.1.0",
|
|
),
|
|
RepositoryRefUpdate(
|
|
entry=RepositoryRefEntry(
|
|
role="role-b",
|
|
entity_path=("plugin-b",),
|
|
repository="https://example.invalid/b.git",
|
|
ref="v2.0.0",
|
|
config_path=config_b,
|
|
line=1,
|
|
),
|
|
latest="v2.1.0",
|
|
),
|
|
]
|
|
|
|
changed = apply_updates(updates)
|
|
|
|
self.assertEqual(set(changed), {config_a, config_b})
|
|
self.assertEqual(read_text(str(config_a)), " ref: v1.1.0\n")
|
|
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 / ROLE_DIR_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 / ROLE_FILE_META_SERVICES).write_text(
|
|
"app:\n repository: https://example.test/app.git\n ref: v1.0.0\n",
|
|
encoding="utf-8",
|
|
)
|
|
(role / ROLE_DIR_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 / ROLE_DIR_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), [])
|
|
|
|
def test_refs_of_an_eol_role_are_not_collected(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
root = Path(tmpdir)
|
|
role = self._role(root)
|
|
(role / ROLE_FILE_META_SERVICES).write_text(
|
|
"example:\n"
|
|
" lifecycle: eol\n"
|
|
" repository: https://example.test/app.git\n"
|
|
" ref: v1.0.0\n",
|
|
encoding="utf-8",
|
|
)
|
|
(role / ROLE_DIR_META_ADDONS / "plug.yml").write_text(
|
|
"config:\n repository: https://example.test/plug.git\n ref: v2.0.0\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
self.assertEqual(collect_entries(root), [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|