Files
computer-playbook/utils/env/README.md
kevinveenbirkenbachandClaude Opus 5.5 088c8be14f fix(ci): pull workspace images through the GHCR mirror like the deploy jobs
The workspace jobs pulled every image anonymously from Docker Hub and
two of them failed on its rate limit in run 39059128678 (#990): the
fedora swarm drill on busybox:1.38 on all three nodes, the centos
compose drill while building tor-smtp_custom. The regular deploy jobs
pull through the GHCR mirror, but the mirror was tied to being a real
GitHub runner: Profile.image_mirror_enabled() returned
runs_on_github(), and the swarm routine generated mirrors.yml only when
cli.meta.runtime reported github, which is act inside make
swarm-zombie.

The mirror is now its own capability, INFINITO_GHCR_MIRROR (next to
INFINITO_GHCR_MIRROR_PREFIX), written as a run fact like
INFINITO_CACHE_STACK: true on a real GitHub runner,
false everywhere else, and a non-empty exported value wins for the run.
Profile.image_mirror_enabled() reads the declaration and keeps
runs_on_github() as the fallback, and 02_provision_inventory.sh gates
on the same value instead of the runtime name. make swarm-zombie
forwards it into act through ACT_ENV, since act ignores the host env.

call-test-workspace.yml declares INFINITO_GHCR_MIRROR=true and passes
INFINITO_GHCR_MIRROR_PREFIX into the workspace container. Nothing else
of the workspace changes: is_ci, runs_on_github, the cache stack and
the docker root stay local-like. The step that loads the rescue paths
now removes the .env it generated on the runner, because on debian
rows load.sh sourced it inside the container and the run inherited
INFINITO_RUNNING_ON_GITHUB=true.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 22:40:13 +02:00

50 lines
3.6 KiB
Markdown

# utils/env 📐
This directory hosts the Python implementation behind `make dotenv`.
For the general rules that govern READMEs in code directories see [documentation.md](../../docs/contributing/documentation.md).
For the per-variable defaults that this code consumes see [default.env](../../default.env).
## Purpose 🎯
The package converts the committed [default.env](../../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](../../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](handlers/README.md). |
The CLI entry point lives at [__main__.py](../../cli/meta/env/__main__.py) under `cli/meta/env/`.
Shell consumers source [load.sh](../../scripts/meta/env/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](handlers/README.md) for the naming convention used there).
- Test files live in [tests/unit/python/utils/env/](../../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](../../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](../../scripts/meta/env/load.sh) preserves non-empty caller values when it sources `.env`; the file keeps the derived default. `INFINITO_RUNNING_ON_GITHUB`, `INFINITO_CACHE_STACK` and `INFINITO_GHCR_MIRROR` 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](handlers/README.md)).