render config
dwe render config [service] renders service config files (e.g. .env,
env.php) from a template pack into each service hub directory, replaying any
service-minted secrets harvested into the durable generated-value store. Configs
are pure render outputs derived from the merged config plus the store.
Contents
Section titled “Contents”- Overview
- Template substrate:
${...}shorthand - The
${generated.<name>}namespace - Generated-value store
generated:declaration- Harvest, not mint
- Template pack resolution
- Manifest schema
- CLI usage
- Pipeline builtins
- Deploy flow
dwe runauto-render- Reset and
--clear-generated - Migration from
configs:copy - Related references
Overview
Section titled “Overview”Config rendering writes straight into the already-mounted src/ tree (no
per-file bind mount / mountpoint machinery). The model has two halves:
- Render — resolve a config template pack, render each manifest entry, and
write the result under the service hub dir (
svc.Dir), mode replace (overwrite). - Generated-once — an opt-in mechanism for service-minted secrets (Laravel
APP_KEY, Magentocrypt.key, …). The service generates the value writing it into its own file; DWE reads it back (“harvest”) into a durable per-service store (.dwe/generated.yml) and replays it on every subsequent render via the${generated.<name>}namespace.
Config rendering is opt-in: a service with no resolvable config pack is a
silent no-op. There is no dwe init scaffold wiring — you author the pack and the
pipeline steps yourself.
Template substrate: ${...} shorthand
Section titled “Template substrate: ${...} shorthand”Unlike render ide / ai / git — which use the raw Go text/template {{ }}
substrate (packcommon.TemplateData) — config templates use the ${...}
shorthand, the same form already used in ${APP_*} / ${DB_*} export rules.
This is a deliberate divergence justified by config-file ergonomics: config
authors expect ${...} parity with the values they reference.
${X} compiles to {{ resolve .Raw "X" }}, so the dot-path is looked up in the
merged config (cfg.Raw) with no raw. prefix — but only when X’s head
is a known namespace (a merged-config root key, or one of the special
namespaces below); an unrecognized head is left as a literal ${...} instead
of silently rendering "":
APP_URL=${services.main.hosts.web}DB_HOST=${vars.databases.main.host}DB_DATABASE=${vars.databases.main.name}APP_KEY=${generated.app_key}- Per-service fields use
${services.<name>...}(e.g.${services.main.ports.http}). This exposes only the curated subset injected intocfg.Raw["services"]—type/container/ dirs /configs/ports/hosts/ … — notrender/generated/ arbitrary merged fields. An omitted or uninjected field renders""(all${...}resolvers are lenient — a missing path is the empty string, never an error). - Free-form values live under
vars:— reference them as${vars.<path>}(e.g.${vars.databases.main}). A bare top-level dot-path with novars.prefix does not resolve: the merged config root is a strict allowlist (project,services,vars, …), so an arbitrary key likedatabasescan never appear there directly. - Generated values use
${generated.<name>}(see below).
There is no singular current-service ${service....} binding — reference the
service by name through ${services.<name>...}.
The ${generated.<name>} namespace
Section titled “The ${generated.<name>} namespace”${generated.<name>} resolves to the harvested value for the current service’s
<name> field from the generated-value store. On the first deploy the store
is empty, so ${generated.app_key} renders "" — the service then mints the
real value and DWE harvests it. On subsequent renders the stored value is
replayed verbatim, so the secret survives run / redeploy while staying out of
git.
An absent key renders "" (lenient, consistent with every other ${...}
resolver). The namespace is scoped to the service being rendered: it reads
store[<current-service>], so ${generated.app_key} in main’s pack never sees
magento’s crypt_key.
Generated-value store
Section titled “Generated-value store”The store lives at .dwe/generated.yml (under the gitignored .dwe/ runtime
directory — it is never committed):
services: main: app_key: "base64:Xa3…==" magento: crypt_key: "241f4fa60be8f69638343cacc5a1a192"- Values are strings (block scalars for multi-line secrets).
- Writes are atomic (temp file + rename), mirroring the deploy journal.
- A missing file is an empty store (first deploy). A corrupt file is a surfaced error — never silently swallowed, so a malformed store cannot be mistaken for “no secrets yet”.
- The store has no
schema_version. - Snapshot create/restore intentionally leaves
.dwe/generated.ymluntouched (it is a separate file from.dwe/deploy/state.yml).
generated: declaration
Section titled “generated: declaration”Generated fields are declared in service.yml (per-service lifecycle state),
not in the manifest — a replayed value can target a command argument, not
just a template:
type: appdir: ./services/mainrender: config: template: laravel # optional pack pin; else convention + .localgenerated: app_key: file: src/.env # output file, relative to the service hub (svc.Dir) pattern: '^APP_KEY=(.*)$' # regex; capture group 1 = value| Field | Type | Required | Description |
|---|---|---|---|
file | string | yes | Output file the service writes the value into, relative to the service hub dir (svc.Dir). Must be a contained relative path (no ..). |
pattern | string | yes | Regex applied per line; capture group 1 is the harvested value. Must compile and declare ≥1 capture group (validated by dwe validate). |
The map key (app_key) is the ${generated.<name>} identifier. dwe validate
rejects an invalid regex, a missing capture group, a path-escaping file, or a
field name that is not a valid ${generated.<name>} identifier.
Harvest, not mint
Section titled “Harvest, not mint”DWE never generates the secret itself — the engine is hermetic (no crypto / randomness), and reusing the service’s own generator is format-agnostic from DWE’s side (DWE only reads back a string). Harvesting:
- Reads
<svc.Dir>/<file>. - Applies
patternline by line, taking capture group 1 of the first match. - Write-if-absent into the store, then saves atomically if anything changed.
Errors are surfaced precisely, never silently skipped: a missing file, a pattern that matches no line, a pattern with no capture group, and a pattern that captures an empty value are all hard errors — so a half-minted secret cannot pollute the store. Write-if-absent means a redeploy is a no-op once a value is stored.
pattern (a regex capturing one string) is used instead of a format type enum
because DWE never enumerates config formats and never writes a foreign format —
it only reads one string out of one file. The same pattern extracts an
APP_KEY= dotenv line or a PHP-array 'crypt' => ['key' => '…'] value.
Template pack resolution
Section titled “Template pack resolution”Config packs live under workspace/templates/config/<pack>/ with the same
<pack>.local/ shadow-pack override convention as ide/ai/git (see
Local overrides). Resolution order — the first match
is used:
workspace/templates/config/<template>/whenrender.config.templateis set — strict: a pinned pack that does not exist is a hard error (catches typos).workspace/templates/config/<service-name>/.- Each ancestor in the service’s
extendschain:workspace/templates/config/<ancestor>/. workspace/templates/config/default/.- If none exist, config rendering is skipped (opt-in — no error).
Symlinked packs are rejected; the pack directory must stay contained inside the project root.
Manifest schema
Section titled “Manifest schema”Config packs are manifest-driven using the shared manifest.yml schema (the
same one read by ide/ai/git, see
Shared manifest schema), with two config-kind
constraints:
render: - from: env.tmpl to: src/.env| Aspect | Config pack |
|---|---|
| Dest root | service hub dir (svc.Dir) |
to shape | any contained relative path |
symlinks | rejected — rendered config files are written in place, never symlinked |
to: src/... is a usage convention, not a hardcoded join: to is interpreted
relative to the service hub dir. Authors target the app tree (already dir-mounted
into the container) by writing to: src/.... Destinations are path-safety
guarded — a to that escapes the hub dir, or resolves outside it via a symlink,
is rejected.
CLI usage
Section titled “CLI usage”dwe render config # render every enabled app service that resolves a pack (DeployOrder)dwe render config main # render only the `main` servicedwe render config main --harvest # harvest-only pass: read on-disk values into the store, NO render- With no argument, every enabled app service is processed in
DeployOrder(deterministic); a service with no config pack is skipped silently. Config rendering is app-only — only app services may declaredir/render/generated, so tool / infra services are not iterated. - With an explicit
[service], the argument is validated (must exist, be enabled, and have a hub dir); a missing pack surfaces a warning. --harvestswitches to a harvest-only pass (HarvestGenerated, no render) — for bootstrapping an existing project’s already-committed secrets into the store before they stop being committed.
The default render path is read-only with respect to project locks: it runs
no preflight and acquires no locks, matching the ide/ai/git renderers.
--harvest mutates the shared generated-value store, so it acquires the
project locks first (mirroring the deploy harvest builtin and reset --clear-generated) to avoid clobbering a concurrent store writer.
Pipeline builtins
Section titled “Pipeline builtins”Three engine builtins drive config rendering inside deploy / reset pipelines (see deploy builtins):
| Builtin | Purpose |
|---|---|
service_configs_render | Render the service’s config pack into its hub dir (mode replace), replaying stored generated values |
service_configs_render_check | Verify the rendered targets exist; pairing it as a check: forces the render step to re-run every deploy |
service_generated_harvest | Harvest the service’s declared generated: fields into the store (write-if-absent) |
service_configs_render_check mirrors service_configs_copy + service_configs_check:
its presence as a check: trips the hasCheck → Run lever, bypassing the
action-hash skip so the render step always re-runs — template edits and store
clears therefore always take effect.
Deploy flow
Section titled “Deploy flow”A deploy pipeline that renders configs and harvests a service-minted secret:
phases: - name: configs steps: - name: render-configs type: builtin cmd: service_configs_render with: service: main check: # presence forces re-run every deploy type: builtin cmd: service_configs_render_check with: service: main
- name: generate-app-key when: # gate: only when the store has no value yet type: builtin cmd: "generated-missing main app_key" type: dwe cmd: "shell main -- php artisan key:generate"
- name: harvest-app-key type: builtin cmd: service_generated_harvest with: service: mainFirst deploy: render writes APP_KEY= (store empty) → gate open → the
service mints APP_KEY=base64:… → harvest captures it.
Subsequent deploys: render replays the stored value → gate closed → generate
skipped → harvest is a no-op. The render re-runs every deploy via its check:.
Invariant: store empty for a key ⟺ value re-minted.
Note that the harvest step is deliberately not gated: service_generated_harvest
skips any field the store already holds, without reading its file. That is what
makes it a no-op above, and it is load-bearing rather than an optimisation —
dwe reset run (without --clear-generated) keeps the store while wiping the
service hub, so on the next deploy the minted file is gone while the value it
produced is still authoritative. A harvest that insisted on re-reading it would
fail the whole deploy over a value it already has. The strict errors below
(missing file, no match, empty capture) therefore apply only to a field that is
not yet stored — which is exactly when a bad read could pollute the store.
The generated-missing <svc> <field> predicate (see
conditions) reads
.dwe/generated.yml and is true when the field is absent or the store is
missing.
dwe run auto-render
Section titled “dwe run auto-render”dwe run re-renders service configs from the store after the deployment gate
passes (and after the post-pull config reload), but before lifecycle phases —
so configs reflect the current templates and replayed secrets at run time. It
never runs generate/harvest on run.
The run-render is non-destructive when replay data is absent: if a deployed
service declares generated: keys that are missing from the store, that service’s
render is skipped with a dwe deploy run hint rather than rendering a blanked
secret. Because the render runs only after the gate, a reset --clear-generated
followed by dwe run fails the gate before render is reached, so secrets are
never blanked.
Reset and --clear-generated
Section titled “Reset and --clear-generated”reset preserves the store by default — the secret survives a reset. Pass
--clear-generated to clear it (scoped by --service / all):
dwe reset run --clear-generated # clear the whole store on full resetdwe reset run --service main --clear-generated # clear only main's entriesThe store is cleared only after the full reset succeeds, including the post-pipeline journal cleanup — never if the pipeline or the journal mutation failed (else a deployed-journal + empty-store mismatch would make the run gate trust a service with no secrets). On a TTY with a non-empty store, an interactive prompt asks whether to also clear the generated values (default No). Rotation = clear + redeploy.
Migration from configs: copy
Section titled “Migration from configs: copy”The copy mechanism (configs: / mountpoint in service.yml,
service_configs_copy / service_configs_check builtins) keeps working but is
deprecated — dwe validate emits a warning and a single runtime notice fires
per copy step. To migrate:
- Move each baked file in
configs/services/<svc>/to a template underworkspace/templates/config/<pack>/, replacing literal values with${...}references. - Declare
render.config(optional pin) and anygenerated:fields inservice.yml; drop theconfigs:/mountpointblock. - Swap
service_configs_copy(+service_configs_check) forservice_configs_render(+service_configs_render_check) in the pipeline, adding the generate-gate +service_generated_harveststeps for any service-minted secrets. - Bootstrap an already-committed secret into the store with
dwe render config <svc> --harvest, then stop committing it.
Related references
Section titled “Related references”- service definitions (
service.yml) —render.configandgenerated:field reference - deploy builtins —
service_configs_render,service_configs_render_check,service_generated_harvest - conditions — the
generated-missingpredicate - render index — shared manifest schema, local overrides, pack resolution
- Templates — Go template syntax and render contexts
- Run
dwe render config --helpfor the live CLI surface