# Layered Environment Model How a repository supplies configuration to its script modules when one repository is operated against several environments — a selector file plus per-environment layers, loaded once by the `base` module. Read this when adding an environment variable, wiring a module reader to the base loader, introducing a second environment, deciding which file a value belongs in, or shipping `.scripts/` into a rendered target repository. This is an **optional layer on top of** `references/shared-modules.md` §2. A repository operated against exactly one environment needs a single `.env` and none of this. --- ## 1. The three layers Loaded in order by the base module's `--env-vars-reader.sh`, each later file overriding the earlier: | Order | File | Holds | Tracked in git | |---|---|---|---| | 1 | `.env` | **selector only** — the environment name | no | | 2 | `.env-` | environment-specific, non-secret configuration | no | | 3 | `.env--credentials` | secrets | no | ```bash _ENVIRONMENT="test" # .env — the only variable this file holds ``` The selector picks the other two: `.env-test` and `.env-test-credentials`. Switching environment is a one-line edit to `.env`, never an edit to a module. The validator fails fast when the selector is unset — an unset selector would otherwise load neither layer and produce a cascade of "missing variable" errors pointing at the wrong cause. **Which file does a value belong in?** Secret → credentials layer. Differs per environment → `.env-`. Selects which environment → `.env`. Nothing else belongs in `.env`; a value parked there silently applies to every environment. Repositories in the gondor family use `HL_V1_` as the prefix (`HL_V1_ENVIRONMENT`). The prefix is per-repository; the layering is not. ## 2. Load with `set -a`, not `export $(grep …)` ```bash set -a; . ""; set +a ``` The common alternative is broken: ```bash export $(grep -v '^#' "" | xargs) # do not use ``` It does not re-expand `$`, so `${HOME}/.kube/config` stays a literal string, and values containing quotes or spaces break apart. Dot-sourcing runs the file as shell assignments, so expansion, quoting, and spaces all behave. The cost of this choice: env files must contain **only** `KEY="value"` assignments. They are executed, so a stray command in one runs. ## 3. Tracked samples Every layer has a committed sample carrying placeholders, never real values: | Real file (git-ignored) | Sample (tracked) | |---|---| | `.env` | `.sample.env` | | `.env-` | `.sample.env-` | | `.env--credentials` | `.sample.env--credentials` | Operator flow: copy each sample to its real name, then fill in. Adding a variable to a layer without adding it to that layer's sample is how the next operator gets a missing-variable failure on a fresh clone. ## 4. Module readers must not re-source The base module has already loaded all three layers. A module's own `--env-vars-reader.sh`: - **Never sources the env files again.** A second read re-applies `.env` on top of `.env-`, silently clobbering the environment-specific overrides it just loaded. - Sets defaults only when unset, and parses CLI flags that override them: ```bash export __TARGET="${__TARGET:-}" # keep any base-provided value ``` This is the same rule as SKILL.md §5 "readers preserve already-exported values", applied to the layered case. Chain order in `lib/--index.sh` is base loader → module reader → API index. Sourcing `base/lib/--index.sh` from a library function re-triggers the whole env load as a side effect. When you only need shared functions, source `base/lib/--index-api.sh` instead — see SKILL.md §8. ## 5. Shipping `.scripts/` into a rendered repository When `.scripts/` is templated into a target repository, the env files must **land in the target** so its copy is operable, while real values stay out of git. - **Do not exclude** `.env`, `.env-*`, or `.sample.env*` from the render. Excluding them produces a target whose scripts cannot run. - The target's `.gitignore` must ignore `.env` and `.env-*`. Real values then live locally and are never committed or pushed. - `.sample.env*` stay **tracked** — they are safe placeholders and the operator's starting point. - Exclude only genuine template-internal files from the render (for example `.copier`). ### Repo-presence is not deployment Three distinct things get conflated: | | Meaning | |---|---| | Present in the repo | the file exists on disk in the target | | Git-tracked | committed and pushed | | **Deployed** | a reconciler (for example Argo CD) applies it from a watched path | **Deployed** means only the git-tracked manifests under the paths a reconciler actually watches. A gitignored local `.env` is present but never tracked, therefore never deployed. Shipping env files into a target is safe for exactly this reason — but it depends on the target's `.gitignore` being correct, so verify it rather than assuming. Regardless of any of this, never bake secret values into git-tracked manifests. Use an external secrets provider. ### Verify exclusions on the effective layer A template tool's config-file `_exclude` list (for example Copier's `.copier/copier.yaml`) is **inert if the tool never loads that config file**. An exclusion that appears correct in a config file may be doing nothing. Confirm the behaviour on the layer that actually takes effect — usually the CLI invocation — by rendering and inspecting the output. ## 6. Checklist - [ ] `.env` holds the selector and nothing else. - [ ] Non-secret per-environment values are in `.env-`; secrets in `.env--credentials`. - [ ] Every new variable was added to its layer's `.sample.*` file. - [ ] The module reader sets `:-` defaults only and does not source env files. - [ ] `lib/--index.sh` chains the base loader before the module reader. - [ ] The target repository's `.gitignore` covers `.env` and `.env-*`. - [ ] No secret value appears in any git-tracked file.