diff --git a/.github/workflows/README.md b/.github/workflows/README.md index f1f09117ff..99501ce9c8 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -204,7 +204,7 @@ entity, next to `modes`: ``` The row then draws from that intersection instead of the run's whole pool, and -so does every row whose transitive service closure contains that role. A +so does every row whose variant deploys that role, as the deploy planner resolves it. A role and a run that permit nothing in common abort the matrix rather than dropping the row. diff --git a/cli/meta/ci/matrix.py b/cli/meta/ci/matrix.py index 0b9ed91563..815159b58b 100644 --- a/cli/meta/ci/matrix.py +++ b/cli/meta/ci/matrix.py @@ -29,12 +29,17 @@ passing state between them. from __future__ import annotations import argparse +import functools import json import os import sys +from cli.administration.deploy.development.inventory.planner import ( + plan_dev_inventory_matrix, +) from cli.meta.ci import chunks, query, slots from utils.cache.applications import get_variants +from utils.cache.files import PROJECT_ROOT from utils.github.variant import axes, instructions, pools, selection, tor from utils.roles.display import display_names @@ -81,6 +86,24 @@ def candidates( return [{**row, "modes": query.row_modes(row, modes)} for row in rows] +@functools.cache +def deployed_rounds(app: str) -> tuple[tuple[str, ...], ...]: + """The apps each variant round of *app* deploys.""" + return tuple( + include + for _index, _dir, _variants, include, _purge in plan_dev_inventory_matrix( + roles_dir=str(PROJECT_ROOT / "roles"), + primary_apps=[app], + base_inventory_dir="plan", + ) + ) + + +def with_deployed_services(row: dict) -> dict: + """*row* with ``services`` narrowed to what its variant deploys.""" + return {**row, "services": deployed_rounds(row["name"])[row.get("variant") or 0]} + + def entries_of( *, modes: tuple[str, ...], @@ -95,12 +118,15 @@ def entries_of( ) -> list[dict[str, str]]: """Every candidate row of the sweep, axes assigned, in global order.""" return axes.assign( - candidates( - modes=modes, - whitelist=whitelist, - priority=priority, - lifecycles=lifecycles, - ), + [ + with_deployed_services(row) + for row in candidates( + modes=modes, + whitelist=whitelist, + priority=priority, + lifecycles=lifecycles, + ) + ], sweep=sweep, tor_mode=tor_mode, distros=distros, diff --git a/docs/architecture/decisions/0002-ci-architecture-axis.md b/docs/architecture/decisions/0002-ci-architecture-axis.md index 1ace3f642c..2ab4c8dba9 100644 --- a/docs/architecture/decisions/0002-ci-architecture-axis.md +++ b/docs/architecture/decisions/0002-ci-architecture-axis.md @@ -58,8 +58,8 @@ machine, not just a value the job applies to itself. .architectures` lists what the role can run on, next to `modes`, because both state a capability rather than a testing preference. The row draws from the intersection of the run's pool with the declarations of the role and of every - service in its transitive closure, because the deploy pulls those in too; an - empty intersection aborts the matrix. + role the deploy planner brings up for that variant, because the deploy pulls + those in too; an empty intersection aborts the matrix. - **CI images are built natively per architecture and published as one tag per distro.** Each architecture builds on its own runner, and diff --git a/tests/unit/python/cli/meta/ci/test_matrix.py b/tests/unit/python/cli/meta/ci/test_matrix.py index 2eb5b3b780..8f47db45e7 100644 --- a/tests/unit/python/cli/meta/ci/test_matrix.py +++ b/tests/unit/python/cli/meta/ci/test_matrix.py @@ -192,5 +192,32 @@ class TestCandidates(unittest.TestCase): self.assertEqual(discovered.call_args.kwargs["blacklist"], "web-app-b") +class TestWithDeployedServices(unittest.TestCase): + def setUp(self) -> None: + matrix.deployed_rounds.cache_clear() + self.addCleanup(matrix.deployed_rounds.cache_clear) + + def test_each_variant_takes_the_apps_its_own_round_deploys(self) -> None: + plan = [ + (0, "plan-0", {}, ("web-app-a", "svc-db-postgres"), ()), + (1, "plan-1", {}, ("web-app-a", "svc-ai-lmstudio"), ()), + ] + with mock.patch.object( + matrix, "plan_dev_inventory_matrix", return_value=plan + ) as planner: + rows = [ + matrix.with_deployed_services( + {"name": "web-app-a", "variant": variant, "services": ["x"]} + ) + for variant in (0, 1) + ] + + self.assertEqual( + [row["services"] for row in rows], + [("web-app-a", "svc-db-postgres"), ("web-app-a", "svc-ai-lmstudio")], + ) + planner.assert_called_once() + + if __name__ == "__main__": unittest.main() diff --git a/utils/github/variant/axes.py b/utils/github/variant/axes.py index 1e59eb5f64..ccfb15f2a8 100644 --- a/utils/github/variant/axes.py +++ b/utils/github/variant/axes.py @@ -209,7 +209,7 @@ def row_architectures( Args: app: role id, read for its ``meta/services.yml`` declaration. run_pool: what the run permits, already narrowed by its own input. - services: the row's transitive service closure, each narrowing the + services: the roles the row's variant deploys, each narrowing the row like the role's own declaration. Raises: