Files
computer-playbook/utils/env/builder.py
T
kevinveenbirkenbachandClaude Opus 4.7 9a578952c3 feat(env): SPOT env-pipeline + INFINITO_* namespace
Replace 8 bash env modules with a single Python generator
(cli.meta.env) that materialises env/static.env + runtime context
into a gitignored .env. Each variable lives in its own handler
under utils/env/handlers/; defaults carry inline one-line `# ...`
comments.

Rename to the INFINITO_* schema:
- workflow inputs: APPS, SERVICES_DISABLED, CMD, DEBUG, WHITELIST,
  BUNDLES, FULL_CYCLE, VARIANT, RUN_FLAGS, LIMIT_HOST, PARALLEL
- inventory: INVENTORY_DIR / INVENTORY_FILE / HOST_VARS_FILE,
  PW_FILE -> INVENTORY_PASSWORD_FILE
- venv: VENV_BASE / VENV_FALLBACK / VENV_NAME
- new SPOT defaults: COMPILE_SILENCE, CONTAINER_NO_CA,
  WAIT_HEALTH_TIMEOUT_S, PACKAGE_CACHE_FRONTEND_INIT_IMAGE,
  promoted IMAGE_TAG from GHA-only to general passthrough

compose.yml hardening:
- inline \${VAR:-default} removed; strict \${VAR:?...} everywhere
- new integration lint blocks regressions

Tooling:
- scripts/install/all.sh owns the stamp-file install logic
  (Makefile target delegates)
- scripts/meta/env/load.sh is the single shell entry point;
  8 bash modules deleted
- new shell lint: no INFINITO_* defaults outside env/static.env

Tests:
- 93 new unit tests for utils/env/{builder,parser,runtime,writer}
- restructure tests/lint into repository/ (file-content rules) vs
  filesystem/{shell,python,env}/ (filesystem-shape rules)
- drift + smoke integration tests against the generator

Docs:
- variables.md is now a pointer; per-variable docs live next to
  the key in env/static.env / env/ci.env
- compose.yml.md drops the Default column

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 20:40:34 +02:00

94 lines
2.7 KiB
Python

"""Compose the `.env` value-set from static defaults plus runtime context.
Thin orchestrator: each variable's mapping/computation lives in its own
module under :mod:`utils.env.handlers`. ``build_env`` walks the
``ORDERED_HANDLERS`` registry, letting each handler read/write through
an :class:`EnvBuilder`.
"""
from __future__ import annotations
import os
from dataclasses import dataclass
from typing import TYPE_CHECKING
from utils.env.handlers import ORDERED_HANDLERS
from utils.env.runtime import detect_gha_act
if TYPE_CHECKING:
from pathlib import Path
@dataclass(frozen=True)
class BuildContext:
"""Per-build inputs passed to every handler.
Attributes are read-only; per-handler scratch state lives on the
:class:`EnvBuilder` instead.
"""
static: dict[str, str]
static_comments: dict[str, str]
repo_root: Path
on_gha: bool
on_act: bool
class EnvBuilder:
"""Mirror bash ``: "${KEY:=value}"`` semantics with per-key comments.
``setdefault`` falls back to the supplied value only when the
process env has no non-empty value for the key. ``set`` is
unconditional. ``get`` reads from the builder, falling back to the
process env so derived values (e.g. ``INFINITO_CONTAINER``) can be
composed off previously-set keys.
"""
def __init__(self) -> None:
self.values: dict[str, str] = {}
self.comments: dict[str, str] = {}
def setdefault(self, key: str, value: str, *, comment: str = "") -> str:
existing = os.environ.get(key, "").strip()
self.values[key] = existing or value
if comment:
self.comments.setdefault(key, comment)
return self.values[key]
def set(self, key: str, value: str, *, comment: str = "") -> str:
self.values[key] = value
if comment:
self.comments.setdefault(key, comment)
return value
def get(self, key: str) -> str:
return self.values.get(key, os.environ.get(key, ""))
def build_env(
static: dict[str, str],
repo_root: Path,
*,
comments: dict[str, str] | None = None,
) -> EnvBuilder:
"""Compose the full env-set from ``static`` (parsed env/static.env) +
runtime context resolved against ``repo_root``.
``comments`` is the parsed per-key comment map from env/static.env
(see :func:`utils.env.parser.parse_static_env_with_comments`).
Dynamic keys carry their comments via each handler's ``COMMENT``
constant.
"""
on_gha, on_act = detect_gha_act()
ctx = BuildContext(
static=static,
static_comments=comments or {},
repo_root=repo_root,
on_gha=on_gha,
on_act=on_act,
)
eb = EnvBuilder()
for handler in ORDERED_HANDLERS:
handler.apply(eb, ctx)
return eb