Upgrading DWE
A new DWE version can change how your project loads, what a pipeline step does, or what a command prints. Most releases change nothing you have to act on; some do. This page is the short list of what breaks and what to do about it, newest release first.
It is deliberately not a change list — that is CHANGELOG.md, which records every change. Here you will only find the ones that need you to edit something.
Upgrade the binary
Section titled “Upgrade the binary”However you installed it:
brew upgrade semsemyonoff/tap/dwe # HomebrewOtherwise download the archive for your platform from the releases page and replace the binary on your $PATH. Installation methods are listed in the README.
DWE does not update itself. The update: block in workspace.yml is unrelated — it controls a git probe against your project repository, not the DWE binary.
After upgrading, in any project
Section titled “After upgrading, in any project”Three things worth doing before you trust the new version in a project:
-
Run
dwe validatefirst. It deploys nothing and starts no service, so a removed config key surfaces as a named diagnostic instead of a failed deploy. A project that no longer loads at all reports one error naming the key. (It does execute the project’s ownchecks.*entries, which may beshellorscriptcommands; those are expected to be idempotent inspection, but the CLI cannot enforce it.) -
Restart the host bridge, if the project uses one. The bridge daemon runs the binary that started it, so after a host upgrade a containerized
dwekeeps executing the old version until the daemon is replaced:Terminal window dwe bridge stop && dwe bridge startdwe bridge statusshows the running daemon;dwe versionfrom inside a bridged container shows which build it answers with. -
Force a redeploy when the release notes say so. A behaviour change that does not alter the deployment hash is invisible to
dwe deploy run, which will reportalready up-to-dateand skip the very step whose semantics moved.dwe deploy run --forcere-runs every step;when:guards still apply.
Upgrading to 0.7.1
Section titled “Upgrading to 0.7.1”Nothing in an existing project has to change: dwe init and its default ai pack stopped creating CLAUDE.md, but a project keeps the pack it was scaffolded with. Claude Code reads AGENTS.md natively, and a CLAUDE.md, .claude/CLAUDE.md or CLAUDE.local.md in or above the working directory makes it skip every AGENTS.md. To drop the links from an older project, all at once:
- Delete the root
CLAUDE.mdsymlink and remove thesymlinks:entry forCLAUDE.mdfrom eachworkspace/templates/ai/*/manifest.yml. - Delete the
services/*/CLAUDE.mdsymlinks by hand on every machine — rendering never removes an output it no longer declares. - A service repository that ships its own
CLAUDE.mdstill suppressesAGENTS.mdinside it, the project root’s included. Rename it toAGENTS.md, or set Claude Code’s Project instructions toclaude-md-and-agents-md(/config). symlink_fallbackindwe init --output jsonis alwaysfalsenow and goes away in the next minor release.
Upgrading to 0.7.0
Section titled “Upgrading to 0.7.0”Nothing in an existing project has to change. One thing to coordinate:
- A workspace that sets the new top-level
render:key needs 0.7.0 on every machine. Older versions reject the unknown key, sodwefails to load the project — and so does every git hook or command that callsdwe. Upgrade everyone before committingrender.workspaceto the workspace repository.
Upgrading to 0.6.3
Section titled “Upgrading to 0.6.3”Nothing in a project has to change, and no redeploy is needed. Three behaviours moved:
- The
dwe cmdbanner is on stderr. The▶ <id> [<type>] <description>line used to open the command’s stdout; a script that skipped or parsed that first line now receives the command’s own output only, and under--output jsonthe banner is not printed at all. The banner also shows only the first line of a multi-linedescription:. dwe validatemay report new findings in an existing project: ahide:that does not evaluate, a missing file undercompose:/compose_after:/compose.base, a command whoseservice:names no compose service, and an info note for ahealthcheck:in the active compose chain withoutstart_period(an image’s ownHEALTHCHECKis not inspected). They point at mistakes that were already there. None of these checks runs in preflight, so none blocks a lifecycle command.dwe compose argvno longer reads dwe flags after the compose command.dwe compose argv exec app ls --allnow prints--allas part of the command; put dwe’s own flags first (dwe compose argv --all ps,dwe -v compose argv ps).
Upgrading to 0.6.2
Section titled “Upgrading to 0.6.2”Integration tests
Section titled “Integration tests”A compose host port routed through vars: is now remapped in every test copy. If a compose file publishes a port as a single variable ("${VALKEY_PORT:-6379}:6379") and an active exports.env rule exports that variable from: vars.<path>, dwe test allocates a free host port for <path> and writes it into the copy’s local.yml — with no env.vars entry in the scenario. Previously such a port bound the live stack’s number and dwe validate warned about it.
What to check in your scenarios:
- A step that hardcoded the original host port now talks to the wrong port. Read it from the variable instead —
${vars.<path>}— or pin the copy’s port withenv.vars: { <path>: 6380 }, which disables the automatic remap for that path. env.vars: { <path>: auto }entries for such a port are now redundant. They keep working and change nothing; delete them at your convenience.autostill matters for a var that no compose port reads.- An untraced variable is unchanged — no
exports.envrule, a rule whosewhen:is falsy, or one reading something other thanvars.<path>/ a declared service port. It still binds the original port and still warns, but the warning now points at the two ways to route the port (a declaredservices.<name>.portsentry, or anexports.envrulefrom: vars.<path>) instead of telling you to writeenv.vars: { …: auto }.
A scenario blocked by the compose isolation scanner creates nothing. The scan now runs before the copy is made, so a blocking finding (container_name:, a literal host port) fails the scenario with no copy directory and no compose project, and no failure report is collected for that run — a report left by an earlier failure of the same scenario stays in place, so read the run’s own warnings rather than the directory’s timestamp. The exit code is unchanged (1). --keep has nothing to keep for such a scenario.
dwe test list --output json ignores per-developer compose overlays. compose.extra / services.<name>.compose.extra from your own workspace/local.yml reference gitignored files the copy never receives, so the command now evaluates scenarios without them, as the copy runs: cost numbers (build_services, external_images) drop those services, and so does cost_profile.isolation_findings. dwe validate tests builds the same view per scenario, so an interpolated_host_port finding that exists only in such an overlay is no longer reported — its other finding kinds (container_name, a literal host port, the volume and network kinds) still come from a project-wide scan and warn regardless of overlays.
Upgrading to 0.6.1
Section titled “Upgrading to 0.6.1”Template functions
Section titled “Template functions”DWE moves to go-sprout 1.1, which no longer accepts Sprig’s argument order. This affects every place {{ … }} templates run — see Templates.
Ten functions now fail on the Sprig order. get, set, unset, hasKey, pick, omit, append, prepend, slice and without take the map or list last. The old order used to be reordered for you with a warning; it is now a render error, so it surfaces the first time the template runs. Rewrite the call as a pipe:
{{ hasKey $m "a" }} → {{ $m | hasKey "a" }}{{ append $list "x" }} → {{ $list | append "x" }}{{ slice $list 1 2 }} → {{ $list | slice 1 2 }}Four regex functions changed argument order and do not fail. The string they work on moved to the last position. A template written for the old order still renders — to the wrong result:
| function | before | now |
|---|---|---|
regexFindAll | regexFindAll <re> <value> <n> | regexFindAll <re> <n> <value> |
regexSplit | regexSplit <re> <value> <n> | regexSplit <re> <n> <value> |
regexReplaceAll | regexReplaceAll <re> <value> <replacement> | regexReplaceAll <re> <replacement> <value> |
regexReplaceAllLiteral | regexReplaceAllLiteral <re> <value> <replacement> | regexReplaceAllLiteral <re> <replacement> <value> |
Nothing reports these, so search for them:
grep -rnE 'regex(FindAll|Split|ReplaceAll)' workspace/The search also hits regexFindAllGroups and regexFindAllNamed, which did not change — skip those. The rest of the regex functions — regexMatch, regexFind, regexQuoteMeta and the *Groups / *Named variants — are unchanged.
The mustRegex* aliases are gone. mustRegexFind, mustRegexFindAll, mustRegexMatch, mustRegexSplit, mustRegexReplaceAll and mustRegexReplaceAllLiteral were deprecated; a template calling one now fails to parse. Drop the must prefix — and apply the new argument order from the table above where it applies.
div by zero is now a render error instead of an arbitrary number.
Platform
Section titled “Platform”Release binaries need macOS 13 Ventura or later. They are built with Go 1.27, which dropped macOS 12; on an older Mac, stay on 0.6.0. Building from source needs Go 1.27.
Integration tests
Section titled “Integration tests”dwe validate warns about compose host ports interpolated from a variable dwe test does not remap. A port such as "${VALKEY_PORT:-6379}:6379", where VALKEY_PORT is exported from: vars.ports.valkey, binds the same host port as the live stack in every test run. It always did; now dwe validate and dwe test run say so. The warning appears only in projects that have a workspace/tests/ directory, and it fails dwe validate --strict. Fix every scenario the warning names:
env: vars: ports.valkey: autoA port exported from: services.<name>.ports.<x> is already remapped while that service is enabled in the scenario; it warns only for a scenario that disables the service while its compose file stays in the stack — one declared in the root compose file, for instance. A required: true service cannot be disabled, so it stays remapped and never warns. See Interpolated host ports.
A warning that names no scenarios means no active exports.env rule traces the variable — it comes from a hand-written .env, the host environment, or a rule whose when: is falsy — so no scenario setting can fix it. Export the variable from: vars.<path> and add env.vars: { <path>: auto } to each scenario, or export it from: services.<name>.ports.<x>.
Skipping straight to 0.6.2? The env.vars: { <path>: auto } step above is no longer needed there — a variable exported from: vars.<path> is remapped automatically (see Upgrading to 0.6.2).
A literal host port behind an interpolated bind address now blocks dwe test run. "${BIND:-127.0.0.1}:8080:80" publishes the literal host port 8080, which collides with the live stack; the scanner used to miss it. Model the port under services.<name>.ports and interpolate it, as for any literal host port, or pass --skip-isolation-check.
dwe validate warns when a host script builds its own compose project name. Inside dwe test, shell steps and scripts get the copy’s name in COMPOSE_PROJECT_NAME; a script that passes docker compose -p "${PROJECT_PREFIX:-dwe}-${PROJECT_NAME:-myproj}" ignores it and addresses the live stack. The new tests.host_project_name warning names the file and the line. Like the port warning, it appears only in projects with a workspace/tests/ directory and fails dwe validate --strict. Keep the old expression as the fallback:
PROJECT="${COMPOSE_PROJECT_NAME:-${PROJECT_PREFIX:-dwe}-${PROJECT_NAME:-myproj}}"See Host scripts that build their own compose project name for what the check reads and what it skips.
Deploy
Section titled “Deploy”The built-in deploy pipeline now brings a stopped stack back up. Its up step carries check: {type: builtin, cmd: containers_running}, so it runs on every deploy instead of being skipped by the journal after the first success. The built-in pipeline therefore never reports already up-to-date any more: a deploy of an unchanged, running project is a quick docker up --wait plus a probe.
The first deploy after upgrading sees a config change, once. A check: is part of the project config hash, so a project deployed with the built-in pipeline no longer matches its recorded hash. An interactive dwe deploy run shows the Deployed config changed. Choose action: selector one time — pick Apply changes. A non-interactive deploy applies the change without asking. Either way the project-level steps (up, info, the success message) run once; per-service steps are not affected.
An ejected or hand-written workspace/deploy.yml keeps the old behaviour. It replaces the built-in pipeline whole, so its docker up step still has no check: and is still skipped once recorded. Add the same check to get the fix — this triggers the same one-time config change:
- name: up type: dwe cmd: "docker up --wait" check: type: builtin cmd: containers_runningSee containers_running for what the check accepts.
Upgrading to 0.6.0
Section titled “Upgrading to 0.6.0”Three groups, in the order you will hit them.
Hard failures on load
Section titled “Hard failures on load”The project does not load at all until these are fixed, and every command reports the same error. The first two name the file; the third names the offending exports.env index instead, since an export rule carries no source layer.
The top-level state: key is gone. Delete it. It was a free-form string with one consumer and no replacement; free-form values belong under vars:.
# workspace/defaults.yml — delete thisstate: ""If something read it — an exports.env rule with from: state, a ${state} reference — move the value under vars: and update the path, or drop the rule.
The top-level ui: block is gone. Delete it. Its three command-browser knobs had no replacement and the browser now runs with what an absent block always resolved to: top-level groups expanded, empty subtrees collapsed while filtering, type badges on. The hotkeys, parameter form, fallback ladder and mouse behaviour that were documented on the old ui reference page now live under Interactive browser in ../reference/config/commands/index.md.
COMPOSE_PROJECT_NAME is now a reserved .env variable and may not be redeclared. An exports.env rule with that name fails the load:
exports.env[3]: "COMPOSE_PROJECT_NAME" is a reserved system variable and cannot beredeclared as an export rule (reserved: PROJECT, UID, GID, COMPOSE_PROJECT_NAME)Delete the rule — DWE emits the line itself. Check the value before you assume it is the same one. The built-in value is the compose project name DWE passes as -p: project_name from workspace/docker.yml, otherwise <project.prefix>-<project.name>, always lowercased. A hand-rolled rule built from a different expression produced a different string, and anything created under the old name — containers, networks, volumes — becomes invisible to the new one. Compare the two before deleting:
grep COMPOSE_PROJECT_NAME .env # what your rule produceddwe render env | grep COMPOSE_PROJECT_NAME # what DWE will emitIf they differ and you have a running stack, either set project_name in workspace/docker.yml to the old value, or accept the new name and redeploy once from clean.
Silent behaviour changes
Section titled “Silent behaviour changes”Nothing errors. Behaviour is different.
Container commands now decide three runtime defaults themselves. Each covers a different set of command types:
| default | applies to | new behaviour |
|---|---|---|
| workdir and user | service_exec, service_run, daemon | With no workdir:, falls back to the service’s cli.workdir → work_dir_internal → dir_internal — the chain dwe shell uses; workdir: internal opts out. A daemon with no user: inherits the service’s cli.user instead of the image’s USER. |
| TTY | service_exec, service_run | A container terminal only when you launched the command yourself and DWE’s streams are terminals, or the run is bridged. Everything else gets -T, with colour forced. |
| exec mode | service_exec | mode: defaults to exec-or-run instead of exec-or-fail: a stopped service falls back to a one-off docker compose run --rm instead of refusing. |
What to check: any daemon whose target service sets cli.user — the ownership of everything it writes changes, so pin user: explicitly if the old uid mattered. Any command that must never create a container — declare mode: exec-or-fail. Any type: command check: pointing at a mode-less service_exec command — it is now container-creating, and the one-off runs with --no-deps, so it can report success against a stack that is down.
None of the three alters the deployment hash. Run dwe deploy run --force once after upgrading, or a step whose semantics moved reports already up-to-date and is skipped.
Raw docker compose from the project root now scopes to DWE’s project. The generated .env carries COMPOSE_PROJECT_NAME, and .env sits in the compose project directory, so Compose picks it up — above any top-level name: in your compose file. That is the point: a manual docker compose ps finally shows the same containers as dwe status. But a compose file declaring a divergent name: — what dwe validate reports as config.compose_project_name — is now dead config, and resources created under it are no longer visible to a bare compose run. Either align the two, or write name: ${COMPOSE_PROJECT_NAME} in the compose file, which now resolves.
Pipeline logs no longer record every redraw frame. A \r-terminated progress frame is held and overwritten by the next one, so .dwe/logs/<pipeline>.log records one line per committed line. The consequence: tail -f .dwe/logs/deploy.log no longer shows live clone or download progress, only committed lines. The live view on your terminal is unchanged.
Flags and messages
Section titled “Flags and messages”dwe docs llms-txt --output PATH is now --out PATH. There is no alias: the old local --output shadowed the root format flag of the same name, which is why -o failed on this command. -o json is now accepted and ignored here — the document is itself the payload.
Strict-loader error text changed. An unknown field in a pipeline, service.yml, command file, manifest, scenario, setup.yml or translation bundle now reads:
workspace/deploy.yml:12: unknown field "defaults" — allowed here: log, phasesIt used to surface the YAML library’s field defaults not found in type config.DeployConfig. A script or CI job grepping for not found in type needs updating.
dwe validate gained warnings that can fail --strict. A dot-path that does not resolve in the merged config is now reported: an exports.env rule’s from: or when:, and a command’s params.<name>.default_from, params.<name>.options.from or context.<name>.from. These are warnings — a from: with a default: is a legitimate optional path, and a path may live in a local.yml that is not on this machine. But dwe validate --strict treats warnings as errors, so a CI job using it can start failing on a path that was deliberately optional. Give such a rule a default:, or correct the path.
dwe validate also now reports the built-in default pipeline honestly: an empty or all-comment deploy.yml reports at info as has no active content (all comments or empty) — built-in default pipeline is active where it used to report OK. Nothing is broken — the file never ran. If you want to start editing from the built-in pipeline, dwe deploy eject --out workspace/deploy.yml --force writes it there as a commented, editable document.
Older releases
Section titled “Older releases”Releases up to and including v0.5.0 predate this page. Their notes are on the releases page.
See also
Section titled “See also”CHANGELOG.md— every change in every release, not just the breaking ones- Troubleshooting — when the stack misbehaves for reasons unrelated to a version change
- Reference (
reference/) — schemas and config fields