fix(deploy): let init pin the guide host the way deploy already does

adb53392bd taught the deploy planner to pin the instructions host to its
smallest variant and to leave a pinned app out of the round total. It taught
only the deploy side. `init` writes the inventory and `deploy` reads it back,
and the directory carries a round suffix only when the plan holds more than
one round, so the two sides named different directories:

    >>> Inventory initialized at /home/runner/inventories/github-0
    === matrix-deploy: round 1/1 inv=/home/runner/inventories/github
    [ERROR] Some application_ids are invalid for this inventory
    [DEBUG] inventory file does not exist

Nine jobs of run 36596159856 died there, before ansible-playbook started. The
trigger is a subject with a single variant: init counted the host's two
variants and suffixed, deploy pinned the host out and did not. svc-ai-s1
declares two variants of its own and was the only instructions row to survive,
which is the shape of the bug read backwards.

`variant_select` opens by stating the invariant this broke - "keeps init and
deploy walking the exact same selection (SPOT)" - so the fix is to honour it
rather than to special-case the suffix.

The accompanying test asserts both entry points forward `pinned_variants`.
The five tests that shipped with adb53392bd all exercised the pin helper on
its own, which is why nothing caught a caller that never asked it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-09-30 01:40:55 +02:00
1 parent adb53392bd
commit 3b85e4e1e1
2 files changed
+72 -2

No files matched your search

@@ -21,7 +21,11 @@ from .inventory import (
_build_services_overrides_for_round as build_services_overrides_for_round,
)
from .storage import detect_storage_constrained
from .variant_select import add_variant_args, apply_variant_filter
from .variant_select import (
add_variant_args,
apply_variant_filter,
env_guide_host_pin,
)
if TYPE_CHECKING:
import argparse
@@ -112,10 +116,12 @@ def handler(args: argparse.Namespace) -> int:
else args.force_storage_constrained == "true"
)
roles_dir = str(compose.repo_root / "roles")
plan = plan_dev_inventory_matrix(
roles_dir=str(compose.repo_root / "roles"),
roles_dir=roles_dir,
primary_apps=primary_apps,
base_inventory_dir=str(args.inventory_dir),
pinned_variants=env_guide_host_pin(roles_dir, primary_apps),
)
try:
plan = apply_variant_filter(plan, args)
@@ -0,0 +1,64 @@
"""``init`` and ``deploy`` must hand the planner the same variant pins.
``init`` writes the inventory and ``deploy`` reads it back. The directory name
carries a round suffix only when the plan has more than one round, so a pin
that one of them applies and the other does not renames the directory under
the other's feet: init built ``<dir>-0`` while deploy looked for ``<dir>`` and
every app came back "not present in inventory".
"""
from __future__ import annotations
import ast
import unittest
from pathlib import Path
from . import PROJECT_ROOT
PLANNER = "plan_dev_inventory_matrix"
PIN = "pinned_variants"
CALLERS = (
"cli/administration/deploy/development/init.py",
"cli/administration/deploy/development/deploy/cli.py",
)
def pins_at(source: str) -> list[bool]:
"""Whether each planner call in *source* passes the pins.
Args:
source: the module's text.
"""
tree = ast.parse(source)
found: list[bool] = []
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
name = node.func.id if isinstance(node.func, ast.Name) else None
if name != PLANNER:
continue
found.append(any(kw.arg == PIN for kw in node.keywords))
return found
class TestInitAndDeployPlanAlike(unittest.TestCase):
def test_both_entry_points_pass_the_variant_pins(self) -> None:
missing: list[str] = []
for rel in CALLERS:
path = Path(PROJECT_ROOT) / rel
calls = pins_at(path.read_text(encoding="utf-8"))
self.assertTrue(calls, f"{rel}: no {PLANNER} call found")
missing.extend(rel for passes in calls if not passes)
self.assertEqual(
missing,
[],
f"These call {PLANNER} without {PIN}, so they plan a different "
"number of rounds than their counterpart and disagree on the "
f"inventory directory: {missing}",
)
if __name__ == "__main__":
unittest.main()