fix(variants): apply a variant pin only while its carrier is in the round

Pins of every role applied at every round index, so a role the round did not deploy could pull a partner into a heavier variant (web-app-discourse#1 measured 35.46 GB against the 32 GB reservation cap). resolve_round now starts every role on its clamped index and recomputes the include and the pins in force until the assignment settles, raising VariantPinError on a cycle. The planner resolves each round once (39.1 s to 16.0 s for all rounds), affected judges every (role, index) with the round the planner deploys for it, and the resource budget lint measures the pin-aware rounds. The stale web-app-matomo pin above the primary-only docs entry is dropped, and test_variant_pin_fixed_point.py guards every planned round against unsettled pins and pins above a disabled entry. docs/contributing/artefact/files/role/variants.md explains the settle loop with diagrams.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5.5 committed 2026-10-31 12:45:53 +01:00
1 parent 8127d61018
commit 76fc6992c4
10 files changed
+935 -173

No files matched your search

@@ -21,11 +21,6 @@ class TestPlanDevInventoryMatrix(unittest.TestCase):
"cli.administration.deploy.development.inventory.legacy_resolver._resolve_round_include",
autospec=True,
)
@patch(
"cli.administration.deploy.development.inventory.legacy_resolver._build_services_overrides_for_round",
autospec=True,
return_value={},
)
@patch(
"cli.administration.deploy.development.inventory.planner.get_variants",
autospec=True,
@@ -33,7 +28,6 @@ class TestPlanDevInventoryMatrix(unittest.TestCase):
def test_single_variant_apps_share_one_unsuffixed_folder(
self,
get_variants_mock: MagicMock,
_overrides_mock: MagicMock,
resolve_include_mock: MagicMock,
) -> None:
get_variants_mock.return_value = {
@@ -63,11 +57,6 @@ class TestPlanDevInventoryMatrix(unittest.TestCase):
"cli.administration.deploy.development.inventory.legacy_resolver._resolve_round_include",
autospec=True,
)
@patch(
"cli.administration.deploy.development.inventory.legacy_resolver._build_services_overrides_for_round",
autospec=True,
return_value={},
)
@patch(
"cli.administration.deploy.development.inventory.planner.get_variants",
autospec=True,
@@ -75,7 +64,6 @@ class TestPlanDevInventoryMatrix(unittest.TestCase):
def test_two_variant_primary_drives_two_suffixed_folders(
self,
get_variants_mock: MagicMock,
_overrides_mock: MagicMock,
resolve_include_mock: MagicMock,
) -> None:
get_variants_mock.return_value = {
@@ -115,11 +103,6 @@ class TestPlanDevInventoryMatrix(unittest.TestCase):
"cli.administration.deploy.development.inventory.legacy_resolver._resolve_round_include",
autospec=True,
)
@patch(
"cli.administration.deploy.development.inventory.legacy_resolver._build_services_overrides_for_round",
autospec=True,
return_value={},
)
@patch(
"cli.administration.deploy.development.inventory.planner.get_variants",
autospec=True,
@@ -127,7 +110,6 @@ class TestPlanDevInventoryMatrix(unittest.TestCase):
def test_round_count_driven_by_primary_max_variants(
self,
get_variants_mock: MagicMock,
_overrides_mock: MagicMock,
resolve_include_mock: MagicMock,
) -> None:
get_variants_mock.return_value = {
@@ -175,11 +157,6 @@ class TestPlanDevInventoryMatrix(unittest.TestCase):
"cli.administration.deploy.development.inventory.legacy_resolver._resolve_round_include",
autospec=True,
)
@patch(
"cli.administration.deploy.development.inventory.legacy_resolver._build_services_overrides_for_round",
autospec=True,
return_value={},
)
@patch(
"cli.administration.deploy.development.inventory.planner.get_variants",
autospec=True,
@@ -187,7 +164,6 @@ class TestPlanDevInventoryMatrix(unittest.TestCase):
def test_purge_set_is_union_when_variants_differ(
self,
get_variants_mock: MagicMock,
_overrides_mock: MagicMock,
resolve_include_mock: MagicMock,
) -> None:
get_variants_mock.return_value = {
@@ -266,11 +242,6 @@ class TestPlanBakesPinnedVariant(unittest.TestCase):
autospec=True,
return_value=("web-app-bar", "web-app-foo"),
)
@patch(
"cli.administration.deploy.development.inventory.legacy_resolver._build_services_overrides_for_round",
autospec=True,
return_value={},
)
@patch(
"cli.administration.deploy.development.inventory.planner.get_variants",
autospec=True,
@@ -278,7 +249,6 @@ class TestPlanBakesPinnedVariant(unittest.TestCase):
def test_round_variants_carry_the_pinned_index(
self,
get_variants_mock: MagicMock,
_overrides_mock: MagicMock,
_include_mock: MagicMock,
_pins_mock: MagicMock,
) -> None:
@@ -163,6 +163,61 @@ class TestAffected(unittest.TestCase):
got = affected_main.affected_selection(["web-app-keycloak"])
self.assertIn("web-app-consumer#0", got)
def test_each_variant_is_judged_with_the_pins_its_own_round_runs(self) -> None:
"""Variant 1 reaches the seed only through its own pin moving the
partner, and an unrelated role's pin would move the subject itself;
only the round the planner deploys for each variant keeps both."""
with tempfile.TemporaryDirectory() as td:
root = Path(td)
for role in (
"web-app-keycloak",
"web-app-matomo",
"web-app-consumer",
"web-app-other",
):
_mk_app_role(root, role, role)
off = {"enabled": False, "shared": False}
on = {"enabled": True, "shared": True}
dump_yaml(
root / "roles" / "web-app-matomo" / ROLE_FILE_META_SERVICES,
{"matomo": {}, "sso": off},
)
dump_yaml(
root / "roles" / "web-app-matomo" / ROLE_FILE_META_VARIANTS,
[{}, {}, {"services": {"sso": on}}],
)
dump_yaml(
root / "roles" / "web-app-consumer" / ROLE_FILE_META_SERVICES,
{"consumer": {}, "sso": on, "matomo": on},
)
(root / "roles" / "web-app-consumer" / ROLE_FILE_META_VARIANTS).write_text(
"- services:\n"
" matomo: {enabled: false, shared: false}\n"
"- services:\n"
" sso: {enabled: false, shared: false}\n"
" # variant-pin: web-app-matomo#2\n"
" matomo: {enabled: true, shared: true}\n"
"- services:\n"
" sso: {enabled: false, shared: false}\n"
" matomo: {enabled: false, shared: false}\n",
encoding="utf-8",
)
dump_yaml(
root / "roles" / "web-app-other" / ROLE_FILE_META_SERVICES,
{"other": {}, "matomo": off},
)
(root / "roles" / "web-app-other" / ROLE_FILE_META_VARIANTS).write_text(
"- services:\n"
" # variant-pin: web-app-consumer#2\n"
" matomo: {enabled: false, shared: false}\n"
"- {}\n",
encoding="utf-8",
)
with patch.object(repo_paths, "PROJECT_ROOT", root):
got = affected_main.affected_selection(["web-app-keycloak"])
self.assertIn("web-app-consumer#0,1", got)
def test_a_seed_keeps_every_variant_of_its_own_role(self) -> None:
with tempfile.TemporaryDirectory() as td:
root = Path(td)