mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-12 05:36:40 +00:00
Fourteen real conflicts plus the 66 generated READMEs.
utils/cache/files.py and its test: both sides added a skip-dir rule, resolved
as a union so the root `build/` prune and the per-root walk key coexist.
plugins/lookup/database.py and postgres_libraries.py: main's added lookups kept,
with get_entity_name renamed to entity_name as this branch renamed it.
roles/web-app-{mailu,matrix}/meta/services.yml: main's `covers: [email]` and its
MAIL_PROVIDER-gated email entry, plus this branch's gitlab/jira/n8n/jitsi
consumers, which the merge base carried on neither side.
group_vars/all/17_resource.yml: this branch's lookup('resource', ...) refactor;
main's single added literal is already covered by _POSTGRES_MAX_LOCKS_PER_TRANSACTION.
docs/requirements/035-mcp-proxy-expansion.md: this branch moved the design into
ADR 0003, so the requirement keeps only the user story and the criteria. Main's
delta inside the moved block was two new roles, carried into the ADR audit:
web-app-stalwart as adapter_candidate, svc-net-tor-smtp as privileged_control_plane.
The no_surface total said 99 while its own subcategories summed to 112.
inventories/development/default.yml: biber keeps no explicit email. Both sides fixed
the same CI failure; this branch fixed it in _apply_account_defaults, which renders
biber@{{ DOMAIN_PRIMARY }} (utils/cache/users/__init__.py:68) — byte-identical to
the address main hardcoded, so the inventory line is a second copy of one fact.
roles/web-app-{gitlab,keycloak}/files/playwright/playwright.spec.js and
roles/web-app-gitlab/templates/playwright.env.j2: union of both import sets, with
requireDotenvValue kept over decodeDotenvQuotedValue for CANONICAL_DOMAIN because
an empty value must fail rather than match every substring.
The 66 role READMEs take this side: aceb5295ef moved Cosmos, Quick Setup and
Credits to the documentation build, and test_readme.py now rejects them in a
committed README. Main's prose edits inside them are carried over by hand (the
Mailu deprecation banner, Mailu -> Stalwart in erpnext and zammad), and the two
READMEs main added fresh are stripped of the three generated sections.
roles/web-app-sphinx/README.md stays deleted; 6eb9a59d14 merged that role into
web-app-docs. The root README's generated role table was regenerated from the
merged metadata.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
216 lines
7.2 KiB
Python
216 lines
7.2 KiB
Python
"""Argument parsing and round orchestration for the ``deploy`` subcommand."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
|
|
from cli.administration.deploy.development.common import (
|
|
make_compose,
|
|
resolve_container,
|
|
)
|
|
from cli.administration.deploy.development.inventory import (
|
|
_build_services_overrides_for_round as build_services_overrides_for_round,
|
|
)
|
|
from cli.administration.deploy.development.inventory import (
|
|
plan_dev_inventory_matrix,
|
|
prune_orphans_after_disable,
|
|
)
|
|
from cli.administration.deploy.development.variant_select import (
|
|
add_variant_args,
|
|
apply_variant_filter,
|
|
env_guide_host_pin,
|
|
)
|
|
from cli.administration.inventory.provision.services_disabler import (
|
|
find_provider_roles,
|
|
parse_services_disabled,
|
|
)
|
|
|
|
from .drill import _maybe_recover_drill
|
|
from .purge import _purge_app_entities
|
|
from .rotate import _rotate_credentials
|
|
from .run import _run_deploy
|
|
|
|
|
|
def _env_full_cycle() -> bool:
|
|
return os.environ.get("full_cycle", "").strip().lower() == "true"
|
|
|
|
|
|
def add_parser(sub: argparse._SubParsersAction) -> None:
|
|
p = sub.add_parser(
|
|
"deploy", help="Run deploy inside the infinito container (requires inventory)."
|
|
)
|
|
p.add_argument(
|
|
"--inventory-dir",
|
|
default=os.environ.get("INFINITO_INVENTORY_DIR"),
|
|
required=os.environ.get("INFINITO_INVENTORY_DIR") is None,
|
|
help=(
|
|
"Inventory directory base (default: $INFINITO_INVENTORY_DIR). When the "
|
|
"primary apps declare more than one matrix-deploy variant, the "
|
|
"wrapper iterates the sibling folders `<dir>-0`, `<dir>-1`, ... "
|
|
"produced by the matching `init` step; otherwise the directory "
|
|
"is used as-is."
|
|
),
|
|
)
|
|
|
|
g = p.add_mutually_exclusive_group(required=True)
|
|
g.add_argument(
|
|
"--apps",
|
|
help="One or more application ids (will include run_after deps automatically).",
|
|
)
|
|
g.add_argument(
|
|
"--id",
|
|
nargs="+",
|
|
default=None,
|
|
help="Explicit application ids (space-separated).",
|
|
)
|
|
p.add_argument(
|
|
"--debug",
|
|
action=argparse.BooleanOptionalAction,
|
|
default=False,
|
|
help="Enable/disable Ansible debug mode (default: disabled).",
|
|
)
|
|
add_variant_args(p, action="deploy")
|
|
p.add_argument(
|
|
"--full-cycle",
|
|
action=argparse.BooleanOptionalAction,
|
|
default=_env_full_cycle(),
|
|
help=(
|
|
"After each round's regular deploy, immediately re-run the "
|
|
"deploy with `-e ASYNC_ENABLED=true` (the async update pass). "
|
|
"Pass 1 + Pass 2 stay co-located on the SAME variant so the "
|
|
"async re-deploy always runs against the host state the "
|
|
"matching sync deploy just produced. Defaults to the "
|
|
"full_cycle environment variable (true|false) when set."
|
|
),
|
|
)
|
|
p.add_argument(
|
|
"ansible_args",
|
|
nargs=argparse.REMAINDER,
|
|
help="Passthrough args appended to `cli.administration.deploy.dedicated` (use `--` to separate).",
|
|
)
|
|
p.set_defaults(_handler=handler)
|
|
|
|
|
|
def handler(args: argparse.Namespace) -> int:
|
|
compose = make_compose()
|
|
|
|
if args.apps:
|
|
primary_app_ids = [
|
|
a.strip() for a in args.apps.replace(",", " ").split() if a.strip()
|
|
]
|
|
else:
|
|
primary_app_ids = list(args.id or [])
|
|
|
|
raw_disabled = os.environ.get("disable", "").strip()
|
|
disabled_app_ids: set[str] = set()
|
|
if raw_disabled:
|
|
services = parse_services_disabled(raw_disabled)
|
|
roles_dir = compose.repo_root / "roles"
|
|
provider_map = find_provider_roles(services, roles_dir)
|
|
disabled_app_ids = set(provider_map.values())
|
|
primary_app_ids = [
|
|
app_id for app_id in primary_app_ids if app_id not in disabled_app_ids
|
|
]
|
|
|
|
if not primary_app_ids:
|
|
raise SystemExit("All primary apps disabled by `disable`: nothing to deploy")
|
|
|
|
passthrough = list(args.ansible_args or [])
|
|
if passthrough and passthrough[0] == "--":
|
|
passthrough = passthrough[1:]
|
|
|
|
roles_dir = str(compose.repo_root / "roles")
|
|
plan = plan_dev_inventory_matrix(
|
|
roles_dir=roles_dir,
|
|
primary_apps=primary_app_ids,
|
|
base_inventory_dir=str(args.inventory_dir),
|
|
pinned_variants=env_guide_host_pin(roles_dir, primary_app_ids),
|
|
)
|
|
try:
|
|
plan = apply_variant_filter(plan, args)
|
|
except ValueError as exc:
|
|
raise SystemExit(f"--variant: {exc}") from exc
|
|
|
|
container_name = resolve_container()
|
|
|
|
rc = 0
|
|
for plan_index, (
|
|
round_index,
|
|
inv_dir,
|
|
round_variants,
|
|
include_roles,
|
|
purge_roles,
|
|
) in enumerate(plan):
|
|
if disabled_app_ids:
|
|
round_overrides = build_services_overrides_for_round(
|
|
roles_dir=str(compose.repo_root / "roles"),
|
|
round_index=round_index,
|
|
primary_app_variants={
|
|
a: round_variants[a] for a in primary_app_ids if a in round_variants
|
|
},
|
|
)
|
|
round_deploy_ids, pruned = prune_orphans_after_disable(
|
|
include=include_roles,
|
|
primary_apps=primary_app_ids,
|
|
disabled_app_ids=disabled_app_ids,
|
|
services_overrides=round_overrides,
|
|
)
|
|
round_deploy_ids = list(round_deploy_ids)
|
|
if pruned:
|
|
print(
|
|
f">>> `disable` orphan-pruned {len(pruned)} transitive dep(s) "
|
|
f"from round {round_index}: {','.join(pruned)}"
|
|
)
|
|
else:
|
|
round_deploy_ids = list(include_roles)
|
|
|
|
if plan_index > 0:
|
|
purge_targets = [
|
|
role for role in purge_roles if role not in disabled_app_ids
|
|
]
|
|
_purge_app_entities(container=container_name, app_ids=purge_targets)
|
|
|
|
def run_pass(
|
|
extra_vars: dict,
|
|
*,
|
|
deploy_ids: list[str] = round_deploy_ids,
|
|
inventory_dir: str = inv_dir,
|
|
) -> int:
|
|
return _run_deploy(
|
|
compose,
|
|
deploy_ids=deploy_ids,
|
|
debug=bool(args.debug),
|
|
passthrough=passthrough,
|
|
inventory_dir=inventory_dir,
|
|
container_name=container_name,
|
|
extra_ansible_vars=extra_vars,
|
|
)
|
|
|
|
pass_label = (
|
|
f"matrix-deploy: round {round_index + 1}/{len(plan)} "
|
|
f"inv={inv_dir} variants={round_variants} apps={round_deploy_ids}"
|
|
)
|
|
print(f"=== {pass_label} PASS 1 (sync) ===")
|
|
rc = run_pass({"VARIANT_INDEX": round_index, "PRIMARY_APPS": primary_app_ids})
|
|
if rc != 0:
|
|
return rc
|
|
|
|
if bool(args.full_cycle):
|
|
_maybe_recover_drill(compose, plan_index)
|
|
_rotate_credentials(
|
|
compose, inventory_dir=inv_dir, round_variants=round_variants
|
|
)
|
|
print(f"=== {pass_label} PASS 2 (async) ===")
|
|
rc = run_pass(
|
|
{
|
|
"ASYNC_ENABLED": True,
|
|
"VARIANT_INDEX": round_index,
|
|
"PRIMARY_APPS": primary_app_ids,
|
|
}
|
|
)
|
|
if rc != 0:
|
|
return rc
|
|
|
|
return rc
|