Philosophy
These are the principles that shape every provider and every design decision in denver — the why behind what Configuration describes as what. None of them are abstract for their own sake; each one exists because its absence caused a real, specific kind of pain somewhere else.
Genericity
denver itself holds no per-provider knowledge — it only calls whichever
provider type a stage’s provider: key names. A provider is a generic,
reusable engine; every project-specific detail comes from denver.yml,
never from the provider’s own code.
The test this principle has to pass: could this provider be dropped into a
completely unrelated project’s denver.yml, with zero code changes, and
just work off that project’s own config? If a provider ever needs an if project == "x" branch, genericity has already failed.
Explicit over implicit
denver never guesses that a file exists because it happens to sit in a
conventional place. There’s no “if there’s a docker-compose.yml next to
the denver.yml, use it,” no “if there’s a conan/recipes directory,
scan it.” Every path an env needs — a compose file, a recipe dir, a
skip-on-success/skip-on-failure script — is named explicitly, and a venv patch step is
run only if patches-apply: names the exact command for it.
What this buys: an env’s denver.yml is a complete, honest description of
what it does. Reading it (or running --show-config-full, which — unlike
the minimal-by-default --show-config — shows every key a stage’s own
provider understands, unset ones included) tells you everything; nothing is
hidden in directory layout that you’d only discover by tripping over it.
The cost is a few extra lines in the config for anything that
does follow a convention — a trade denver makes deliberately, on the
theory that a wrong guess is far more expensive than an explicit line.
Central default resolution
Every default a provider might fall back to — a PATH lookup, a conventional value, anything — is computed once, centrally, before any stage’s real work begins. A provider’s own build/setup step never guesses a default itself; it only ever reads what’s already been resolved.
What this buys: --show-config and a real run can never drift apart. If a
value looks wrong in --show-config, it is exactly as wrong in the real
run — there is no separate “what the code actually decides at run time” to
go check. Most tools with a “show me the effective config” mode can’t make
this promise, because their config resolution is scattered across the
codepath that actually runs things; denver’s can, because resolution and
execution are deliberately kept as two separate steps.
Fail loud on the unexpected
An unrecognised top-level key, an unrecognised key in a stage’s own
section, a --until/--skip naming a stage id that isn’t declared — all
of these are hard errors, immediately, not silently ignored.
The alternative — quietly doing nothing with a key nobody recognises — is how a typo’d or orphaned config key survives for months unnoticed. If a key doesn’t do anything, denver says so instead of pretending everything is fine.
Fast by default, never at the cost of correctness
Fingerprinting exists so that re-running an unchanged environment is cheap.
But speed is never allowed to compromise the one guarantee that actually
matters: what --show-config shows is what runs. When there’s tension
between “skip this because it’s probably unchanged” and “always resolve
correctly,” correctness wins, and --force exists as the deliberate,
explicit escape hatch for the rare case a fingerprint gets it wrong.
Reproducibility as a first-class goal
The same denver.yml, run on a fresh clone and on a six-month-old working
copy, should produce the same environment — not one shaped by whatever
history happens to be sitting on that particular machine.
[!NOTE] For full reproducibility you need to upload the docker container and the conan packages to some artifactory, so that denver downloads them prebuilt each instead of re-building it again.