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

validate.yml

Проверки готовности проекта.

workspace/validate.yml объявляет проверки готовности уровня проекта. CLI использует их из двух точек входа:

  • dwe validate — запускает каждую проверку (плюс YAML-shape валидаторы в доменах config, templates, commands, translations и bridge, плюс environment-probe’ы в домене env) и выводит диагностику.
  • Хук preflight в dwe deploy run, dwe run, dwe stop, dwe restart и dwe reset run — запускает подмножество проверок, связанных с соответствующей стадией, до любого побочного эффекта на Docker, git или файловую систему.

Цель — заранее показать проблемы, которые пользователь может починить («вы не залогинены в ghcr.io», «DATABASE_URL пуст в .env», «VPN лёг») ДО того, как шаги деплоя упадут на середине с непонятными ошибками.

Команда validate запускает шесть доменов в дополнение к существующим YAML-shape валидаторам:

ДоменИсточникНастраивается?
env.*Жёстко зафиксировано в CLIНет — семь фиксированных probe’ов
checks.*Записи workspace/validate.ymlДа — декларативно
linters.*Встроенные адаптеры (shellcheck, hadolint) + блок linters: в workspace/validate.ymlДа — декларативно
translations.*Файлы переводов workspace/i18n/Нет — фиксированные валидаторы (ошибки парсинга, осиротевшие id команд/групп, неизвестные ключи render.*)
snapshot.*Директории снапшотов на диске + workspace/snapshot.ymlНет — фиксированные валидаторы на каждое имя снапшота
tests.*Файлы сценариев workspace/tests/*.ymlНет — фиксированные валидаторы сценариев (рендерят и резолвят шаги каждого сценария, флагают неизвестные сервисы / ссылки на команды / дублирующиеся имена шагов и показывают риски compose-изоляции как предупреждения)

Домен tests.* работает только в validate (как и snapshot.*) — он никогда не запускается в preflight и молчит, если workspace/tests/ отсутствует. Полный набор проверок сценариев (dwe validate tests) см. в tests.md.

Probe’ы env.* это: env.docker_bin, env.docker_daemon, env.docker_compose, env.git_bin, env.shell_bin, env.project_perms, env.ports_free. Они запускаются на каждом вызове dwe validate и на каждом preflight (независимо от стадии — у env нет понятия стадии), с одним исключением: env.ports_free пропускает себя на стадии stop, поскольку конфликты портов нерелевантны при сворачивании проекта.

env.ports_free читает каждый хост-порт, объявленный в services.<name>.ports (только enabled-сервисы), и проверяет, можно ли каждый забиндить. Он один раз опрашивает docker ps --format=json, чтобы узнать, какие контейнеры держат какие порты сейчас: контейнеры с лейблом com.docker.compose.project=<наш проект> трактуются как «наши» (compose переиспользует их при up); контейнеры из любого другого compose-проекта вызывают диагностику конфликта с указанием чужого контейнера и проекта; для портов, не удерживаемых ни одним контейнером, probe откатывается к прямой попытке привязки, чтобы обнаружить не-Docker процессы. Недоступность Docker проходит молча — env.docker_daemon покрывает этот случай.

config.compose_project_name предупреждает, когда активная цепочка compose -f объявляет верхнеуровневый name:, отличающийся от имени проекта, которое dwe передаёт через docker compose -p. dwe всегда вызывает compose с -p <resolved> (разрешённый project_name из workspace/docker.yml, иначе каноническое <prefix>-<name> из project.name), и этот -p молча переопределяет любой верхнеуровневый name: в compose-файлах. Поэтому расходящийся name: — это мёртвая конфигурация: реальная область проекта (лейблы контейнеров/сетей/томов) равна разрешённому имени dwe, а не тому, что как будто объявляет файл — и грабли для всех, кто запускает голый docker compose без -p от dwe. Проверка моделирует реальную приоритетность compose: она проходит активную цепочку (ComposeFiles() — только включённые оверлеи) в порядке -f и сравнивает последний объявленный name: (более поздний -f переопределяет более ранний), поэтому базовый name:, уже исправленный более поздним оверлеем, не вызывает предупреждения. Чинится приведением к одному значению: поменять эффективный compose name: на совпадающее, либо задать project_name в docker.yml и убрать ставший лишним compose name:. Проверка молчит, когда эффективное имя использует неразрешённую интерполяцию (например, name: ${COMPOSE_PROJECT_NAME} — сравнить нельзя) или когда project.name не задан (dwe опускает -p, и compose использует собственный name: файла).

config.formal_block_fields предупреждает, когда формализованный верхнеуровневый блок конфига несёт неизвестный вложенный ключ. Смерженный трёхслойный конфиг декодируется нестрого (обычный yaml.Unmarshal, без KnownFields), поэтому опечатка под одним из этих блоков — например, stop: { port_release_timeot: 0 } — молча отбрасывается, и поле проваливается к своему дефолту, незаметно меняя поведение. Строгий корневой allowlist ловит неизвестные ключи верхнего уровня жёсткой ошибкой, но не вложенные; эта проверка закрывает пробел нефатальным предупреждением dwe validate (она не запускается в preflight, поэтому никогда не блокирует dwe run). Она сканирует все три слоя (workspace.yml, workspace/defaults.yml, workspace/local.yml) и привязывает каждую находку к файлу и строке. Набор распознаваемых ключей для каждого блока выводится рефлексией из YAML-тегов соответствующей Go-структуры, поэтому он не может разойтись при добавлении поля в структуру. Покрытые блоки (только непосредственные дочерние ключи — проверка не спускается во вложенные структуры вроде runtime.spx): project (name, prefix), runtime (use_https, spx), exports (env), compose (base, extra), docs (mermaid, cache_size_mb), update (mode), bridge (vars_writable), stop (port_release_timeout). Блок ui: покрывается отдельным валидатором config.ui (который к тому же спускается в ui.commands); свободные (vars) и per-service (services) блоки намеренно исключены.

config.template_refs предупреждает о ссылке ${head.path}, чей head является разрешённым корневым ключом смерженного конфига, но оставшийся путь не резолвится — почти всегда это опечатка, поскольку vars: — единственный корневой ключ с полностью свободным содержимым (${vars.opechatka}, когда объявлен только vars.source.repo). Head сверяется со списком разрешённых корневых ключей, а не с тем, что проект реально объявил, поэтому ${vars.source.repo} в проекте вообще без блока vars: тоже будет отмечен. Проверка намеренно молчит на нераспознанном head’е (${HOME}, случайный знак доллара) и на специальных неймспейсах, которые никогда не живут в Raw (param, context, files, host, snapshot, args, generated) — см. Два синтаксиса: shorthand и полные шаблоны в справочнике по шаблонам.

Область проверки не ограничивается шагами пайплайна, но и не покрывает все поля подряд: проверяются скалярные поля, которые varsusage считает шаблонизируемыми, по всему workspace-YAML — cmd, text, value, title, project_name, confirm, скалярный when:, timeout шага и собственный command у files_gate, argv_append_from команды, плюс каждый скалярный лист (на любой глубине) под маппингом with: или env: и каждый элемент последовательностей argv: / compose_args: команды — плюс тела рендер-шаблонов под workspace/templates/config/**. Скалярные поля вне этого набора (workdir, messages.*, confirmation_text) не сканируются, поэтому ${vars.typo} в них не будет отмечен. Проверяется только shorthand-форма ${...} — Go-шаблонная форма ({{ resolve .Raw "vars.x" }}) не проверяется.

config.container_name предупреждает, когда container_name: compose-сервиса расходится с общепринятым <project>-<service> — именем, которое напрямую строят daemon-builtins и на которое по привычке полагаются скрипты и документация. Дефект — само расхождение, а не регистр. Собственные per-service команды dwe (dwe stop/restart/logs <name>) находят контейнеры по compose-меткам project+service, а не угадыванием этого имени, поэтому на них расхождение не влияет; грабли — голое использование docker/docker compose, скрипты и документация. Обратите внимание: удалить container_name — не то же самое, что выровнять его: compose тогда назовёт контейнер <project>-<service>-1. Проверка молчит, когда объявленное значение уже совпадает, или когда это интерполированное значение ${...}, которое нельзя сравнить без разрешения окружения.

config.ports_exports предупреждает, когда сервис объявляет services.<name>.ports.<key>, но ни одно правило exports.env не читает services.<name>.ports.<key>. Такой порт — только для отображения: его переопределение в local.yml никак не сдвинет реальную привязку контейнера, и автоматическая изоляция host-портов dwe test (см. tests.md) на него тоже молча не распространяется. Сервис, не объявляющий собственных портов, наследует всю карту портов родителя через extends:; такие унаследованные порты повторно на ребёнке не репортятся — находка принадлежит service.yml родителя, где порт реально записан.

config.info сообщает об эффективном состоянии workspace/info.yml, а не просто о том, «существует ли он»: файл, состоящий только из комментариев или пустой, трактуется так же, как отсутствующий — молча активен встроенный дашборд — и репортится с SeverityInfo (а не SeverityOK, чтобы агент, сканирующий на зелёный статус, не прекращал искать); намеренный sections: [] репортит своё собственное состояние с SeverityInfo; только авторский дашборд с реальным содержимым получает SeverityOK.

templates.ai / templates.ide / templates.git предупреждают об отсутствующем template-паке только после того, как сервис явно выставил render.<kind>.enabled. Сервис type: app на неявном значении по умолчанию, у которого пака на диске нет, — это состояние после скаффолда, а не дефект, и он молчит. templates.git применяет то же правило к своему уведомлению «нет src/.git» — репозиторий может быть наполнен deploy-шагом ещё до того, как отработает render.

config.reset аналогично больше не сообщает об отсутствующем workspace/reset.yml: файл опционален и применяется встроенный дефолт, поэтому его отсутствие — нормальное состояние, а не повод для диагностики.

Валидаторы checks.* создаются по одному на каждую запись validate.yml. Каждый в рантайме диспатчится либо во встроенную процедуру инспекции, либо в изолированную пользовательскую команду.

checks:
- id: ghcr-login
description: Authenticated against ghcr.io
stages: [deploy]
severity: error
hint: |
Run `docker login ghcr.io` with a personal access token.
type: builtin
cmd: shell
with:
cmd: docker pull ghcr.io/owner/repo:latest >/dev/null
- id: project-deps
description: Project dependency check script passes
stages: [run, deploy]
type: command
cmd: deps.check

Файл опционален. При его отсутствии запускаются только env.* и существующие YAML-shape валидаторы.

ПолеТипОбязательноеОписание
checkslistнетЗаписи проверок (см. ниже). Опционально; может быть пустым или опущенным.
lintersmapнетАдаптеры внешних линтеров (см. Внешние линтеры).

Неизвестные поля верхнего уровня отвергаются на загрузке (строгое декодирование).

ПолеТипОбязательноеОписание
idstringдаУникальный идентификатор. Становится Target в диагностике.
descriptionstringдаЧеловекочитаемое summary, показываемое в таблице диагностики.
stageslist of stringsдаСтадии, на которых срабатывает эта проверка (см. Стадии).
serviceslist of stringsнетОграничивает проверку проектами, где включён хотя бы один из перечисленных сервисов (см. Привязка к сервисам).
typestringдаОдно из builtin или command. Неизвестные значения отвергаются.
cmdstringдаИмя билтина (для type: builtin) или ID пользовательской команды (для type: command).
severitystringнетОдно из error (по умолчанию), warning, info. Неизвестные значения отвергаются.
hintstringнетПодсказка по устранению, включаемая в диагностику. Держите её краткой; длинные подсказки разбивайте через \n.
withmapнетПараметры, передаваемые билтину или пользовательской команде.

Правила схемы, проверяемые на загрузке:

  • id должен быть уникальным среди записей.
  • stages должен быть непустым.
  • services должен быть непустым, если ключ задан (опустите ключ целиком, чтобы запустить проверку безусловно). Пустые строки внутри списка отвергаются.
  • Неизвестные имена сервисов (опечатки вида services: [aap], когда есть только api) всплывают как error-диагностики config.validate в рантайме, не как load-ошибки — у загрузчика нет доступа к merged-карте сервисов.
  • type должен быть builtin или command.
  • severity должен быть error / warning / info, если задан.
  • Валидность формы with: (обязательные ключи, типы) проверяется методом Validate целевого билтина — падения всплывают как диагностики checks.<id> в рантайме, не как load-ошибки.

Проверка запускается всякий раз, когда её список stages содержит стадию, запрошенную вызывающим. CLI определяет пять зарезервированных стадий с встроенными хуками:

СтадияТриггеры
deploydwe deploy run, dwe validate --stage deploy
rundwe run, dwe restart (нога run), dwe validate --stage run
stopdwe stop, dwe restart (нога stop), dwe reset run, dwe validate --stage stop
commanddwe validate --stage command (зарезервировано на будущее; автоматического хука нет)
post-setupтолько финальный preflight деплоя — dwe deploy run, dwe deploy после setup-визарда, dwe validate --stage post-setup

dwe validate без --stage запускает каждую проверку независимо от стадии.

deploy vs post-setup: когда в потоке деплоя запускается проверка

Заголовок раздела «deploy vs post-setup: когда в потоке деплоя запускается проверка»

У потока деплоя два момента preflight:

  1. Ранний pre-wizard gate (только в интерактивном меню dwe deploy, до показа setup-визарда) — всплывает на проблемах вроде упавшего демона Docker раньше, чем пользователь вложит время в заполнение визарда.
  2. Финальный preflight, запускаемый прямо перед выполнением пайплайна деплоя — и в dwe deploy run, и после визарда в интерактивном dwe deploy.

Проверки stages: [deploy] запускаются на обоих моментах. Для проверки, зависящей от значения, которое визард пишет в local.yml, это неправильно: на раннем gate значение ещё не задано, поэтому проверка блокирует пользователя до того, как он доберётся до визарда.

Проверки stages: [post-setup] запускаются только на финальном preflight — после того, как визард заполнил local.yml, либо (когда визарда нет, например dwe deploy run) прямо перед пайплайном. Это правильная стадия для гардов «значение должно быть задано перед деплоем»: интерактивный визард его заполняет, а неинтерактивный путь всё равно ловит незаданное значение до любого побочного эффекта, а не падает посреди пайплайна. Сочетайте с config_keys_present для проверки значений смерженного конфига или с env_keys_present для отрендеренных .env-файлов.

Проверка post-setup не несёт стадию deploy, поэтому естественным образом пропускается на раннем gate. (stages: [deploy, post-setup] принимается, но избыточно — ведёт себя ровно как [deploy].) Вне потока деплоя post-setup смысла не имеет; на dwe run/dwe stop она никогда не срабатывает.

Неизвестные стадии принимаются (открытое перечисление), но производят предупреждение на загрузке, чтобы пользователи ловили опечатки рано:

  • stage "deplooy" is not a known preflight stage (с предложением, если близко по расстоянию Левенштейна)
  • Особые заметки: restart композитный (использует обе стадии stop и run, отдельного preflight нет); reset использует только стадию stop

Неизвестные стадии всё ещё можно вызвать явно через dwe validate --stage <name>, если нужно (например, для кастомных workflow’ов валидации).

services: ограничивает проверку проектами, где включён хотя бы один из перечисленных сервисов. Семантика — ИЛИ:

  • Поле отсутствует → проверка запускается всегда (когда сматчилась стадия).
  • services: [api] → проверка запускается только если включён api.
  • services: [api, worker] → проверка запускается, если включён api ИЛИ worker. Все сервисы выключены → проверка тихо пропускается (никакой строки в диагностической таблице).

services: и stages: — независимые AND-фильтры: сначала проверяется стадия, затем сервисы. Проверка с stages: [deploy] и services: [api] запускается, только когда оба условия выполнены.

Поведение идентично в preflight и dwe validate (без флагов и без переменных окружения). Один escape hatch: dwe validate checks <id> с явным id отключает services-гейт, чтобы можно было инспектировать проверку, все целевые сервисы которой выключены — полезно при отладке самого гейта.

Неизвестные имена сервисов производят error-диагностику в таргете config.validate, чтобы опечатки всплывали рано; саму проверку с gate-ом в этом проходе не выполняем (неизвестное имя ничего не добавляет в ИЛИ).

Все семь билтинов применимы и как записи type: builtin проверок, и как тела шагов деплоя / блоки экшенов check:.

Запускает shell-команду через жёстко зафиксированный sh -c (по соглашению с предикатом when: деплоя). Exit 0 = pass. Этот билтин использует POSIX-переносимый sh -c независимо от настроенного в проекте shell, обеспечивая идентичный запуск проверок во всех окружениях.

Команда выполняется с рабочим каталогом, установленным в корень проекта, поэтому относительные пути означают то же самое, что и в file_exists и в условии when:, независимо от каталога, из которого вызван dwe.

КлючТипОбязательноеПо умолчаниюОписание
cmdstringдаТело shell-команды.
timeoutdurationнет10sМаксимальное время выполнения. 0 означает без ограничения (а не «истечь немедленно»).

Сообщение об ошибке при ненулевом exit: exit status N: <последняя строка stderr>.

См. deploy: cmd: shell vs type: shell для различия между этим билтином и типом выполнения шага type: shell.

Проверяет, что файл присутствует на диске.

КлючТипОбязательноеОписание
pathstringдаПуть относительно корня проекта.

Проверяет, что бинарь разрешается через exec.LookPath.

КлючТипОбязательноеОписание
namestringдаИмя исполняемого файла (без сегментов пути).

Проверяет, что один или несколько ключей существуют с непустыми значениями в файле в стиле .env. Парсинг следует соглашениям .env: пустые строки и full-line #-комментарии пропускаются; обрамляющие кавычки "..." / '...' снимаются; KEY=, KEY="" и KEY='' все считаются пустыми.

КлючТипОбязательноеОписание
filestringдаПуть к файлу в стиле .env, относительно корня проекта.
keyslist of stringsдаКлючи, которые должны присутствовать И быть непустыми.

Сообщение об ошибке: missing or empty keys: A, B, C.

Проверяет, что один или несколько точечных путей резолвятся в непустые значения в смерженной конфигурации DWE — слоях workspace.yml / defaults.yml / local.yml после слияния. Это конфиг-ориентированный аналог env_keys_present: вместо чтения .env с диска он читает смерженный конфиг в памяти, поэтому сразу видит оверлеи local.yml и не зависит от того, материализован ли уже отрендеренный .env.

Адресация — тот же точечный путь, который setup-визард использует в поле writes:, так что проверяемый путь — ровно тот, в который визард записал — например vars.db.api_key или vars.app.log_level. Сочетайте с stages: [post-setup], чтобы проверка запускалась после того, как визард заполнит local.yml.

КлючТипОбязательноеОписание
keyslist of stringsдаТочечные пути в смерженный конфиг; каждый должен резолвиться в непустое значение.

Путь считается «отсутствующим», когда он не резолвится, когда резолвится в null или когда рендерится в пустую строку. Нестроковые скаляры — числа, булевы — считаются присутствующими. Сообщение об ошибке: missing or empty keys: vars.db.api_key, vars.app.log_level.

Какие пути достижимы. Проверяйте те же пути, которые может писать визард — см. scope writes: в setup. Кастомные значения живут в песочнице vars: (vars.db.*, vars.app.*, …) — корень смерженного конфига строгий, поэтому свободные ключи должны быть вложены под vars:, чтобы пережить слияние и резолвиться здесь. Под services.<name> в local.yml допускаются только enabled, ports.<name> и hosts.<name> — и визард, и загрузчик конфига отвергают всё остальное, поэтому посервисный секрет не может жить по пути services.<name>.env.* в local.yml. Держите посервисные секреты в отрендеренном .env сервиса и проверяйте их через env_keys_present; config_keys_present — для top-level значений, которые пишет визард.

Пытается сделать TCP-dial до host:port.

КлючТипОбязательноеПо умолчаниюОписание
hoststringдаИмя хоста или IP.
portintдаПорт в диапазоне 1–65535.
timeoutdurationнет3sТаймаут dial’а.

Выполняет HTTP GET и утверждает статус ответа (и, когда задано, подстроку тела), повторяя при сбое.

КлючТипОбязательноеПо умолчаниюОписание
urlstringдаАбсолютный http/https URL с хостом.
statusintнет200Ожидаемый код статуса.
containsstringнетПодстрока, которая должна присутствовать в теле ответа.
retriesintнет0Дополнительные попытки после первой (всего попыток = retries + 1).
intervaldurationнет1sОжидание между попытками.
timeoutdurationнет5sТаймаут на попытку.

Сообщение об ошибке при сбое: http_check <url>: expected status 200, got 503 (с добавлением (after N attempts), когда настроены ретраи). Полное описание поведения см. в справочнике билтинов.

Запись проверки с type: command диспатчится в декларативную пользовательскую команду из workspace/commands/. Блок with: пробрасывается как payload params: пользовательской команды — ровно как dwe commands <id> --set k=v.

Ограничения, проверяемые на загрузке:

  • type: целевой команды ДОЛЖЕН быть shell или script. Цели workflow, service_exec, service_run, dwe и builtin-as-command отвергаются с сообщением: checks may only invoke user commands of type shell or script (got: <type>).
  • Неизвестный ID команды отвергается с сообщением: unknown command: <id>.

Выполнение зафиксировано:

  • SkipConfirm = true — промпты подтверждения обходятся.
  • NonInteractive = true — UI-пути с промптами короткозамыкаются.
  • SkipNotify = true — desktop-уведомления подавляются.
  • stdout отбрасывается; stderr захватывается, и его хвост включается в сообщение диагностики, если проверка падает.

Проверки должны быть идемпотентной инспекцией

Заголовок раздела «Проверки должны быть идемпотентной инспекцией»

Проверка отвечает на вопрос «готов ли мир?», а не «подготовь мир». По соглашению, каждая проверка ДОЛЖНА быть идемпотентной, без побочных эффектов и быстрой.

CLI этого НЕ обеспечивает. Read-only песочницы для подпроцессов нет; проверка type: command, чьё тело — rm -rf /tmp/work, выполнит ровно это в preflight.

Что CLI ОБЕСПЕЧИВАЕТ для проверок type: command:

  • Неинтерактивное выполнение (без промптов, без подтверждений).
  • Уведомления подавлены.
  • stdout отброшен, stderr захвачен.

Компромисс: CLI держит мост минимальным, чтобы пользовательские shell/script-команды можно было переиспользовать и для шагов деплоя, и для проверок готовности, не вводя новый ограниченный режим выполнения. Изменяющая проверка — это острый край для автора: явно документируйте это в description:, если ваша проверка обязана что-то менять, и предпочитайте чистую инспекцию (shell: docker pull ... --quiet, shell: test -f path) везде, где возможно.

1. Логин в реестр контейнеров (встроенный shell):

checks:
- id: ghcr-login
description: Authenticated against ghcr.io
stages: [deploy]
severity: error
hint: |
Run `docker login ghcr.io` with a GitHub PAT.
type: builtin
cmd: shell
with:
cmd: docker pull ghcr.io/owner/private-image:latest --quiet
timeout: 30s

2. Локальный дамп БД присутствует (file_exists):

- id: db-dump-present
description: Seed dump exists for first-run import
stages: [deploy]
severity: warning
hint: Download from s3://team-dumps/latest.sql and place at .dwe/seed.sql
type: builtin
cmd: file_exists
with:
path: .dwe/seed.sql

3. Необходимые секреты сконфигурированы (env_keys_present):

- id: app-secrets
description: Required app secrets configured in .env
stages: [run, deploy]
severity: error
hint: |
Copy .env.example to .env and fill in:
DATABASE_URL, REDIS_URL, JWT_SECRET
type: builtin
cmd: env_keys_present
with:
file: .env
keys: [DATABASE_URL, REDIS_URL, JWT_SECRET]

4. Проверка с привязкой к сервису (services gate):

- id: api-jwt-secret
description: JWT_SECRET configured for API
stages: [run, deploy]
services: [api] # запускается только когда api включён
severity: error
hint: Set JWT_SECRET in services/api/.env
type: builtin
cmd: env_keys_present
with:
file: services/api/.env
keys: [JWT_SECRET]

5. Значение от визарда обязательно перед деплоем (post-setup + config_keys_present):

- id: db-api-key-set
description: vars.db.api_key must be set before deploy
stages: [post-setup] # только финальный preflight — после setup-визарда
severity: error
hint: |
Run `dwe deploy` and complete the wizard, or set
vars.db.api_key in workspace/local.yml.
type: builtin
cmd: config_keys_present
with:
keys: [vars.db.api_key]

Setup-визард пишет vars.db.api_key в local.yml (путь под песочницей vars:services.<name>.env.* НЕ является легальной целью визарда/local.yml, см. заметку о достижимости выше); эта проверка утверждает, что тот же точечный путь задан. Поскольку она post-setup, на раннем pre-wizard gate она пропускается (чтобы визард был достижим) и запускается на финальном preflight — ловя незаданное значение до старта деплоя, включая dwe deploy run, где визарда нет.

6. Корпоративный VPN доступен (tcp_reachable):

- id: corporate-vpn
description: Internal git mirror is reachable (VPN up?)
stages: [deploy, run]
severity: error
hint: Connect to the corporate VPN and retry.
type: builtin
cmd: tcp_reachable
with:
host: git.internal.example.com
port: 22
timeout: 2s

7. Скрипт проверки зависимостей проекта (type: command):

- id: project-deps
description: Required CLIs installed (./scripts/check-deps.sh)
stages: [run]
type: command
cmd: deps.check

Где workspace/commands/deps.yml объявляет:

group: deps
commands:
check:
type: shell
description: Verify required CLIs
cmd: |
set -e
command -v node
command -v pnpm
command -v psql

8. Исполняемый файл в PATH (executable_in_path):

- id: jq-installed
description: jq is available for compose introspection helpers
stages: [deploy]
severity: warning
type: builtin
cmd: executable_in_path
with:
name: jq
  • dwe validate — запускает config.*, templates.*, commands.*, bridge.*, env.* и все checks.*. Опциональный позиционный scope сужает запуск (например, dwe validate env, dwe validate checks ghcr-login, dwe validate bridge). Строка итога называет активный scope — (scope: all), (scope: config), (scope: config/services), — а --output json несёт то же значение в summary.scope, так что суженный запуск отличим от полного не только числом диагностик.
  • dwe validate bridge — статические проверки только per-service блоков bridge:: enum on_unreachable (fail / warn), абсолютность shim_path и маппинг dir / dir_internal сервиса с включённым мостом, поверх которого работает shim. Только для validate — домен bridge не участвует в preflight.
  • dwe validate --stage <name> — локальный флаг команды validate. Фильтрует checks.* по стадии. env.* и другие домены не затрагиваются (у них нет стадий).
  • dwe validate --strict — трактовать предупреждения как ошибки (exit 1).
  • dwe validate --quiet — скрыть строки ok / info.
  • dwe validate --level <levels> — показать только указанные уровни серьёзности (через запятую: ok, info, warning, error; например --level error,warning). Только для отображения — не влияет ни на итоговые счётчики, ни на код выхода. Применяется и к таблице, и к --output json.
  • Подсказка о фильтрации. После длинного запуска в человекочитаемом режиме — больше 20 отрисованных строк — dwe validate печатает одну info-строку в stderr, называя --level и --quiet. Она подавляется, если любой из этих флагов уже задан, если все показанные строки — ошибки (ни один флаг ничего бы не убрал), и в режиме --output json, где stdout остаётся разбираемой поверхностью, а потребитель фильтрует массив сам.
  • --skip-preflight — локальный флаг для deploy run, run, stop, restart и reset run. Если задан, preflight печатает preflight skipped (--skip-preflight) в stderr и НЕ запускает валидаторов. Флаг — это полноценный байпас: проверки type: command вызывают произвольные пользовательские скрипты, поэтому CLI не запускает их под флагом, который пользователь назвал «skip».

Диагностики используют ту же модель рендеринга и severity, что и остальной dwe validate:

  • Severity: из entry.severity (по умолчанию error).
  • Domain: checks (или env для жёстко зафиксированных probe’ов).
  • Target: id записи.
  • File: workspace/validate.yml (записи) или пусто (env-probe’ы).
  • Line: номер строки (1-based) первого ключа записи (записи).
  • Message: строка ошибки билтина / команды.
  • Hint: из entry.hint.

Preflight пишет ту же таблицу диагностики в stderr перед падением с exit-кодом 1. Используйте \n в подсказках, чтобы разбить длинный текст устранения на строки — таблица Lipgloss соблюдает переводы строк.

Таблица диагностики подстраивается под терминал, в который она пишется: по мере сужения терминала колонки ужимаются и переносятся, а ниже границы, где колонки ещё помещаются, таблица заменяется блоками записей с подписями — по блоку на диагностику, строки вида label value вместо ячеек. Ничего никогда не обрезается: длинный URL из подсказки остаётся целым и копируемым в обеих раскладках, поэтому ссылка шире терминала — единственное, что всё ещё может выйти за его границы.

Когда вывод уходит в пайп или перенаправляется, ширина терминала неизвестна, поэтому адаптивная раскладка отключается и печатается неизменная полноширинная таблица — dwe validate > report.txt даёт одни и те же байты независимо от терминала, в котором команда запущена. Для машинного вывода используйте --output json.

Ширина берётся от того потока, в который команда фактически пишет: dwe validate печатает свою таблицу в stdout, а preflight и меню деплоя — в stderr. Перенаправление одного не влияет на другое.

Домен linters.* запускает известные внешние линтеры (shellcheck, hadolint) и произвольные адаптеры type: generic как часть dwe validate. Линтеры не запускаются в preflight — preflight отвечает на вопрос «можем ли мы запуститься?», а не «чист ли код?».

linters:
shellcheck:
enabled: true
bin: shellcheck
paths: [workspace/scripts, scripts]
extensions: [.sh, .bash]
flags: [--severity=warning]
severity: warning
hadolint:
paths: ["."]
filenames: [Dockerfile]
extensions: [.dockerfile]
yamllint:
type: generic
bin: yamllint
paths: ["."]
extensions: [.yml, .yaml]
flags: [-s]

Ключ маппинга — это ID адаптера. Неизвестные поля отвергаются на загрузке (строгое декодирование).

ПолеТипОбязательноеОписание
typestringнетbuiltin (по умолчанию) или generic.
enabledboolнетОпущено → автоопределение (true, если bin есть в PATH). false → молчаливый пропуск.
binstringнетПо умолчанию — дефолт адаптера (например, shellcheck). Должно быть голым именем команды — без сепараторов пути. Абсолютные или относительные пути отвергаются на загрузке.
pathslist of stringsнетПо умолчанию — дефолт адаптера. Каждая запись должна быть относительной, непустой и не содержать ... "." разрешён (равенство корню, используется hadolint).
extensionslist of stringsнетПо умолчанию — дефолт адаптера. Каждая запись должна начинаться с . (например, .sh, не sh).
filenameslist of stringsнетЛитеральные basename’ы, матчащиеся рядом с расширениями (например, Dockerfile). Сепараторы пути не разрешены.
flagslist of stringsнетДописываются после встроенных флагов адаптера. Встроенные адаптеры резервируют флаги output-формата (--format, -f) — передача их в любой argv-форме (--format=gcc, -f tty, -fgcc) отвергается на загрузке.
severitystringнетОдно из error, warning, info. Ограничивает находки адаптера сверху (например, severity: warning понижает находки адаптера уровня error до warning). ok не разрешено — используйте enabled: false для отключения. Операционные диагностики (timeout, truncation, parse failure, missing-path) никогда не ограничиваются, так что пользователь не может случайно заглушить сигналы рантайм-падений.
IDBin по умолчаниюPaths по умолчаниюExtensions по умолчаниюFilenames по умолчаниюЗарезервированные флаги
shellcheckshellcheckworkspace/scripts, scripts.sh, .bash--format, -f
hadolinthadolint..dockerfileDockerfile--format, -f

Generic-адаптер запускает bin <flags> <files...> и конвертирует ненулевой exit в одну диагностику severity error с объединённым stdout+stderr в качестве сообщения (обрезано до ~2 KB, чтобы таблица оставалась читаемой). У него нет зарезервированных флагов — пользователь сам управляет всем набором флагов — и нет построчного парсинга. Используйте его для линтеров, чей формат вывода мы не парсим нативно.

  1. Для каждого известного встроенного адаптера, если в linters: нет записи → синтезировать запись с дефолтами (на адаптер, не all-or-nothing).
  2. Блок присутствует, enabled опущен → true.
  3. enabled: false → молчаливый пропуск (без диагностики).
  4. Дефолтный bin: отсутствует в PATH → молчаливый пропуск («мы попробовали автоопределить; делать нечего»).
  5. Явный bin: задан, но отсутствует в PATH → одна Warning-диагностика (проблема конфига, не кода).
  6. Раскрытие путей не дало файлов → молчаливый пропуск.

Можно переопределить путь к бинарю для любого линтера через свой пользовательский конфигурационный файл (~/.config/dwe/config). Полезно, когда у вас кастомные установки, замены (например, podman вместо docker) или бинари вне стандартного PATH.

Добавьте строку в свой user config:

binary_shellcheck=/custom/path/to/shellcheck
binary_hadolint=/opt/hadolint

Формат — binary_<linter-id>=<path>. Пути могут быть абсолютными или относительными к текущей директории. Если путь не существует или не исполняем, dwe validate выводит error-диагностику в домене linters.

Заметка: Эти переопределения учитываются только во время dwe validate. Lifecycle-команды (deploy, run, stop и т. д.) не используют бинари линтеров, так что сломанные переопределения не влияют на обычную работу.

Запустить все линтеры или сузить до одного через подкоманду linters:

dwe validate # all domains (including linters)
dwe validate linters # all linters
dwe validate linters shellcheck # only shellcheck

Неизвестные ID линтеров дают пустой результат (не жёсткую ошибку — зеркалит поведение checks).

  • Timeout: 5 минут на линтер (DefaultLinterTimeout). Превышение → Error-диагностика; частичный вывод не парсится.
  • Лимит вывода: 50 MB объединённого stdout+stderr на линтер (MaxLinterOutputBytes). Излишек отбрасывается, и выводится Warning-диагностика; парсер всё равно запускается на захваченном префиксе.
  • Конкурентность: линтеры запускаются параллельно, ограничено runtime.NumCPU() (MaxLinterConcurrency). Падение одного линтера (panic, timeout, ошибка парсера) никогда не отменяет соседей.
  • Записи paths: рекурсивно обходятся внутри корня проекта.
  • Явные пути к файлам (записи, разрешающиеся в обычный файл) обходят фильтры extensions/filenames.
  • Файл матчится, если его расширение в extensions: ИЛИ его basename в filenames:.
  • Симлинки пропускаются (защита от выходов за пределы корня проекта).
  • .git/ всегда пропускается. Сужение специфичного для адаптера шума (например, node_modules, vendor) оставлено пользователю через paths:.
  • Отсутствующие дефолтные пути (например, workspace/scripts shellcheck’а в проекте, где их нет) молча отбрасываются. Отсутствующие пользовательские пути (записи, явно написанные пользователем) дают Warning.

bin: ограничен голым именем команды, разрешаемым через PATH в рантайме; абсолютные и относительные пути запрещены на загрузке. Обоснование: validate.yml едет с репозиторием; вредоносный конфиг с bin: ./scripts/evil.sh не должен молча выполнять произвольный код на dwe validate. Пользователи, которым действительно нужен кастомный путь бинаря, устанавливают его в PATH (или оборачивают).

  • dwe validate — полный прогон валидации (все домены).
  • dwe validate env — только env-probe’ы.
  • dwe validate checks [id] — декларативные проверки (опциональный id сужает до одной).
  • dwe validate linters [id] — внешние линтеры (опциональный id сужает до одного).
  • dwe deploy run / run / stop / restart — автоматически вызывают preflight (см. --skip-preflight). Линтеры в preflight не запускаются.