Files
computer-playbook/docs/contributing/design/variants.md
T
kevinveenbirkenbachandClaude Opus 4.7 170b16c59d docs(design): rename matrix-deploy.md to variants.md, add inventory.md, sync cross-links
Two design docs under docs/contributing/design/:

- variants.md (renamed from matrix-deploy.md): explains the
  folder-per-round wrapper that consumes per-role meta/variants.yml
  and iterates variants at deploy time. Headings normalised to
  sentence-case; the "legacy unsuffixed inventory path" wording
  rewritten to drop the historical-context phrasing forbidden by
  documentation.md.
- inventory.md (new): explains how a single round's inventory is
  assembled out of meta/services.yml plus the variant payloads,
  which inclusion mechanisms exist (operator --apps, run_after walk,
  shared-service auto-include via strict `is True`), what lives in
  the inventory file vs. host_vars/<host>.yml, and when Jinja
  actually gets rendered (only at play runtime, via
  utils.cache.applications._render_with_templar). Worked example
  documents the WordPress + Discourse wiring as a reference case.

Cross-links updated wherever they pointed at matrix-deploy.md or
its concept name as a file:

- docs/agents/action/iteration/role.md
- docs/contributing/actions/deploy.md
- docs/contributing/artefact/files/plugins/lookup/applications.md
- docs/contributing/artefact/files/role/variants.md
- scripts/tests/deploy/local/deploy/reuse-kept-app.sh (path comment)

Plain-text occurrences of "matrix-deploy" as a concept term inside
Python comments, log messages, and test docstrings stay as-is; only
file-link references are migrated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 12:54:09 +02:00

6.3 KiB

Variants 🧬

How the development deploy CLI consumes per-role meta/variants.yml and iterates the resulting matrix at deploy time. For general documentation rules such as links, writing style, RFC 2119 keywords, and Sphinx behavior, see documentation.md. For the file format itself see variants.md. For how a single round's inventory is assembled (the layer this matrix wrapper sits on top of), see inventory.md. For the contributor-facing make target / dev-CLI workflow see deploy.md.

Folder-per-round model 🎛️

The development deploy CLI uses a folder-per-round model that splits cleanly between the init step (which materialises the inventories) and the deploy step (which iterates them):

  1. Round count. total_rounds = max(variant_count for app in primary_apps). If every primary app has only one variant, both steps degrade to a single folder and the unsuffixed inventory path is used.
  2. Per-round variant selection. In round R every primary app uses variant index R if R < its_variant_count else 0. So a single-variant app stays on variant 0 in every round, and a 3-variant app in a 5-round deploy follows 0, 1, 2, 0, 0.
  3. Inventory folders. init.py calls build_dev_inventory_matrix, which creates one folder per round at <inventory-dir>-<round> (or just <inventory-dir> when there is a single round). For each folder, build_dev_inventory resolves the round's per-app variant payload and bakes it into the inventory's host_vars as applications.<app>: <variant-payload> overrides. The inventory is therefore variant-resolved on disk; no runtime selector is needed.
  4. Deploy iteration. deploy.py re-derives the same plan via plan_dev_inventory_matrix (the planner is a pure function shared between init and deploy) and runs one deploy per folder. Round 0 deploys the full include set (the baseline state, including dependencies). Rounds R>0 only re-deploy apps that have a real variant for that round (i.e. round_variants[app] == R); apps that would only fall back to variant 0 are left at whatever state the previous round produced. This is the user-facing default: if variant 1 only exists for WordPress, round 1 re-deploys WordPress alone, not Keycloak or other dependencies. A round in which no app has a real variant is skipped entirely.
  5. Cleanup between rounds. Before each non-first iteration the wrapper runs entity.sh for the apps that are both about to be re-deployed in this round AND whose variant changed since the previous round. Apps that are not being re-deployed are NOT purged: their previous-round state stays. The final round is followed by no purge so the last state remains available for inspection or follow-up specs.
  6. --full-cycle (async update pass). When set (or FULL_CYCLE=true in the environment), the wrapper runs each round's deploy TWICE: first the regular sync pass, then immediately a second pass with -e ASYNC_ENABLED=true overriding the host_var, against the SAME variant's folder, BEFORE moving to the next round. The two passes are therefore always co-located on the same host state; the async pass never accidentally targets a host that a previous round's variant left behind.
  7. Pinning to a single round. Both init and deploy accept --variant <idx> (or read the VARIANT environment variable) to pin operations to a single round's folder. In single-round mode no inter-round cleanup runs, since there is no previous round to diff against. The make deploy-reuse-* targets honour the same VARIANT env-var by suffixing INVENTORY_DIR / INVENTORY_FILE to <base>-<idx>, so a redeploy can target one specific variant without iterating the full matrix.

The in-play loader (utils.cache.applications.get_merged_applications) always uses variant 0 as the default and lets the inventory's applications.<app> overrides win via deep merge: the inventory itself is the source of truth for what variant a round runs against.

What not to do 🚫

  • You MUST NOT introduce a parallel cache. The variants are cached per roles_dir inside utils/cache/applications.py and returned as deep copies; mutating the result MUST NOT corrupt subsequent lookups.
  • You MUST NOT skip the inter-round purge.
  • You MUST NOT introduce a runtime variant-selector extra-var. Variant data lives in the inventory after init; the deploy stage reads it as plain applications.<app> overrides.

Reference files 📌

File Purpose
variants.md File-structure rules for roles/<role>/meta/variants.yml.
applications.py (cache) Loader and cache for _build_variants (get_variants is the public Python entry point). The merged-applications path stays variant-agnostic; variant payloads reach it as inventory-level applications.<app> overrides.
inventory.py Dev inventory build: DevInventorySpec, build_dev_inventory (variant baking), plan_dev_inventory_matrix (pure planner shared with the deploy wrapper), build_dev_inventory_matrix (creates one folder per round).
init.py Dev CLI entry that drives build_dev_inventory_matrix to materialise <inventory-dir>-<round> folders.
deploy.py Dev CLI entry that re-derives the matrix plan and iterates one deploy per folder, with per-app cleanup between rounds.
entity.sh Cleanup script invoked between rounds for every app whose variant changed.
test_variants.py Unit tests covering parser edge cases, deep-merge semantics, caching, and the variant-zero default path.
test_inventory.py Unit tests covering the planner, variant baking, and the per-folder build flow.
test_matrix_deploy.py Wrapper-level tests covering the per-round folder iteration and cleanup logic.