Перейти к содержимому

Типы команд

Типы команд определяют разные контексты выполнения. У каждого типа собственная диспетчерская полезная нагрузка и собственный allowlist полей.

ТипИсполнительПолезная нагрузкаПрименение
shellHost shellcmd или argvЗадачи на хосте (скрипты, git, сборка)
dweDWE CLIcmdВызов подкоманды
scriptScript runnerблок script:Структурированное многофазное выполнение
service_execDocker Compose exec/runcmd или argvОперации в контейнере на существующих/новых контейнерах
service_runDocker Compose runcmd или argvВыполнение в одноразовом контейнере
workflowОркестратор командsteps[]Многокомандные последовательности (отдельный синтаксис, см. ниже)
builtinВнутреннее действие движкаcmd (имя builtin) + withВызов общего builtin движка (например, wait-for-healthy) без подпроцесса
daemonСахар реестраблок daemon: + service + argvОбъявить долгоживущий фоновый контейнер; разворачивается в четыре виртуальные команды (.start / .logs / .stop / .restart)

Все типы кроме script и workflow используют каноничное поле cmd: для своей полезной нагрузки. type: script использует собственный блок script: с фазами run, plan, cleanup. type: workflow использует собственный блок steps: со строковым синтаксисом command: / confirm: / with: / when: — см. Тип: workflow ниже. type: builtin кладёт имя builtin в cmd:, а его параметры в with: — см. Тип: builtin ниже.

Запускает shell-команду на хостовой машине. Используйте для задач, которым не нужен контейнер или DWE binary.

ПолеОбязательноОписание
cmdодно из cmd/argvСтрока shell-команды, передаваемая в sh -c (полная семантика shell)
argvодно из cmd/argvВектор аргументов, исполняемый напрямую без shell
argv_append_fromопциональноShell-выражение на хосте, строки stdout которого дописываются в argv отдельными элементами — см. Вычисляемые аргументы. Требует argv; пустой вывод пропускает команду
workdirопциональноРабочая директория; относительные пути разрешаются относительно корня проекта
chmod-scripts:
type: shell
description: Make all scripts executable
cmd: chmod +x workspace/scripts/**/*.sh
commit-config:
type: shell
description: Commit a generated config file
argv:
- git
- commit
- -m
- "chore: regen config"
- "${files.cfg.path}"
files:
cfg:
access: read
path: "config/generated.yml"
required: true

cmd и argv взаимоисключающи.

Подпроцессы type: shell наследуют окружение родительского процесса плюс значения из блока env: команды. Поверх этого раннер экспортирует небольшой контракт, чтобы shell-сниппеты могли обращаться к хостовому DWE CLI и активному compose-проекту без переоткрытия:

ПеременнаяЗначение
DWE_BINАбсолютный путь к запущенному DWE-бинарнику — используйте его вместо жёсткого пути ./bin/dwe
COMPOSE_PROJECT_NAMEИмя активного compose-проекта (например, dwe-laravel) — docker compose ... подхватывает его без -p
COMPOSE_FILEОбъединённый через двоеточие список путей активных оверлеев, приведённых к абсолютным относительно корня проекта — docker compose ... подхватывает их без флагов -f

Именно это позволяет команде type: shell достучаться до docker compose с тем же набором оверлеев, что и остальной DWE:

hub.chown-src-host:
type: shell
description: Chown the host-side mount via the running container
cmd: |
"$DWE_BIN" docker exec -u root app-main -- \
chown -R www-data:www-data /workspace/src

COMPOSE_FILE опускается, если файлы оверлеев не настроены; COMPOSE_PROJECT_NAME опускается, если имя проекта не задано. Записи, уже объявленные в блоке env: команды, сохраняются, но запись из контракта побеждает при коллизии ключей — os/exec Go использует последнюю запись для дубликатов ключей, а контракт дописывается после env:.

Вызывает другую подкоманду dwe, используя текущий запущенный бинарник. Это позволяет избежать жёсткого пути ./bin/dwe в определениях команд и делает вызовы переносимыми.

ПолеОбязательноОписание
cmdдаСтрока подкоманды (без пути к бинарнику); передаётся через sh -c
db.up:
type: dwe
private: true
description: Start the database container in the background
cmd: "docker up db"
app.install:
type: dwe
description: Install the Laravel application via installer container
cmd: "compose raw --bare -- --progress tty -f compose/installer.yml run --rm -u ${host.uid}:${host.gid} app-install"

workdir не допускается для type: dwe (подкоманда наследует корень проекта).

Выполняет файл shell-скрипта со строгим контрактом окружения, внедряемым раннером. Скрипты — структурное исключение в системе команд: команда type: script использует собственный блок script: с полями run, plan, cleanup вместо каноничного поля cmd:. Это сделано намеренно: скрипты — это конфигурации запуска, а не отдельные команды.

db.dump-create:
type: script
description: Create a database dump file
params:
database: { default_from: vars.db.database, pattern: ^[a-zA-Z0-9_-]+$ }
dump_dir: { default_from: vars.db.backup_dir, required: true }
files:
dump:
access: write
path: "${param.dump_dir}/${param.database}_{{ now | date \"2006-01-02\" }}.sql.gz"
mkdir: true
overwrite: true
on_error: remove
env: DUMP_FILE
env:
DB_NAME: "${param.database}"
MYSQL_PWD: "${vars.db.password}"
script:
path: workspace/scripts/db/dump-create.sh
shell: bash
ПолеОписание
script.shellИнтерпретатор для вызова (по умолчанию sh)
script.pathОдиночный скрипт (простой режим)
script.runОсновной скрипт (фазовый режим)
script.planОпциональный пре-скрипт (фазовый режим)
script.cleanupОпциональный always-runs-after скрипт (фазовый режим)

Режимы взаимоисключающи: либо path отдельно, либо run (с опциональными plan / cleanup).

Пути скриптов в script.path, script.run и т. д. разрешаются относительно корня проекта — никогда относительно workdir. Это делает безопасным размещение скриптов под workspace/scripts/ независимо от того, откуда команда запущена.

Раннер всегда внедряет следующие env-переменные в процесс скрипта:

ПеременнаяОписание
DWE_ROOTАбсолютный корень проекта
DWE_BINАбсолютный путь к запущенному DWE бинарнику
DWE_COMMAND_IDПолный идентификатор данного вызова
DWE_TEMP_DIRWritable temp-директория, ограниченная этим вызовом (автоудаление)
DWE_NONINTERACTIVE1, когда родительский RunContext имеет NonInteractive: true (установлено commands --yes / -y), или раннер наследует DWE_NONINTERACTIVE=1 из собственного окружения (например, вложенные вызовы). Иначе 0. Само по себе обнаружение TTY этого не переключает — скрипты, которым нужно по-разному вести себя на не-TTY, должны проверять собственный stdin.
DWE_PARAMS_JSONРазрешённые params как JSON-объект
DWE_CONTEXT_JSONРазрешённый context как JSON-объект
DWE_FILES_JSONJSON-объект, отображающий идентификаторы файлов в {path}

Используйте DWE_BIN вместо жёсткого пути ./bin/dwe:

#!/bin/bash
set -euo pipefail
TMPFILE=$(mktemp "${DUMP_FILE}.XXXXXX")
trap 'rm -f "$TMPFILE"' EXIT
"$DWE_BIN" docker exec -T -e MYSQL_PWD db -- \
mariadb-dump -u"$DB_USER" "$DB_NAME" | gzip > "$TMPFILE"
mv "$TMPFILE" "$DUMP_FILE"

DWE не линтит shell-скрипты. Мы рекомендуем установить ShellCheck и прогонять его по workspace/scripts/ как часть вашего локального процесса или CI. Это внешний инструмент, полностью опциональный, но он ловит классы багов, которые в этом контексте наиболее болезненны: неэкранированные подстановки, отсутствующий set -euo pipefail, сломанные обработчики trap, тонкие проблемы с кавычками вокруг $DUMP_FILE / $DWE_BIN и несоответствующий синтаксис тестов.

Окно терминала
# one-off check
shellcheck workspace/scripts/db/dump-create.sh
# whole tree
shellcheck workspace/scripts/**/*.sh

Если вы его внедрите, зафиксируйте диалект shebang-ом или директивой, чтобы ShellCheck выбрал правильные правила (особенно когда задано script.shell: bash):

#!/bin/bash
# shellcheck shell=bash
set -euo pipefail

Рекомендуемые соглашения для скриптов под workspace/scripts/ (независимо от ShellCheck — это хорошие практики в любом случае):

  • Начинайте с set -euo pipefail — fail fast, никаких тихих багов с unset-переменными.
  • Экранируйте каждую подстановку: "$DUMP_FILE", "$DWE_BIN", "$DB_NAME".
  • Используйте trap 'rm -f "$TMPFILE"' EXIT для временных файлов; раннер очищает $DWE_TEMP_DIR за вас, но временные файлы отдельных шагов всё равно требуют собственных trap.
  • Считайте unset env-переменные ошибками через ${VAR:?error message}, когда скрипт не должен запускаться без них.

Запускает команду внутри существующего контейнера через docker compose exec. Поле mode: управляет тем, что происходит, когда целевой контейнер не запущен — см. разрешение mode ниже.

ПолеОбязательноОписание
serviceдаИмя compose-сервиса
cmd / argvодно изСтрока shell-команды ИЛИ сырой argv
argv_append_fromопциональноShell-выражение, строки stdout которого дописываются в argv. Выполняется на хосте, а не в контейнере — см. Вычисляемые аргументы
modeопциональноexec-or-fail (по умолчанию), exec, run или exec-or-run — см. разрешение mode
userопциональноКонтейнерный пользователь, под которым запускаться. Полный список принимаемых значений и правила fallback см. в Разрешение user.
workdirопциональноКонтейнерный workdir; рендерится через шаблоны
workdir_fromопциональноТочечный путь в объединённый конфиг, разрешающийся в строку workdir
compose_argsопциональноДополнительные флаги, передаваемые в docker compose exec/run (шаблонизированные)
РежимКогда контейнер запущенКогда контейнер не запущен
exec-or-fail (по умолчанию)запускается через docker compose execотказывает с ясной ошибкой DWE, предлагающей dwe docker up <svc>
execзапускается через docker compose execвсё равно вызывает compose exec; docker выдаёт собственную (загадочную) ошибку
runвсегда запускает свежий эфемерный контейнер через docker compose run --rmто же
exec-or-runзапускается через docker compose execтихо переключается на docker compose run --rm; выводит жёлтое предупреждение, чтобы поведение с эфемерным контейнером было видно

Выбирайте exec-or-fail (значение по умолчанию) для обычных интерактивных инструментов, зависящих от персистентного состояния контейнера (базы данных, серверы приложений и т. д.) — отсутствующий контейнер должен проявиться как actionable ошибка, а не как разовый запуск с побочными эффектами. Выбирайте exec-or-run только для инструментов, которые легитимно работают как эфемерные запуски (mc, composer install на свежем checkout и т. д.), и где вы понимаете, что между вызовами состояние не сохранится. runner.mode подчиняется тому же enum и тем же правилам приоритета, что и runner.user.

Поле user: на service_exec / service_run (и в блоке переопределения runner:) принимает следующие значения:

ЗначениеЭффект
(опущено / пусто)Откатывается к services.<svc>.cli.user целевого сервиса. Если cli.user тоже пуст, флаг --user не передаётся и контейнер запускается под директивой USER образа. Это значение по умолчанию для новых команд — объявите cli.user один раз на сервисе, и каждая команда, нацеленная на этот сервис, наследует его.
currentПередаёт --user <HOST_UID>:<HOST_GID>, чтобы процесс контейнера запускался под хостовым пользователем. Используйте это, когда команда пишет файлы в bind-mount директории и они должны принадлежать хостовому пользователю.
rootПередаёт --user root. Используйте для разовых операций, требующих повышенных привилегий внутри контейнера (установка пакетов, chown и т. д.).
internalНе передаёт флаг --user и пропускает fallback к cli.user. Контейнер запускается под встроенной директивой USER образа (или root, если образ её не объявляет). Используйте это для явного отказа от cli.user для конкретной команды (например, entrypoint, который должен запускаться под пользователем по умолчанию образа).
любая другая строкаПередаётся как есть в --user <value>. Принимает те же формы, что и docker --user: имя пользователя (www-data), числовой UID (1000) или UID:GID (1000:1000).

Приоритет сверху вниз:

  1. runner.user (если блок runner: его задаёт).
  2. Верхнеуровневый user: на команде.
  3. services.<svc>.cli.user разрешённого целевого сервиса (после редиректа runner.service).
  4. Без флага --user (USER образа).

runner.service перенаправляет цель до lookup-а cli.user, поэтому fallback читает cli.user из перенаправленного сервиса, а не из исходного.

Установка user: internal коротко замыкает шаг 3 — резолвер трактует internal как явное решение и никогда не читает cli.user.

composer-install:
type: service_exec
description: Install PHP dependencies via Composer
service: app-main
user: current
workdir_from: services.main.work_dir_internal
mode: exec-or-run
argv:
- composer
- install
- --prefer-dist
- --no-interaction
db.create:
type: service_exec
description: Create a database in the db container
service: db
mode: exec-or-run
params:
database: { required: true, pattern: ^[a-zA-Z0-9_-]+$ }
env:
MYSQL_PWD: "${vars.db.password}"
cmd: "mariadb -u${vars.db.user} -e 'CREATE DATABASE IF NOT EXISTS `${param.database}`;'"

compose_args — это список дополнительных флагов, вставляемых перед генерируемыми раннером флагами --user / --workdir / -e. Используйте его для -T, -d, --name, --rm и т. д.

compose_args:
- "-T" # disable TTY (useful when piping)
- "--name"
- "${param.database}_loader"

Для контейнеров env-переменные внедряются через окружение docker-процесса плюс флаги -e KEY (только имя), так что секретные значения никогда не появляются в argv (и потому не попадают в ps или /proc/<pid>/cmdline).

То же, что service_exec, но всегда использует docker compose run --rm, чтобы запустить свежий, одноразовый контейнер. Используйте для разовых задач, которые не должны требовать уже запущенного контейнера.

artisan-tinker:
type: service_run
service: app-main
user: current
workdir_from: services.main.work_dir_internal
argv: [php, artisan, tinker]

mode — недопустимое поле для service_run (он всегда использует docker compose run --rm) — любое указание mode: отвергается на этапе загрузки; опускайте его.

argv_append_from принимается здесь на тех же условиях, что и в service_exec: выражение выполняется на хосте, а строки его вывода дописываются в argv — см. Вычисляемые аргументы.

И service_exec, и service_run принимают блок runner: для переопределения service / user / workdir / workdir_from (а для service_exec — также mode) без дублирования остального определения. runner.mode отвергается для service_run, который всегда использует docker compose run. Ненулевые поля в runner: побеждают верхнеуровневые поля.

queue-worker:
type: service_exec
service: app-main
argv: [php, artisan, queue:work]
runner:
user: root
workdir: /workspace
mode: run

Workflow запускает упорядоченную последовательность других команд с опциональными подтверждениями и условными шагами. Workflow — единственный способ скомпоновать несколько команд за одним идентификатором.

Примечание: Шаги workflow используют строковый синтаксис command: / confirm: / with: / when:. Условия when: внутри workflow — выражения на мини-языке строк, отличные от типизированных when: / check:, используемых в шагах пайплайнов (см. deploy).

bootstrap:
type: workflow
description: Full bootstrap — start db, create database, install deps, migrate
steps:
- command: db.start
- command: services.main.db.create
- command: services.main.composer-install
- command: services.main.key-generate
- command: services.main.migrate

Каждый шаг — это либо command-шаг, либо confirm-шаг, либо parallel-шаг (взаимоисключающи).

ПолеИспользуется вОписание
commandcommand-шагПолный идентификатор вызываемой команды
withcommand-шагКарта переопределений параметров (шаблонизированные значения)
confirmconfirm-шагТекст запроса, показываемый перед продолжением
parallelparallel-шагКонкурентная группа листовых подшагов-команд (см. Параллельные подшаги)
whencommand / parallel-шагУсловие; шаг (или вся группа) пропускается, если ложь
continue_on_errorcommand / parallel-шагОшибка логируется как warning; workflow продолжается

Значения with: рендерятся относительно контекста рендера родительского workflow, поэтому могут тянуть из конфига, параметров и хостовых хелперов:

- command: db.create
with:
database: "${vars.db.database}"
- command: services.main.db.dump-deploy
with:
target_database: "${param.target_db}"
dump_dir: "${param.backup_dir}"

Выражения when: сначала рендерятся, затем классифицируются в одну из трёх форм:

  1. Булев литералtrue, false, 1, 0, пустая строка. После рендера это быстрый путь.
  2. Builtin-предикат — файловые проверки относительно корня проекта.
  3. Shell-командаcmd: <command>; вычисляется через sh -c; exit 0 = true.
steps:
- command: services.main.composer-install
when: "file-missing services/main/src/vendor/autoload.php"
- command: bootstrap-cache-warm
when: "{{ if .Params.warm }}1{{ else }}0{{ end }}"
- command: install-deps
when: "cmd: test ! -d services/main/src/vendor"

Builtin-предикаты (путь — относительно корня проекта):

ПредикатИстина когда
dir-exists <path>путь — существующая директория
dir-missing <path>путь отсутствует или не директория
dir-empty <path>путь отсутствует или не имеет записей
dir-not-empty <path>путь — директория с как минимум одной записью
file-exists <path>путь — существующий обычный файл
file-missing <path>путь отсутствует или не обычный файл

Помечает шаг, который может упасть без прерывания workflow. Ошибка логируется как warning, затем выполнение продолжается:

steps:
- command: optional-cache-warm
continue_on_error: true
- command: services.main.migrate

Недопустимо на confirm-шагах.

steps:
- confirm: "This will drop the database. Continue?"
- command: db.drop
with:
database: "${vars.db.database}"

Confirm-шаги тихо пропускаются под --yes или DWE_NONINTERACTIVE=1. Иначе huh выводит запрос на TTY, а fallback [y/N] через stdin обрабатывает piped-ввод.

Шаг workflow может объявить блок parallel:, который разворачивает группу подшагов конкурентно. Это зеркалит схему parallel: пайплайна в deploy → Группы параллельных шагов — те же регуляторы max_concurrent / fail_fast и тот же live-block UI — но живёт внутри workflow, чтобы группу можно было переиспользовать между пайплайнами и вызывать ad-hoc через dwe commands.

services.all.composer-install:
type: workflow
description: Run composer install across every app service in parallel
steps:
- parallel:
max_concurrent: 4
fail_fast: true
steps:
- command: services.main.composer-install
- command: services.api.composer-install
- command: services.worker.composer-install
- command: services.admin.composer-install
ПолеОбязательноПо умолчаниюОписание
max_concurrentопциональноmin(NumCPU, len(steps))Верхняя граница одновременно работающих горутин
fail_fastопциональноtrueПри true первая ошибка подшага отменяет соседей через context; при false все подшаги выполняются, а ошибки агрегируются через errors.Join
always_show_outputопциональноfalseПри true захваченные stdout/stderr каждого подшага выгружаются между полосами ───── output: <command> ───── / ────────────────── после завершения группы — включая успешные подшаги. По умолчанию сохраняется поведение «только при ошибках». Пропущенные и отменённые подшаги никогда не дают вывода и не затрагиваются.
stepsобязательноПодшаги; каждый должен быть листовым command-шагом (без confirm, без вложенного parallel)

when: и continue_on_error: уровня группы допустимы на шаге, несущем parallel: (они управляют всей группой). Per-подшаговые when: и continue_on_error: тоже допустимы и ведут себя так же, как в последовательном workflow. Подшаговый when: вычисляется один раз на preflight (до запуска любой горутины), так что предикаты с побочными эффектами не выполняются дважды.

  1. Минимум два подшага — список parallel.steps: длиной 0 или 1 отвергается на этапе валидации.
  2. Без вложенных parallel — подшаг не может сам объявить parallel:. Распрямите структуру или разделите на отдельные шаги workflow.
  3. Без confirm в подшагахconfirm:-шаги интерактивно запрашивают подтверждение; параллельный live-block UI владеет терминалом и не может разместить запрос. Подшаг, ссылающийся на команду с confirmation: true, требует --yes (или DWE_NONINTERACTIVE=1); preflight иначе отвергает группу, а runtime-гард ловит транзитивные вызовы подтверждения.
  4. Без with: на контейнере — параллельный контейнер не имеет собственных параметров; каждый подшаг несёт собственный with:.
  • Ad-hoc: dwe commands <workflow-id> (алиас dwe cmd <workflow-id>) запускает live-block самого workflow на терминале. Ctrl-C распространяется как SIGINT через signal.NotifyContext, что отменяет группу и даёт детям до 5 с на выход до эскалации до SIGTERM.
  • Внутри последовательного шага пайплайна: когда cmd: последовательного шага пайплайна разрешается в workflow с блоком parallel:, футер пайплайна приостанавливается на время тела шага (существующий контракт SuspendForExec / ResumeAfterExec), и workflow рендерит собственные строки блока в этом промежутке. Счётчик шагов пайплайна продвигается ровно на один — подшаги НЕ считаются шагами пайплайна.
  • Внутри параллельной группы пайплайна ИЛИ другого параллельного workflow: отвергается во время выполнения. Только один live-block может владеть терминалом одновременно. Ошибка — сентинел ErrWorkflowNestedParallel.
  • Строки блока (TTY): у каждого подшага есть строка вида <спиннер-или-глиф> [<elapsed>] [<i>/<N>] <command>[: <последняя-строка>]. Последняя строка отслеживает как вывод, завершённый переводом строки, ТАК И кадры carriage-return, поэтому прогресс-бары curl / wget / docker pull видны на месте (строка обновляется каждый раз, когда дочерний процесс пишет кадр).
  • Сводка в конце блока (TTY): когда параллельная группа завершается, строки замораживаются с финальными глифами (✓/✗/◎) в scrollback, а вместо них печатается однострочный сводный футер: ✓ [<elapsed>] parallel: <workflow-id> зелёным при успехе, ✗ ... красным, когда хотя бы один подшаг упал. Per-подшаговые строки ✓ [i/N] Done: … НЕ переиздаются на TTY, потому что та же информация уже есть в замороженных строках блока выше.
  • Не-TTY режим (CI / piped stdout): нет live-блока. Каждый подшаг печатает строку терминального состояния (✓ [i/N] Done, ◎ [i/N] Skipped, ✗ [i/N] Failed, ◎ [i/N] Cancelled), за которой следует обычный текстовый сводный футер (✓ [<elapsed>] parallel: <workflow-id>).
  • Дампы при сбое: захваченный вывод упавшего подшага воспроизводится между полосами ───── output: <command> ───── / ────────────────── на stderr в ОБОИХ режимах — TTY и не-TTY — потому что live-строка не может показать весь буфер. Верхняя полоса именует подшаг, чтобы дампы при множественных сбоях оставались атрибутируемыми, а ANSI escape-последовательности в выводе ребёнка пересылаются как есть, чтобы цвета пережили round-trip.
  • Цвет для параллельных подшагов: каждый ребёнок запускается с per-подшаговым псевдо-терминалом, чтобы инструменты, выбирающие цветной вывод по isatty(STDOUT) — Pest, PHPUnit/Symfony Console, ripgrep, fzf, CLI на базе lipgloss и т. д. — продолжали испускать ANSI-коды, несмотря на то что захваченный вывод потребляется внутренним line tee, а не терминалом пользователя. Мастер-сторона PTY читается в tee, так что прогресс по строкам (например, \r-кадры docker pull) и буферизованный дамп видят один и тот же поток байтов. В качестве страховки дети также наследуют CLICOLOR_FORCE=1, FORCE_COLOR=1 и COLORTERM=truecolor (пересылаются в контейнер через -e для type: service_exec / service_run), чтобы env-зависимые инструменты тоже сохраняли цвет независимо от обнаружения PTY.

Объединённые stdout/stderr каждого подшага захватываются в .dwe/logs/parallel/workflow/<workflow-id>/<sub-command>.log. Только кадры, завершённые переводом строки, пишутся в файл лога (прогресс-кадры с carriage-return остаются в live-строке и отбрасываются из логов), так что файл остаётся читаемым без спама \r.

Именование подшагов и переопределения из пайплайна

Заголовок раздела «Именование подшагов и переопределения из пайплайна»

Каждый подшаг workflow может задать явный name: (опционально). При отсутствии эффективное имя по умолчанию — указанная command. Это имя — ручка, которую пайплайн использует для sub_step_overrides: — см. deploy → Нацеливание на подшаги workflow с переопределениями. Когда два подшага в одном workflow имеют одно и то же эффективное имя, переопределение из пайплайна, нацеленное на это имя, отвергается на этапе планирования как неоднозначное; задайте подшагам явные name:, чтобы их различать.

commands:
dumps-deploy:
type: workflow
steps:
- parallel:
steps:
- name: deploy-main # explicit name → pipeline can target it
command: services.main.db.dump-deploy
- name: deploy-stock
command: services.main.db.dump-deploy-stock

Workflow никогда не знают, прикреплены ли к ним переопределения, — они непрозрачны для решений о гейтировании. Вызов workflow с переопределениями случается только через шаг пайплайна, объявляющий sub_step_overrides:.

Вызывает по имени внутреннее builtin-действие движка — тот же реестр, что пайплайны используют в deploy.yml / reset.yml / lifecycle.yml. Подпроцесс не порождается; builtin работает in-process на Go.

Используйте type: builtin, когда команда иначе переизобретёт логику, которую движок уже предоставляет (ожидание здоровых контейнеров, удаление томов проекта, обеспечение директорий сервисов, …). Это правильный выбор для любого листового шага, которому нужно структурированное, аудитируемое выполнение вместо shell-пайплайна, — и это обходит ловушку встраивания {{...}} из других инструментов (например, docker inspect --format) внутрь cmd: команды type: shell, которые иначе столкнулись бы с рендером шаблонов команды.

ПолеОбязательноОписание
cmdдаИмя builtin (например, docker_wait_healthy)
withопциональноКарта параметров, передаваемых builtin
db.wait:
type: builtin
private: true
description: Wait for the db container to become healthy
cmd: docker_wait_healthy
with:
services: [db]
timeout: 120s
interval: 2s

Строковые значения внутри with: — включая записи во вложенных списках и картах — рендерятся в template-пространстве команды (${...}, {{ ... }}) до того, как builtin их увидит. Это позволяет параметризовать builtin lookup-ами из конфига, параметрами или контекстом:

db.wait-target:
type: builtin
cmd: docker_wait_healthy
params:
service: { required: true }
with:
services: ["${param.service}"]
timeout: "${docker.wait_timeout}"

Не-строковые скаляры (булевы, целые) пропускаются без изменений.

Список доступных builtin, их параметры и поведение задокументированы один раз в deploy → Доступные builtin. Те же builtin доступны из команд type: builtin — один общий реестр.

Наиболее полезные для экспозиции в качестве команд builtin, как правило, — это долгоживущие, идемпотентные: в особенности docker_wait_healthy, который предназначен для вызова всякий раз, когда проекту нужно блокироваться до тех пор, пока стек (или конкретный сервис) не станет здоровым.

type: builtin — это листовое действие; он отвергает каждое поле, специфичное для других типов: argv, script:, steps:, service, compose_args, workdir / workdir_from, user, mode и runner:. Используйте params: / context: / env: / files: / messages: как на любом другом типе для входов, экспозиции env и стилизованного вывода.

type: daemon — декларативная форма для долгоживущих, параметризованных фоновых процессов внутри сервисов DWE (канонический пример: Laravel queue worker). Один YAML-блок разворачивается во время загрузки реестра в четыре полноценные виртуальные команды:

Виртуальный IDПоведениеБлокирующая
<base>.startdocker compose run -d --name <full> ...нет
<base>.logsdocker logs -f --tail=100 <full>да — Ctrl-C отсоединяется (контейнер продолжает работать)
<base>.stopdocker stop -t <timeout> <full>нет
<base>.restart<base>.stop затем <base>.startнет

Каждая виртуальная команда появляется в реестре, браузере dwe cmd, completion, inspect и может ссылаться из workflow. Исходная команда <base> не запускается сама по себе — запускаются только четыре виртуальные команды.

Имена контейнеров автоматически префиксуются разрешённым именем compose-проекта — project_name из workspace/docker.yml, если оно задано, иначе ProjectConfig.FullName() (<prefix>-<name>) — так что демоны попадают в ту же project-область, что и сервисы под управлением compose (и один проект может работать на нескольких checkout-ах одновременно). Каждый контейнер несёт стандартизированные метки, чтобы dwe status daemons, completion и _auto_reap_daemons могли найти их через docker psбез отдельного файла состояния.

commands:
queue:
type: daemon
description: "Laravel queue worker"
service: app-main # literal compose service name (no ${...})
workdir_from: services.main.work_dir_internal
user: www-data
env:
QUEUE_CONNECTION: redis
params:
name:
default: default
pattern: ^[a-zA-Z0-9_-]+$
argv:
- php
- artisan
- queue:listen
- --timeout=0
- --queue=${param.name}
daemon:
container_template: "php_queue_${param.name}"
on_already_running: error # error | noop
auto_remove: true # default true → adds --rm
stop_timeout: 10s

service, workdir/workdir_from, user, env, params, argv, compose_args следуют той же семантике, что и type: service_run. Конфигурация, специфичная для daemon, целиком живёт под блоком daemon:.

ПолеОбязательноПо умолчаниюОписание
container_templateдаШаблон имени контейнера; рендерится в template-пространстве команды и префиксуется <project.full>-. После рендера должен соответствовать ^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$.
on_already_runningопциональноerrorerror прерывает .start, если контейнер уже существует; noop делает .start идемпотентным.
auto_removeопциональноtrueПри true .start добавляет --rm, чтобы контейнер удалялся по остановке.
stop_timeoutопционально10sСтрока длительности. Конвертируется в целые секунды для docker stop -t <secs>; значения ниже 1s округляются вверх до 1s (никогда 0).
<project.full>-<rendered container_template>

project.full — это разрешённое имя compose-проекта: project_name из workspace/docker.yml, если задано, иначе ProjectConfig.FullName() (<prefix>-<name>, если задан prefix:, иначе <name>) — так что демоны попадают в ту же project-область, что и сервисы под управлением compose. Post-render regex ^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$ — авторитетная защита; недопустимые символы в отрендеренных значениях шаблона дают сбой во время выполнения, даже если pattern: параметра случайно их разрешил.

Каждый контейнер daemon несёт три метки, чтобы docker ps был единственным источником истины:

  • dwe.project=<project.full>
  • dwe.daemon.id=<base> (например, services.main.queue)
  • dwe.daemon.params=<json> (например, {"name":"emails"}) — производится через encoding/json.Marshal для безопасного round-trip через кавычки, обратные слэши и управляющие символы

dwe status daemons, completion --set и _auto_reap_daemons все фильтруют по этим меткам.

  • .start — выпускает docker compose run -d --no-deps --entrypoint "" [--rm] --name <full> [--user …] [--workdir …] -e K1 -e K2 --label dwe.project=… --label dwe.daemon.id=… --label dwe.daemon.params=… <service> <argv…>. Значения env передаются через окружение дочернего процесса (cmd.Env), никогда через хостовый argv, так что секреты не появляются в ps или /proc/<pid>/cmdline. --no-deps оставляет работающий стек нетронутым; --entrypoint "" гарантирует, что фактически запускается пользовательский argv:. При on_already_running: error плюс ошибке конфликта имён docker builtin выдаёт ErrDaemonAlreadyRunning; при noop та же ошибка проглатывается, и .start завершается успешно.
  • .logs — запускает docker logs -f --tail=100 <full> на переднем плане. Ctrl-C посылает SIGINT только процессу docker logs (мягкое отсоединение через cmd.Cancel); контейнер никогда не получает сигнал. Если контейнер не запущен, .logs ошибается с подсказкой, указывающей на .start.
  • .stop — запускает docker stop -t <stop_timeout-в-секундах> <full>. Отсутствующий контейнер не ошибка (идемпотентная остановка).
  • .restart — виртуальный type: workflow из <base>.stop, за которым следует <base>.start. Шаги workflow явно прокидывают каждый объявленный param.<name> через with:, так что dwe cmd queue.restart --set name=emails перезапускает демон emails (а не default).

dwe validate и загрузочный cmd.Validate() обеспечивают:

  • service: обязателен и должен быть литеральным — никаких ${...} или {{...}}. (Параметризованный service: намеренно вне области v1, чтобы метка dwe.daemon.id оставалась стабильной.)
  • daemon.container_template обязателен и непуст.
  • daemon.on_already_running — одно из error / noop (пусто = default error).
  • daemon.stop_timeout парсится через time.ParseDuration и строго положителен.
  • Каждый ${param.X}, упомянутый в container_template, должен быть объявлен в params: И нести pattern: (рекомендательно — runtime-регекс на отрендеренном имени контейнера — авторитетный гейт).
  • Синтетические идентификаторы (<base>.start, .logs, .stop, .restart) не должны конфликтовать ни с одной явной командой в реестре.

.logs интерактивна — она тейлит вывод контейнера на переднем плане и отсоединяется через Ctrl-C. Как и confirm, она отвергается где бы то ни было внутри группы шагов parallel: на этапе планирования (deploy / lifecycle пайплайны и parallel-блоки workflow), независимо от --yes. .start, .stop и .restart могут появляться внутри параллельных групп.

Всякий раз, когда запускается dwe stop (независимо от наличия lifecycle.yml), синтетическая фаза _auto_reap_daemons дописывается в начало stop-пайплайна. Она перечисляет каждый контейнер, помеченный dwe.project=<full> с непустым dwe.daemon.id, и останавливает их параллельно. Опций отказа нет; фаза видна в выводе плана. Форму stop-пайплайна см. в lifecycle.md.

Если lifecycle.yml отсутствует, dwe stop всё равно работает (только с фазой _auto_reap_daemons плюс дефолтным сообщением Project is stopped. Have a nice day!).

  • Значения параметров попадают в dwe.daemon.params как JSON-метки, которые docker inspect экспонирует любому с доступом к docker-сокету на хосте. Не помещайте секреты в params:. Используйте вместо этого env: — env-значения передаются через окружение контейнера (docker compose run -e KEY со значением в cmd.Env), никогда через хостовый процессный argv, так что не появляются в ps или /proc/<pid>/cmdline.
  • Регекс имени контейнера применяется после рендера — недопустимые символы в отрендеренных значениях параметров — жёсткая runtime-ошибка, даже если YAML-pattern: случайно их разрешил. Проверка pattern: параметров валидатором рекомендательна; регекс отрендеренного имени — авторитетная защита.
  • Параметризация service: отвергается в v1 — метка dwe.daemon.id должна быть стабильной между перезапусками, чтобы completion, status и reap могли надёжно соотносить состояние между вызовами.

Исходная команда daemon отвергает поля, конфликтующие с её декларативной формой: script:, steps:, cmd: (действие неявно), mode. Используйте params: / context: / env: / files: / messages: / argv / service / workdir / workdir_from / user / compose_args / runner как на любом раннере сервиса. Всё это перетекает в виртуальный вызов .start.

argv_append_from тоже отвергается, хотя daemon и строит argv: разворачивание упаковывает этот argv в синтетическую команду .start, где задокументированное «пустой вывод → пропуск» читалось бы как молчаливый отказ запустить демона. См. Вычисляемые аргументы.

Окно терминала
# Start a worker for the "emails" queue
dwe cmd queue.start --set name=emails
# Tail it (Ctrl-C detaches, container stays)
dwe cmd queue.logs --set name=emails
# Check what's running
dwe status daemons
# Restart it
dwe cmd queue.restart --set name=emails
# Stop one daemon
dwe cmd queue.stop --set name=emails
# Stop everything (reaps all daemons in this project automatically)
dwe stop

workdir принимает шаблонизированный путь. Относительные пути разрешаются относительно корня проекта для хостовых раннеров (type: shell, type: script) и относительно файловой системы контейнера для раннеров сервисов.

workdir_from допустим для service_exec / service_run / daemon и читает строку из объединённого конфига:

workdir_from: services.main.work_dir_internal

Когда заданы и workdir, и workdir_from, побеждает workdir_from — тот же паттерн «конфиг побеждает, литерал — страховочная сетка», что и у params.*.default_from. Внутри блока runner: действует то же правило между runner.workdir_from и runner.workdir.

Порядок разрешения:

flowchart LR
    F[workdir_from] -- нестрока --> X[ошибка]
    F -- пусто/отсутствует --> W[workdir]
    F -- строковое значение --> U[использовать]
    W -- пусто --> N[без флага --workdir]
    W -- непусто --> U
  • workdir_from разрешается в непустую строку → использовать её.
  • workdir_from отсутствует в конфиге или разрешается в пустую строку → откатиться к литеральному workdir.
  • workdir_from разрешается в нестроковое значение → жёсткая ошибка (баг конфигурации).
  • Ни одно не задано → раннер не передаёт --workdir (применяется default контейнера).

service, workdir и workdir_from рендерятся через template-пространство команды до разрешения, так же как argv, cmd и compose_args. Это позволяет одному определению нацеливаться на несколько сервисов без дублирования:

hub.chown-src:
type: service_run
private: true
params:
service: { type: string, required: true, pattern: '^[a-z0-9_-]+$' }
service: app-${param.service}
workdir_from: services.${param.service}.work_dir_internal
user: root
argv: [sh, -c, "chown -R www-data:www-data /workspace/src"]

Затем пайплайн (или другая команда) вызывает то же определение per-сервис через --set service=<name>. Поля блока runner: (runner.service, runner.workdir, runner.workdir_from) рендерятся идентично.