Files
development-scripts-3darch/references/environment-model.md
T
root-at-skicandClaude Opus 5 6c8446cf90 Add layered environment model and taskfile composition rules
Fold two overlapping local skills (scripts-shell, shell-scripts-module)
into this one, keeping only what it did not already cover.

- Add references/environment-model.md: the three-layer .env selector model,
  why set -a dot-sourcing beats export $(grep|xargs), tracked samples, the
  no-re-sourcing rule for module readers, and the deployment boundary
  (repo-presence is not git-tracked is not deployed).
- Extend references/taskfile.md with the one-env-driven-task rule (no
  :default pairs) and composing commands through the task surface.
- Add the lifecycle-hook module type to references/module-patterns.md.
- Keep this skill's _moduleExampleV1_ naming as canonical; the retired
  skills' HL_V1_ and double-underscore dialects were not carried over.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 22:45:32 +03:00

143 lines
6.0 KiB
Markdown

# 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-<env>` | environment-specific, non-secret configuration | no |
| 3 | `.env-<env>-credentials` | secrets | no |
```bash
<PREFIX>_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-<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; . "<file>"; set +a
```
The common alternative is broken:
```bash
export $(grep -v '^#' "<file>" | 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-<env>` | `.sample.env-<env>` |
| `.env-<env>-credentials` | `.sample.env-<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-<env>`, silently clobbering the environment-specific overrides it just loaded.
- Sets defaults only when unset, and parses CLI flags that override them:
```bash
export <PREFIX>_<MODULE>_TARGET="${<PREFIX>_<MODULE>_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-<env>`; secrets in `.env-<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.