dwe render ide
Generate IDE-specific config files for each enabled service from a template pack. Output goes into the service’s hub directory (e.g. services/main/.vscode/settings.json).
Manifest required. Every IDE pack must contain a
manifest.ymlat its root listing each file to render. A missing manifest is a hard error with a migration hint. The schema is shared withrender aiandrender git— see Shared manifest schema. Per-file local overrides via the sibling<pack>.local/shadow tree apply identically to all three renderers.
Contents
Section titled “Contents”- Pipeline
- Service selection
- Template pack resolution
- Manifest schema
- Per-file rendering
- Worked example
- Output messages
- Common pitfalls
- Related references
Pipeline
Section titled “Pipeline”flowchart TD
CFG["Load merged config"] --> SEL{"Argument given?"}
SEL -- no --> SP["Select services<br/>policy filter + collision"]
SEL -- yes --> EXV["Validate the argument"]
EXV --> RHA["Resolve hub anchor<br/>deepest wins"]
SP --> LIST["Sorted service list"]
RHA --> LIST
LIST --> EACH{"For each service"}
EACH --> RP["Resolve template pack"]
RP --> LM["Load manifest.yml<br/>strict decode"]
LM --> VM["Validate manifest"]
VM --> RNDR["Render each manifest.render<br/>parse, execute, write"]
RNDR --> EACH
EACH -- "end" --> DONE["done"]
Service selection
Section titled “Service selection”Activation gate
Section titled “Activation gate”A service participates in IDE rendering only when both flags are true:
| Gate | Source | Default |
|---|---|---|
| Project-level | services.<name>.enabled (3-layer merged + required service override) | depends on service |
| IDE policy | services.<name>.render.ide.enabled | true for type: app; false otherwise |
If either gate is false, the service is skipped. Skips fall into two groups:
| Group | When | Reported as warning? |
|---|---|---|
| Policy skips | The service is disabled at the project level, or render.ide.enabled is false (explicitly or by type default). | no — these are the documented opt-in/opt-out behavior |
| Actionable skips | The service has no hub directory, or another service won a directory collision. | yes — these usually indicate a misconfiguration |
Directory normalization
Section titled “Directory normalization”After the activation gate, services without a hub directory are dropped — either because dir is empty or because it resolves to the project root. A service with no hub has nowhere to write, and a hub equal to the project root would let the renderer scribble over workspace.yml itself.
Collision resolution: deepest-wins
Section titled “Collision resolution: deepest-wins”When more than one surviving service points at the same dir, exactly one wins:
- Walk each service’s
extendschain to compute a depth. - The service with the deepest chain wins. The chain depth is capped (currently at 32 hops) as a cycle guard.
- Ties at the same depth are broken lexicographically by service name, so the result is deterministic.
The losing services emit a warning that names the winning service and the contested directory.
flowchart LR
M["main<br/>extends: nothing<br/>depth 0"] --> D
MD["main-debug<br/>extends: main<br/>depth 1"] --> D
D["dir: services/main"] --> W{"deepest wins"}
W --> WIN["main-debug renders"]
W -. "skip warning" .-> M
Rationale: IDE configs are about per-variant overrides (different debugger settings for main-debug, different launch profiles for a stage variant), so the most-specialized service in the chain owns the rendered files.
Explicit [service] argument
Section titled “Explicit [service] argument”dwe render ide <name> treats <name> as a hub anchor: it must be a real, eligible service, but the deepest-wins policy is then applied to figure out which sibling actually renders.
Validation order (first failure wins):
- Service not in config.
- Service is disabled at the project level.
- Service has no hub directory, or its hub is the project root.
render.ide.enabledevaluates tofalse— either explicitly or by the type’s default (non-apptypes are off by default; the error message tells you which case applies and how to opt in).
Once validated, the same deepest-wins resolution is applied scoped to siblings sharing the same dir. If the winner differs from the argument, an info line announces the substitution.
So dwe render ide main from a per-service deploy pipeline still does the right thing when main-debug is the active variant.
Template pack resolution
Section titled “Template pack resolution”For each selected service the renderer picks one pack directory under workspace/templates/ide/.
flowchart TD
S{"render.ide.template set?"}
S -- yes --> EX["workspace/templates/ide/{template}/"]
EX -- exists --> USE["use this pack"]
EX -- missing --> ERR["error<br/>explicit is strict"]
S -- no --> SN["workspace/templates/ide/{service-name}/"]
SN -- exists --> USE
SN -- missing --> DEF["workspace/templates/ide/default/"]
DEF -- exists --> USE
DEF -- missing --> WARN["warning + skip<br/>implicit not found"]
Rules:
- Explicit is strict. If
render.ide.templateis set, only that pack is tried. A missing pack is a hard error — no silent fallback. This protects against typos liketemplete:accidentally resolving todefault/and rendering surprising content. - Implicit chain (when
render.ide.templateis unset):<service-name>→ walk theextends:chain ancestor-by-ancestor →default. The first existing pack wins. Fall-through happens only when the candidate directory is missing; any other filesystem error is a hard error. If the implicit chain exhausts without finding a pack, rendering is skipped with a warning. Invalid pack names in the chain (leading dot, path separator) are skipped silently and the walk continues. - Why ancestors: an
extends:child likemain-debugtypically does not ship its own pack; it inherits the parent’s IDE configuration. Falling straight todefault/would render the wrong content because the default pack is calibrated for unrelated services. - A pack must be a real directory. Symlinked packs are rejected.
- The chosen pack must be inside the project root with no symlinked parent components.
Template-key validation rejects:
- Path separators (
/,\). - Leading dot (subsumes
..and hidden-name keys). - Empty (treated as “unset”, which is allowed and triggers the implicit chain).
Service names used as the implicit pack key go through the same strict pack-name validator (manifest.ValidatePackName): a service name with a leading dot, leading hyphen, or path separator is silently skipped as a candidate and the walk continues.
Manifest schema
Section titled “Manifest schema”Each IDE pack must contain a manifest.yml at its root using the shared manifest schema:
render: - from: .vscode/settings.json.tmpl to: .vscode/settings.json - from: .devcontainer/devcontainer.json.tmpl to: .devcontainer/devcontainer.json
# symlinks: optional — same semantics as render aiA missing manifest.yml is a hard error: every pack must include a manifest listing each file. Files inside the pack that are not referenced by render are ignored (the renderer never walks the pack on its own).
Validation runs in two passes:
| Pass | What it checks |
|---|---|
| Shape (pure) | At least one render or symlinks entry; to paths are contained under the hub; no duplicate to; every symlink to references a known render destination. |
| Sources (resolver-aware) | Every from exists either at the canonical workspace/templates/ide/<pack>/<from> or at the override workspace/templates/ide/<pack>.local/<from>. |
Strict YAML decode (yaml.Decoder.KnownFields(true)) rejects misspelled keys like renders: at load time.
Per-file rendering
Section titled “Per-file rendering”For each entry the destination is built by joining the service hub directory with the entry’s explicit to path (the from/source path is independent and is not used to derive the destination). The renderer:
- Reads the template file from the pack.
- Parses it as a Go text template with strict mode enabled — any reference to a missing field aborts rendering instead of writing a
<no value>placeholder. - Executes the template against the template variables.
- Resolves the destination and runs the path-safety guards.
- Creates any missing parent directories.
- Refuses to overwrite the destination if it already exists as a symlink.
- Writes the rendered bytes.
Existing regular files at the destination are overwritten without prompting — that is the whole point of the command.
Template variables
Section titled “Template variables”Templates receive a single object with these top-level fields:
| Variable | Source | Notes |
|---|---|---|
.Project | project: block from workspace.yml | e.g. .Project.Name, .Project.Prefix |
.Service | canonical config identity — the root of the rendering service’s extends: chain. | Use this for raw-config lookups keyed by service name (e.g. (index .Cfg.Raw.cs .Service).standard). Equals .Resolved when no extends chain. |
.Resolved | rendering identity — the service whose hub is actually being rendered (the deepest-extends collision winner). | Equals .Service in the no-collision case. |
.ServiceCfg | the effective service config of .Resolved, after extends resolution. | e.g. .ServiceCfg.Container, .ServiceCfg.Dir, .ServiceCfg.DirInternal, .ServiceCfg.WorkDirInternal, .ServiceCfg.CLI.* reflect the rendering service’s overlay. |
.Runtime | merged runtime block | .Runtime.UseHTTPS, .Runtime.SPX.Path. Per-service ports / hosts live on each service entry — use ((index .Services "<name>").Port "<port-name>") / ((index .Services "<name>").Host "<host-name>"). |
.Services | map[string]ServiceConfig keyed by service name | Indexed access only (Go template requirement): (index .Services "main"). Filter by type via .AppServices / .ToolServices / .InfraServices (zero-arg methods returning typed subsets). |
.Cfg | merged DweConfig (advanced) | .Cfg.Raw is the post-merge config map after DWE normalization (services.* injected from per-service service.yml files) — see Templates. Prefer the dedicated fields above for common cases. |
Advisory. IDE outputs land at
<svc.Dir>/<entry.To>— typically tracked project files (.vscode/settings.json,.devcontainer/devcontainer.json, …). Avoid consuming developer-local or secret keys via.Cfg.Rawin IDE templates: any value layered in fromworkspace/local.ymlwill surface in the rendered file and produce per-developer diffs in tracked artefacts. Use.Cfg.Rawfor repo-wide conventions only.
Strict-mode means a typo in {{.Servic.Name}} aborts rendering instead of writing a <no value> placeholder. Use {{if ...}} guards for fields that may legitimately be empty.
Accessing .Cfg.Raw
Section titled “Accessing .Cfg.Raw”Go’s text/template resolves dot-segments only when each segment matches [A-Za-z_][A-Za-z0-9_]*. Keys with hyphens, dots, leading digits, or any other non-identifier character cannot be reached with dot syntax — use index instead.
{{ .Cfg.Raw.ide.workspace_name }} {{- /* dot — identifier-safe keys */ -}}{{ index .Cfg.Raw "my-tool" "api-key" }} {{- /* index — hyphenated keys */ -}}Note: per-service ports / hosts live on the ServiceConfig value for that service — ((index .Services "main").Port "http"), ((index .Services "adminer").Host "web"). There is no separate .Runtime.Ports / .Runtime.Hosts / .Tools namespace.
The full set of helper functions available inside *.tmpl files (appURL, sprout registries, text/template built-ins) is documented in Templates.
Path-safety guards
Section titled “Path-safety guards”The renderer applies multiple boundary checks because a malicious or careless template path could otherwise be used to write files outside the service hub or the project root. The full chain, in order:
- Path cleanup. Each pack entry’s relative path is normalized; absolute paths and
..escapes are rejected during the pack walk. - Service-dir containment. The resolved destination must be inside the service hub directory.
- No symlinks in the destination path. Existing path components are checked before any directory is created. This catches a pre-existing
.devcontainersymlink that would otherwise let directory creation follow it outside the hub. - Real-path boundaries after creation. After parent directories are created, the destination directory is resolved through any symlinks and must still be inside both the real project root and the real service hub. This catches a race where a symlink is dropped between checks.
- No symlink at the destination file. If a symlink already exists at the target path, the write is refused (rather than following the link and overwriting whatever it points at).
The same guards are applied to the pack itself: the pack directory is checked for symlinked parent components, and the walker rejects any symlink inside the tree.
Worked example
Section titled “Worked example”Layout:
workspace/services/ main/ service.yml main-debug/ service.ymlworkspace/templates/ide/ default/ .devcontainer/devcontainer.json.tmpl .vscode/settings.json.tmpl main-debug/ .devcontainer/devcontainer.json.tmpl .vscode/settings.json.tmpl .vscode/launch.json.tmplworkspace/services/main/service.yml:
type: appcontainer: app-maindir: ./services/main# render.ide.enabled defaults to true (type: app)workspace/services/main-debug/service.yml:
type: appextends: maincontainer: app-main-debugdir: ./services/main # same dir as parent — collisionrender: ide: template: main-debug # use the main-debug packTemplate workspace/templates/ide/main-debug/.vscode/settings.json.tmpl:
{ "container.name": "{{.ServiceCfg.Container}}", "workspace.root": "{{.ServiceCfg.DirInternal}}", "service": "{{.Resolved}}"}dwe render ide (no argument):
- Selection: both
mainandmain-debugpass the activation gate. They sharedir: ./services/main.main-debughas a deeperextendschain (depth 1 vs 0), somain-debugwins.mainis reported as a collision skip and a warning is printed. - Pack resolution for
main-debug:render.ide.template: main-debugis explicit;workspace/templates/ide/main-debug/exists, so it is used. - Manifest render entries (in declaration order) yield the three outputs:
.devcontainer/devcontainer.json,.vscode/launch.json,.vscode/settings.json. - Each is rendered with
.Service = "main"(the chain root — what user-config maps are keyed on),.Resolved = "main-debug"(the rendering service),.ServiceCfg.Container = "app-main-debug", etc.
Result:
services/main/ .devcontainer/ devcontainer.json .vscode/ launch.json settings.jsondwe render ide main produces the same result — main is validated, but the hub-anchor resolution picks main-debug and prints ide [main] — resolved to main-debug (hub services/main).
Output messages
Section titled “Output messages”| Stream | Trigger |
|---|---|
| info | Explicit argument resolved to a different sibling — names the chosen winner and the shared hub directory. |
| warning | A selected service was skipped because it has no hub directory (or its hub is the project root). |
| warning | A selected service was skipped because another service won the directory collision — the winner is named. |
| info | A <pack>.local/<rel> override was used in place of the canonical pack file. |
| success | One line per rendered file, naming the relative path inside the project. |
| info | Nothing was selected after applying policy and collision rules. |
Errors are returned as command failures and name the offending service so the source of the problem is clear.
Common pitfalls
Section titled “Common pitfalls”- Non-
appservices do not render by default. Setrender.ide.enabled: trueinworkspace/services/<name>/service.ymlto opt in. - Typos in
render.ide.templateare hard errors. Explicit packs are strict; a missingworkspace/templates/ide/<name>/does not silently fall through todefault/. Either fix the name or removerender.ide.template. - Templates referencing missing fields fail. Strict-mode rendering means
{{.ServiceCfg.NoSuchField}}aborts rendering. Guard optional fields with{{if ...}}. - Symlinks at destinations are refused. If
.devcontainer/orsettings.jsonis a symlink, the renderer will not overwrite it. Remove the symlink and re-run. - Files not listed in
manifest.ymlare silently ignored. The renderer does not walk the pack — add an entry underrender:to include a template. dir: "."is rejected. A service whose hub is the project root would let templates scribble overworkspace.ymland other root files. Give every IDE-rendered service a real subdirectory.
Related references
Section titled “Related references”services.<name>.render.ideblock —enabled,template, inheritance viaextendsrender ai— companion command with the opposite collision policy- Run
dwe render ide --helpfor the live CLI surface