4. Solution Strategy
Seven ideas. Chapter 5 shows the code that implements them; chapter 9 records why each one won.
4.1 Declarative config, generic providers
denver.yml describes what. Providers know how. A provider hard-codes
nothing about a project.
Change an environment → edit config.
Need genuinely new behavior → add a provider.
Serves: extensibility, least duplication.
4.2 An ordered pipeline of stages
stages: is an ordered list of stage ids. Each id has a section, and that
section must name its provider: explicitly — even when the id and the
provider name are the same word.
Two
uvstages can exist side by side, with different venvs.Renaming a stage never silently changes its behavior. See ADR-0005.
Serves: extensibility, transparency.
4.3 One central place for every default
Every provider default is computed once, centrally, before any stage runs —
never inside setup(). Providers only read keys that are already there.
Consequence: --show-config and the real run cannot disagree, because they
call the same function. See
ADR-0001.
Serves: transparency.
4.4 Nothing is inferred from the directory layout
A file that merely exists changes nothing. A path takes effect only when config names it, and a named path that is missing is a fatal error.
Serves: transparency, reproducibility. See ADR-0002.
4.5 Two ways to inherit config
Mechanism |
Pulls in |
Used for |
|---|---|---|
Whole-file |
another env’s entire config, as a base |
“this env is that base, plus a few pins” |
Section-level |
one named section from another env |
“reuse that env’s |
Both use the same merge rules, so the mental model stays the same: base first, this layer on top.
Serves: least duplication.
4.6 Loud failure over a silent guess
Unknown top-level key, unknown stage key, unknown stage id, unknown provider → error, with the closest match suggested.
Two layers setting the same string key to different values → error, unless the override is marked with
!.A configured path that does not exist → error, at resolution time, before anything runs.
Serves: reproducibility, transparency.
4.7 Fast by default, and the container is only a relocation
Two speed decisions and one packaging decision, all in service of “the second run must be cheap and the host run must always be possible”:
Every stage that does real work fingerprints its inputs and skips itself when nothing changed.
--fastskips the check entirely,--forcebypasses it.The
dockerprovider does not build anything. It relocates the rest of the pipeline: it re-invokes denver inside the container with that wrapper stage skipped. Same stages, same config, host or container.Every provider talks to the world only through
Context.run()/which()andContext.write_text()/mkdir()/…, so--dry-runand the offline test suite both work by intercepting one object.
Serves: speed, no Docker lock-in, testability.