Files
computer-playbook/utils/env
kevinveenbirkenbachandClaude Opus 5 96df7ea0eb feat(env): give every checkout its own stack domain
Every stack answered to *.infinito.test, and the host resolver pointed that
zone at 127.0.0.1, so a worktree's stack on its own bind IP could not be opened
in a browser while the primary stack existed.

INFINITO_DOMAIN is now derived per checkout: main.infinito.test for the
primary one, and for a linked worktree its branch, lowercased with everything
outside a-z0-9_- turned into "-", below INFINITO_DNS_DOMAIN
(feature/store-i18n -> feature-store-i18n.infinito.test). The branch is read
from the worktree's gitdir HEAD instead of running git, because git refuses the
mounted checkout inside the stack container. GitHub and act runs keep the
static default, custom.env still wins.

The handler overrides the caller's value on purpose: inside the stack
container that value is the previous .env, which make setup rewrote on every
deploy, so the old domain kept feeding itself back into the next compose up.

make network-dns-setup routes each stack's domain to the INFINITO_BIND_IP in
that checkout's .env, with a local= line so AAAA answers NODATA instead of the
primary stack's ::1. It skips domains outside the base zone, which a stale
.env still carries, and fails when two checkouts claim one domain.
worktree-up prints the domain and the follow-up command.

Existing checkouts switch after make dotenv-force and a redeploy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 17:44:38 +02:00
..
…

utils/env 📐

This directory hosts the Python implementation behind make dotenv. For the general rules that govern READMEs in code directories see documentation.md. For the per-variable defaults that this code consumes see default.env.

Purpose 🎯

The package converts the committed default.env plus a small runtime context into the gitignored .env at the repo root. Every module here belongs to one of four roles: parse, resolve, orchestrate, write.

Structure 🗂️

File Role
parser.py Read default.env into (values, comments) tuples.
runtime.py Resolve host-context lookups (disk, RAM, hostname, GHA/Act flags, /proc/version, helper-script invocation).
builder.py Thin orchestrator that defines EnvBuilder, BuildContext, and build_env(). Walks the handler registry.
writer.py Serialise an EnvBuilder into a docker-compose-compatible .env on disk.
handlers/ One module per dynamically-computed variable. See handlers README.

The CLI entry point lives at __main__.py under cli/meta/env/. Shell consumers source load.sh under scripts/meta/env/.

File Naming 🏷️

  • Module names MUST stay lowercase snake_case.
  • Pure passthrough or simple parser/writer helpers MUST live directly under utils/env/.
  • Per-variable computation MUST live under handlers/ (see handlers README for the naming convention used there).
  • Test files live in tests/unit/python/utils/env/ and mirror the module name with a test_ prefix.

Machine Facts vs Run Facts 🧭

The writer a handler picks decides how long a value lives. Both kinds are registered in default.env and both are visible to the lints; they differ only in whether the generated .env remembers what the environment said.

Writer Meaning On the next make dotenv
eb.setdefault machine fact a non-empty value in the process env is adopted and written back
eb.set run fact recomputed from BuildContext; whatever the process env said is discarded

setdefault is the operator-override contract: exporting a key changes it and the change persists. That persistence is why it MUST NOT carry a value an environment declares for a single run. Makefile exports BASH_ENV, so every make recipe sources .env — a value adopted once is re-exported into every later recipe and into every container that mounts the repo, and a per-run decision has silently become a property of the machine.

A key that some environment declares per run therefore MUST be written by a dedicated handler through eb.set. The exporting shell still wins for its own run, because load.sh preserves non-empty caller values when it sources .env; the file keeps the derived default. INFINITO_RUNNING_ON_GITHUB and INFINITO_CACHE_STACK are written this way. Note the consequence: for those keys .env and the running shell can legitimately disagree, and .env is not the authority.

Import Rules 🔗

  • parser.py, runtime.py, and writer.py MUST NOT import from builder.py or handlers/.
  • builder.py MUST NOT import individual handler modules; it MUST go through the ORDERED_HANDLERS list exposed by handlers/__init__.py.
  • Handler modules MUST NOT import each other (see handlers README).