Files
computer-playbook/cli/deploy/development/common.py
T
kevinveenbirkenbachandClaude Opus 4.7 d97ed7a9e8 feat(deploy): per-role matrix-deploy variants with folder-per-round model
Introduces per-role `meta/variants.yml` for declaring multi-variant
deploys (e.g. WordPress Single-Site vs. Multisite) and reshapes the
development deploy CLI to materialise one inventory folder per variant
and iterate them with co-located sync+async passes.

Why:
- The previous single-inventory + Pass-1-then-Pass-2 split deployed each
  variant twice and ran the async pass against a host left in a
  different variant's state, masking real bugs.
- Inventory and runtime concerns were tangled: variant selection lived
  as a runtime extra-var and required cross-stage coordination.

How:
- `roles/<role>/meta/variants.yml` (file format docs at
  docs/contributing/artefact/files/role/variants.md): YAML list of
  deep-merge overrides on top of `config/main.yml`. Empty entry `{}` is
  variant 0 (= legacy payload). 6 roles get their dev overrides moved
  here from inventories/dev.yml; WordPress declares 2 variants.
- `utils/runtime_data.py` gains `_build_variants` / `get_variants` and
  drops the retired `_active_variants` runtime selector. Loader always
  returns variant 0 as the default; non-zero variants reach
  `get_merged_applications` as inventory-level `applications.<app>`
  overrides baked at init time.
- New `cli/deploy/development/inventory.py`: `DevInventorySpec`,
  `plan_dev_inventory_matrix` (pure planner shared between init and
  deploy), `build_dev_inventory_matrix` (materialises one folder per
  round at `<dir>-<R>`, or `<dir>` when single-variant). Single-folder
  paths preserve full backward compat.
- `cli/deploy/development/deploy.py`: matrix loop with per-round
  cleanup (only apps whose variant changed AND that are about to
  re-deploy), `--variant <idx>` (or `VARIANT=` env) to pin a single
  round, and `--full-cycle` (or `FULL_CYCLE=true`) which interleaves
  sync + async per round so the async re-deploy targets exactly the
  host state the matching sync deploy just produced.
- `cli/deploy/development/exec.py` gains repeatable `--env KEY=VAL` so
  bash callers can inject per-run context into in-container helpers
  without inlining heredocs.
- `scripts/meta/env/inventory.sh` exports `INVENTORY_VARS_FILE`
  (Python mirror in `cli.deploy.development.common.DEV_INVENTORY_VARS_FILE`;
  test_inventory_vars_file_spot.py locks them against drift).
- `make deploy-fresh-purged-apps` and `scripts/tests/deploy/ci/dedicated.sh`
  switch to a single init + single deploy `--full-cycle`. The
  `make deploy-reuse-*` bash scripts honour `VARIANT=` by suffixing
  `INVENTORY_DIR`/`INVENTORY_FILE`.
- The 5 `scripts/tests/deploy/local/deploy/*.sh` scripts now delegate
  their in-container heredocs to dedicated helpers in
  `scripts/tests/deploy/local/utils/`, removing the bash-in-bash
  quoting hell.

Tests:
- `tests/unit/utils/test_variants.py` (renamed from
  `test_application_variants.py`): loader edge cases, variant-zero
  default, retired-`_active_variants` guard.
- `tests/unit/cli/deploy/development/test_inventory.py` (new): planner,
  variant baking, builder, `filter_plan_to_variant`, `DevInventorySpec`.
- `tests/unit/cli/deploy/development/test_matrix_deploy.py` (new):
  per-round deploy filter, cleanup decisions, `--variant` pinning,
  `--full-cycle` per-round interleave.
- `tests/unit/cli/deploy/development/test_inventory_vars_file_spot.py`
  (new): drift guard for the bash <-> Python literal pair.
- `tests/unit/cli/deploy/development/test_exec.py` extended for `--env`.

Docs:
- `docs/contributing/artefact/files/role/variants.md` (new): file format.
- `docs/contributing/design/matrix-deploy.md` (new): runtime mechanism
  (folder layout, --variant, --full-cycle, cleanup logic).
- `docs/contributing/actions/deploy.md` (new): contributor dev-deploy
  flow (make targets, --variant pinning).
- `docs/administration/deploy.md` slimmed to admin-only (was mixed
  admin+dev), cross-links to the new contributor doc.
- `docs/agents/action/iteration/role.md` keeps only agent-iteration
  recipes; mechanism explanation moved out and linked.
- `docs/contributing/documentation.md` adds the "no self-SPOT
  declaration" rule; `applications.md` renamed its SoT header for
  consistency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 13:47:48 +02:00

57 lines
1.9 KiB
Python

from __future__ import annotations
import os
from pathlib import Path
from typing import TYPE_CHECKING
from .deps import apps_with_deps, resolve_run_after
if TYPE_CHECKING:
# Import only for type checking to avoid runtime import cycles.
from .compose import Compose
# SPOT: canonical repo-relative path to the development vars file that
# `infinito create inventory --vars-file <...>` consumes. Mirrors
# `INVENTORY_VARS_FILE` from scripts/meta/env/inventory.sh; the bash file
# is the SPOT-of-record for callers that go through the deploy chain
# (env is set before Python runs), and the literal here is the fallback
# for direct/test invocations that bypass that chain. A unit test locks
# both literals together so they cannot silently drift.
DEV_INVENTORY_VARS_FILE: str = os.environ.get(
"INVENTORY_VARS_FILE", "inventories/development/default.yml"
)
def repo_root_from_here() -> Path:
# <repo>/cli/deploy/development/common.py -> parents[3] == <repo>
return Path(__file__).resolve().parents[3]
def ensure_distro_env(distro: str) -> None:
# Keep env consistent with compose wrapper and other scripts
os.environ["INFINITO_DISTRO"] = distro
def make_compose(*, distro: str) -> Compose:
from .compose import Compose
ensure_distro_env(distro)
return Compose(repo_root=repo_root_from_here(), distro=distro)
def resolve_deploy_ids_for_app(compose: Compose, app_id: str) -> list[str]:
deps = resolve_run_after(compose, app_id)
return apps_with_deps(app_id, deps_role_names=deps)
def resolve_deploy_ids_for_apps(compose: Compose, app_spec: str) -> list[str]:
"""Resolve deploy ids for one or more space- or comma-separated app ids."""
app_ids = [a.strip() for a in app_spec.replace(",", " ").split() if a.strip()]
result: list[str] = []
for app_id in app_ids:
for dep in resolve_deploy_ids_for_app(compose, app_id):
if dep not in result:
result.append(dep)
return result