validate.yml
Проверки готовности проекта.
Содержание
Заголовок раздела «Содержание»- Назначение
- Домены валидации
- Структура
- Поля верхнего уровня
- Поля записи check
- Стадии
- Привязка к сервисам
- Доступные билтины
- Проверки
type: command - Проверки должны быть идемпотентной инспекцией
- Разобранные примеры
- CLI-флаги
- Диагностический вывод
- Внешние линтеры
- Связанные команды
Назначение
Заголовок раздела «Назначение»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 валидаторы.
Поля верхнего уровня
Заголовок раздела «Поля верхнего уровня»| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
checks | list | нет | Записи проверок (см. ниже). Опционально; может быть пустым или опущенным. |
linters | map | нет | Адаптеры внешних линтеров (см. Внешние линтеры). |
Неизвестные поля верхнего уровня отвергаются на загрузке (строгое декодирование).
Поля записи check
Заголовок раздела «Поля записи check»| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
id | string | да | Уникальный идентификатор. Становится Target в диагностике. |
description | string | да | Человекочитаемое summary, показываемое в таблице диагностики. |
stages | list of strings | да | Стадии, на которых срабатывает эта проверка (см. Стадии). |
services | list of strings | нет | Ограничивает проверку проектами, где включён хотя бы один из перечисленных сервисов (см. Привязка к сервисам). |
type | string | да | Одно из builtin или command. Неизвестные значения отвергаются. |
cmd | string | да | Имя билтина (для type: builtin) или ID пользовательской команды (для type: command). |
severity | string | нет | Одно из error (по умолчанию), warning, info. Неизвестные значения отвергаются. |
hint | string | нет | Подсказка по устранению, включаемая в диагностику. Держите её краткой; длинные подсказки разбивайте через \n. |
with | map | нет | Параметры, передаваемые билтину или пользовательской команде. |
Правила схемы, проверяемые на загрузке:
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 определяет пять зарезервированных стадий с встроенными хуками:
| Стадия | Триггеры |
|---|---|
deploy | dwe deploy run, dwe validate --stage deploy |
run | dwe run, dwe restart (нога run), dwe validate --stage run |
stop | dwe stop, dwe restart (нога stop), dwe reset run, dwe validate --stage stop |
command | dwe 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:
- Ранний pre-wizard gate (только в интерактивном меню
dwe deploy, до показа setup-визарда) — всплывает на проблемах вроде упавшего демона Docker раньше, чем пользователь вложит время в заполнение визарда. - Финальный 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.
| Ключ | Тип | Обязательное | По умолчанию | Описание |
|---|---|---|---|---|
cmd | string | да | — | Тело shell-команды. |
timeout | duration | нет | 10s | Максимальное время выполнения. 0 означает без ограничения (а не «истечь немедленно»). |
Сообщение об ошибке при ненулевом exit: exit status N: <последняя строка stderr>.
См. deploy: cmd: shell vs type: shell для различия между этим билтином и типом выполнения шага type: shell.
file_exists
Заголовок раздела «file_exists»Проверяет, что файл присутствует на диске.
| Ключ | Тип | Обязательное | Описание |
|---|---|---|---|
path | string | да | Путь относительно корня проекта. |
executable_in_path
Заголовок раздела «executable_in_path»Проверяет, что бинарь разрешается через exec.LookPath.
| Ключ | Тип | Обязательное | Описание |
|---|---|---|---|
name | string | да | Имя исполняемого файла (без сегментов пути). |
env_keys_present
Заголовок раздела «env_keys_present»Проверяет, что один или несколько ключей существуют с непустыми значениями в файле в стиле .env. Парсинг следует соглашениям .env: пустые строки и full-line #-комментарии пропускаются; обрамляющие кавычки "..." / '...' снимаются; KEY=, KEY="" и KEY='' все считаются пустыми.
| Ключ | Тип | Обязательное | Описание |
|---|---|---|---|
file | string | да | Путь к файлу в стиле .env, относительно корня проекта. |
keys | list of strings | да | Ключи, которые должны присутствовать И быть непустыми. |
Сообщение об ошибке: missing or empty keys: A, B, C.
config_keys_present
Заголовок раздела «config_keys_present»Проверяет, что один или несколько точечных путей резолвятся в непустые значения в смерженной конфигурации 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.
| Ключ | Тип | Обязательное | Описание |
|---|---|---|---|
keys | list 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_reachable
Заголовок раздела «tcp_reachable»Пытается сделать TCP-dial до host:port.
| Ключ | Тип | Обязательное | По умолчанию | Описание |
|---|---|---|---|---|
host | string | да | — | Имя хоста или IP. |
port | int | да | — | Порт в диапазоне 1–65535. |
timeout | duration | нет | 3s | Таймаут dial’а. |
http_check
Заголовок раздела «http_check»Выполняет HTTP GET и утверждает статус ответа (и, когда задано, подстроку тела), повторяя при сбое.
| Ключ | Тип | Обязательное | По умолчанию | Описание |
|---|---|---|---|---|
url | string | да | — | Абсолютный http/https URL с хостом. |
status | int | нет | 200 | Ожидаемый код статуса. |
contains | string | нет | — | Подстрока, которая должна присутствовать в теле ответа. |
retries | int | нет | 0 | Дополнительные попытки после первой (всего попыток = retries + 1). |
interval | duration | нет | 1s | Ожидание между попытками. |
timeout | duration | нет | 5s | Таймаут на попытку. |
Сообщение об ошибке при сбое: http_check <url>: expected status 200, got 503 (с добавлением (after N attempts), когда настроены ретраи). Полное описание поведения см. в справочнике билтинов.
Проверки type: command
Заголовок раздела «Проверки type: command»Запись проверки с 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: 30s2. Локальный дамп БД присутствует (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.sql3. Необходимые секреты сконфигурированы (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: 2s7. Скрипт проверки зависимостей проекта (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: depscommands: check: type: shell description: Verify required CLIs cmd: | set -e command -v node command -v pnpm command -v psql8. Исполняемый файл в 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: jqCLI-флаги
Заголовок раздела «CLI-флаги»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:: enumon_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 адаптера. Неизвестные поля отвергаются на загрузке (строгое декодирование).
Поля записи
Заголовок раздела «Поля записи»| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
type | string | нет | builtin (по умолчанию) или generic. |
enabled | bool | нет | Опущено → автоопределение (true, если bin есть в PATH). false → молчаливый пропуск. |
bin | string | нет | По умолчанию — дефолт адаптера (например, shellcheck). Должно быть голым именем команды — без сепараторов пути. Абсолютные или относительные пути отвергаются на загрузке. |
paths | list of strings | нет | По умолчанию — дефолт адаптера. Каждая запись должна быть относительной, непустой и не содержать ... "." разрешён (равенство корню, используется hadolint). |
extensions | list of strings | нет | По умолчанию — дефолт адаптера. Каждая запись должна начинаться с . (например, .sh, не sh). |
filenames | list of strings | нет | Литеральные basename’ы, матчащиеся рядом с расширениями (например, Dockerfile). Сепараторы пути не разрешены. |
flags | list of strings | нет | Дописываются после встроенных флагов адаптера. Встроенные адаптеры резервируют флаги output-формата (--format, -f) — передача их в любой argv-форме (--format=gcc, -f tty, -fgcc) отвергается на загрузке. |
severity | string | нет | Одно из error, warning, info. Ограничивает находки адаптера сверху (например, severity: warning понижает находки адаптера уровня error до warning). ok не разрешено — используйте enabled: false для отключения. Операционные диагностики (timeout, truncation, parse failure, missing-path) никогда не ограничиваются, так что пользователь не может случайно заглушить сигналы рантайм-падений. |
Встроенные адаптеры
Заголовок раздела «Встроенные адаптеры»| ID | Bin по умолчанию | Paths по умолчанию | Extensions по умолчанию | Filenames по умолчанию | Зарезервированные флаги |
|---|---|---|---|---|---|
shellcheck | shellcheck | workspace/scripts, scripts | .sh, .bash | — | --format, -f |
hadolint | hadolint | . | .dockerfile | Dockerfile | --format, -f |
type: generic
Заголовок раздела «type: generic»Generic-адаптер запускает bin <flags> <files...> и конвертирует ненулевой exit в одну диагностику severity error с объединённым stdout+stderr в качестве сообщения (обрезано до ~2 KB, чтобы таблица оставалась читаемой). У него нет зарезервированных флагов — пользователь сам управляет всем набором флагов — и нет построчного парсинга. Используйте его для линтеров, чей формат вывода мы не парсим нативно.
Правила автоопределения
Заголовок раздела «Правила автоопределения»- Для каждого известного встроенного адаптера, если в
linters:нет записи → синтезировать запись с дефолтами (на адаптер, не all-or-nothing). - Блок присутствует,
enabledопущен →true. enabled: false→ молчаливый пропуск (без диагностики).- Дефолтный
bin:отсутствует в PATH → молчаливый пропуск («мы попробовали автоопределить; делать нечего»). - Явный
bin:задан, но отсутствует в PATH → одна Warning-диагностика (проблема конфига, не кода). - Раскрытие путей не дало файлов → молчаливый пропуск.
Пользовательские оверрайды бинаря
Заголовок раздела «Пользовательские оверрайды бинаря»Можно переопределить путь к бинарю для любого линтера через свой пользовательский конфигурационный файл (~/.config/dwe/config). Полезно, когда у вас кастомные установки, замены (например, podman вместо docker) или бинари вне стандартного PATH.
Добавьте строку в свой user config:
binary_shellcheck=/custom/path/to/shellcheckbinary_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 lintersdwe 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/scriptsshellcheck’а в проекте, где их нет) молча отбрасываются. Отсутствующие пользовательские пути (записи, явно написанные пользователем) дают 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 не запускаются.