chunk_size() multiplied the runner slots a chunk may assume by the waves the queue window affords, and nothing could say otherwise. The derivation is the right default -- too few rows leaves runners idle, too many push a chunk's tail past the queue cancel -- but a run that wants smaller chunks had to edit default.env to get them. INFINITO_CI_CHUNK_SIZE now takes precedence when it is greater than zero, and zero keeps the derivation. The input travels the same path offset already travels: entry-manual-steer.yml offers the field and maps empty to 0, call-orchestrator.yml hands it to all four chunk blocks, call-test-deploy.yml exports it as the env var. It is a string rather than a number for the reason resume_from_chunk already documents: a workflow_dispatch number input reaches the caller as a string, and a number-typed workflow_call input then makes GitHub refuse the job before it exists. Two places in call-test-deploy.yml set it, not one. The plan summary resolves the chunk size itself, so exporting it only for the app resolution would print a plan that disagrees with the matrix the run then deploys. Measured against the live helper: 0 gives 80 rows over 3 chunks, 25 gives 25 over 4, and returning to 0 restores 80 and 3. The gate found three faults in the first attempt and all three are in this commit: the key sorted after INFINITO_CI_CONCURRENCY rather than before it, the dispatch description ran to 81 characters against a limit of 80, and the retrigger fixture did not carry the new input. That last one matters beyond the test: it is the check that would otherwise let a retrigger drop the chunk size silently. carried_inputs() itself needed nothing, since it derives from the declared inputs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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, andwriter.pyMUST NOT import frombuilder.pyorhandlers/.builder.pyMUST NOT import individual handler modules; it MUST go through theORDERED_HANDLERSlist exposed byhandlers/__init__.py.- Handler modules MUST NOT import each other (see handlers README).