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 | Да — декларативно |
secrets.* | Recipient блока secrets:, маркеры ENC[age:…] в слоях, источники паков *.age | Нет — три фиксированных валидатора |
linters.* | Встроенные адаптеры (shellcheck, hadolint) + блок linters: в workspace/validate.yml | Да — декларативно |
translations.* | Файлы переводов workspace/i18n/ | Нет — фиксированные валидаторы (ошибки парсинга, осиротевшие id команд/групп, неизвестные ключи render.*) |
snapshot.* | Директории снапшотов на диске + workspace/snapshot.yml | Нет — фиксированные валидаторы на каждое имя снапшота |
tests.* | Файлы сценариев workspace/tests/*.yml | Нет — фиксированные валидаторы сценариев (рендерят и резолвят шаги каждого сценария, флагают неизвестные сервисы / ссылки на команды / дублирующиеся имена шагов и показывают риски compose-изоляции как предупреждения); также предупреждают, когда host-скрипт или shell-шаг сам собирает имя compose-проекта, а не выводит его из $COMPOSE_PROJECT_NAME |
Домен tests.* работает только в validate (как и snapshot.*) — он никогда не запускается в preflight и молчит, если workspace/tests/ отсутствует. Полный набор проверок сценариев (dwe validate tests) см. в tests.md.
У домена secrets.* три валидатора, и они разделены по линии «контент / готовность», которая и определяет, что попадает в preflight (см. Стадии):
| Валидатор | Вид | Срабатывает, когда |
|---|---|---|
secrets.recipient | контент — только dwe validate | Есть маркеры или источники .age, но secrets.recipient отсутствует либо не является корректным age1…; либо payload маркера повреждён (плохой base64 / не age-файл) |
secrets.unresolved | готовность — работает и в preflight | Хоть одно зашифрованное значение в объединённом конфиге не расшифровалось здесь, либо источник .age резолвящегося конфиг-пака не расшифровывается загруженным identity |
secrets.shadowed | эффективность — только dwe validate, предупреждение | Маркер перебит плейнтекстом из вышележащего слоя, поэтому зашифрованное значение никогда не читается |
secrets.recipient сам грузит файлы слоёв, когда конфиг не загрузился, поэтому scoped-запуск dwe validate secrets всё равно диагностирует некорректный recipient, а не остаётся слепым. Повреждённый payload определяется вообще без ключа — поэтому он относится к контент-валидатору: разработчика без ключа никогда не отправляют искать ключ, который всё равно бы не помог.
secrets.unresolved — второе исключение из правила, что в preflight попадают только env.* и checks.* (первое — config.validate). Это вопрос готовности — «может ли эта машина реально деплоить прямо сейчас», — а не контента, поэтому dwe run / dwe restart / dwe stop / dwe deploy run / dwe reset run и pre-flight-гейт мастера деплоя останавливаются с одной и той же понятной подсказкой (per-service dwe stop|restart <имя> намеренно обходят и compose-оверлей, и preflight), а dwe status, dwe docs, dwe validate и dwe prompt продолжают работать. Нерасшифрованные значения группируются по причине: одна диагностика на причину со списком отсортированных путей, чтобы единственное осмысленное действие не было похоронено под одинаковыми строками на каждый маркер. Причины: no_identity (источника нет нигде), invalid_identity (источник задан, но ключа age не содержит — обрезанный DWE_AGE_KEY, keyfile, потерявший строку с ключом; сообщение называет источник и составлено из фиксированных формулировок, потому что ошибка парсера age повторяет байты приватного ключа, на которых он споткнулся), wrong_identity (читаемый identity другого recipient) и corrupt (повреждённый payload — проблема не в ключе). В интерактивном dwe run / dwe restart / dwe deploy отсутствующий identity предлагается ввести до preflight, поэтому сюда доходит только то, чего предложение не покрывало: неинтерактивный запуск или сломанный источник, который импорт ключа всё равно не починит. Отказ от предложения сюда не доходит — команда завершается прямо на гейте с той же подсказкой.
Скан источников .age повторяет то, что реально обходит render config, поэтому выключенный сервис или нерезолвящийся пак невидимы валидатору ровно так же, как и во время рендера. Модель и команда dwe secrets status, которая сообщает то же самое, ничего не блокируя, — в secrets.md.
secrets.shadowed сообщает о маркере, перебитом плейнтекстом из вышележащего слоя: он отлично расшифровывается и числится читаемым, а проект при этом читает плейнтекст — то есть пара ключей не является тем, через что это значение шарится. Это предупреждение, а не ошибка, и в preflight валидатор не попадает: временно перебить общий секрет локальным значением — законный приём.
Находки группируются по перебивающему файлу и вердикту — ровно то, от чего меняется способ починки:
| Вердикт | Чтение | Подсказка |
|---|---|---|
secrets.shadowed:identical | Перебивающее значение совпадает с маркером | Почти всегда копия, оставшаяся после переезда значения в шифр — её надо удалить, маркер уже хранит то же содержимое |
secrets.shadowed:different | Перебивающее значение отличается | Осознанный локальный оверрайд; удалите его, чтобы вернуться к общему зашифрованному значению |
secrets.shadowed:unknown | Маркер здесь не расшифровался, значения не сравнивались | Начинается с идентичности: пока плейнтекст прикрывает маркер, потерянный ключ больше нигде в повседневной работе не проявится |
Сравнение даёт только вердикт — ни одна из сторон в отчёт не попадает. Маркер, перебитый другим маркером, находкой не является: выигравшее значение по-прежнему зашифровано на диске. Эффективное значение считается тем же merge, что и в загрузчике, поэтому вышележащий слой, заменяющий всё родительское поддерево маркера (скаляром, списком), тоже отлавливается.
Все три валидатора секретов выдают строку ✓, когда не нашли проблем, поэтому здоровый проект сообщает validation result: 3 checks, а не validation skipped (no files found) — именно это делает dwe validate secrets пригодным как самопроверка после онбординга ключа. Строке recipient достаточно корректного secrets.recipient, и она идёт без сообщения; строка unresolved появляется, только когда есть что читать и не возникло ни одной диагностики, и считает инвентарь (N encrypted value(s) and M config-pack source(s) readable via <источник>, где источник назван словом — env / env-file / keyfile — и никогда путём); строка shadowed появляется, только когда в проекте есть маркеры, и считает их (N encrypted value(s), none shadowed by a plaintext override) — именно это позволяет зелёному прогону означать «пара ключей и есть то, через что шарятся эти значения», а не только «маркеры расшифровываются». Проект без блока secrets: и без шифрованных значений остаётся молчаливым. Preflight и гейт мастера деплоя отфильтровывают строки SeverityOK, поэтому ни один из них не печатает ничего лишнего.
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 покрывает этот случай.
При per-service деплое probe сужается до портов, которые этот запуск реально может забиндить: dwe deploy run --service <name> (и деплой, который выполняет dwe services enable|disable --apply) проверяет только названные сервисы плюс транзитивное замыкание depends_on из их service.yml — то, что поднимает docker compose up <name>. Чужой контейнер, держащий порт другого сервиса, больше не блокирует запуск, который этот порт не трогает. Запуски на весь проект (dwe deploy run, dwe run) и dwe validate не изменились: они проверяют каждый enabled-сервис. Замыкание идёт только по depends_on из service.yml, поэтому зависимость, объявленная исключительно в голом compose, не учитывается — она проявится ошибкой привязки compose внутри шага.
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. Поскольку .env теперь несёт COMPOSE_PROJECT_NAME (см. render env → Системные переменные), голый compose из корня проекта и так следует имени dwe — расходящийся name: не действует даже в этом случае, а ресурсы, созданные под ним, становятся невидимы для голого compose. Проверка моделирует реальную приоритетность compose: она проходит активную цепочку (ComposeFiles() — только включённые оверлеи) в порядке -f и сравнивает последний объявленный name: (более поздний -f переопределяет более ранний), поэтому базовый name:, уже исправленный более поздним оверлеем, не вызывает предупреждения. Чинится приведением к одному значению: поменять эффективный compose name: на совпадающее, задать project_name в docker.yml и убрать ставший лишним compose name:, либо вписать в compose-файл name: ${COMPOSE_PROJECT_NAME} — теперь оно резолвится из сгенерированного .env. Проверка молчит, когда эффективное имя использует неразрешённую интерполяцию (например, 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), secrets (recipient). Свободные (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.exports предупреждает о правиле exports.env, чей dot-path в from: или when: не резолвится в смерженном конфиге. Это голые dot-path’ы, а не ссылки ${...}, поэтому config.template_refs их не видит: from: vars.db.passwrod — корректная строка, загрузчик её принимает, а dwe render env рендерит DB_PASSWORD= — пустое значение, которое доходит до каждого контейнера так, будто его так и объявили. Нерезолвящийся when: ещё тише: он ложный, поэтому правило пропускается и переменная не появляется вовсе — и раз правило мертво, его from: не репортится вторично. Подсказка называет последствие для конкретной формы правила — переменная рендерится пустой, всегда используется default:, или dwe render env падает на правиле с required:. Гейт читается первым, как его читает рендерер: если when: резолвится, но ложный, правило пропускается ещё до резолва from:, поэтому подсказка вместо этого говорит, что гейт сейчас ложный и промах проявится, когда гейт включится. Это предупреждение, а не ошибка: from: вместе с default: — легитимный паттерн необязательного пути, а сам путь может жить в local.yml, которого нет на этой машине. Путь, который резолвится в существующий ключ со значением null, не репортится — ключ есть, значит пустой рендер осознан автором. Находка привязывается к слою, объявившему правило (workspace.yml, workspace/defaults.yml или workspace/local.yml).
Домен commands применяет тот же критерий к трём dot-path полям в workspace/commands/*.yml: params.<name>.default_from, params.<name>.options.from и context.<name>.from. Каждое предупреждает, когда путь не резолвится в смерженном конфиге — нерезолвящийся default_from оставляет параметр без значения по умолчанию, нерезолвящийся options.from — с пустым списком вариантов, а нерезолвящийся context.from рендерит ${context.<name>} пустой строкой внутри тела команды. Файлы команд проверяются в том виде, как написаны, поэтому синтетические команды, развёрнутые из daemon-блока, никогда не репортятся дважды.
Тот же домен проверяет hide: на командах и в блоках group:. Помимо синтаксиса шаблона, каждое выражение рендерится против смерженного конфига — того же контекста, что использует runtime-видимость, — и ошибка рендера даёт предупреждение hide: expression does not evaluate: …. Runtime-вычисление fail-open, поэтому иначе такое выражение оставляет команду видимой, оставив лишь строку в логе. Затем результат рендера должен быть тем, что runtime умеет вычислить: литерал true / false / 1 / 0 или пустая строка, cmd: <shell-команда> или when:-предикат с аргументами (dir-exists <path>, generated-missing <svc> <field>, …). Всё остальное — yes, опечатка в глаголе вроде dir-exist, file-exists без пути — даёт предупреждение hide: expression renders to "…", which is neither a boolean nor a known predicate; в runtime такое выражение тоже оставило бы команду видимой. Отрендеренный cmd: или builtin-предикат dwe validate не исполняет (сообщается только пустой cmd: — как hide: expression renders to an empty `cmd:` command; в runtime это ошибка вычисления); при неудачной загрузке конфига проверка рендера пропускается. Проверяется только результат для текущего конфига: ветку шаблона, в которую этот конфиг не заходит, проверка не видит. См. Условие hide.
Тот же домен проверяет цель команд service_exec, service_run и daemon. Runtime передаёт service: (или runner.service:) прямо в docker compose exec / run, поэтому там должно стоять имя compose-сервиса, а не ключ dwe-сервиса. Проверка даёт предупреждение service: "<name>" is not a service in any compose overlay, disabled services included, если ни один compose-файл под git не объявляет такой сервис. Это compose.base и файлы из compose: и compose_after: каждого сервиса, включая выключенные, поэтому команда, нацеленная на compose-only сервис выключенного сейчас инструмента, не считается ошибкой. Локальные оверлеи не учитываются: compose.extra из local.yml (общепроектный и посервисный) и сгенерированный bridge-оверлей у каждой машины свои, а файл команд общий, поэтому ответ одинаков в любом чекауте, а сервис, который объявлен только в локальном оверлее, считается ошибкой. Подсказка предлагает ближайшее compose-имя, а если значение является ключом dwe-сервиса, то compose-сервис, под которым этот ключ запускается. Если отслеживаемые файлы не объявляют ни одного сервиса, подсказка говорит об этом. Проверка пропускается, если значение шаблонное (${param.*}, {{ … }}), если конфиг не загрузился и если какой-то файл набора не читается, не разбирается или подключает другие файлы через include: верхнего уровня. Отсутствующий или нечитаемый файл набора сообщает config.compose_files, а файл, который не разбирается, — нет.
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.compose_files предупреждает, когда файл из compose: или compose_after: сервиса не существует (или является каталогом); так же проверяется compose.base, с которого начинается любая цепочка -f. Без этой проверки опечатка всплывает только ошибкой docker compose при следующем dwe run. Пути разрешаются так, как их получает compose, — от корня проекта, а не от папки сервиса; если отсутствующий путь существует относительно папки сервиса, подсказка называет этот файл. Проверяются все сервисы независимо от того, включены ли они: операции compose с --all загружают файлы выключенного сервиса, а его последующее включение сломало бы стек. Путь, унаследованный дочерним сервисом через extends:, сообщается один раз с перечислением всех сервисов, где он указан. Это предупреждение, а не ошибка: compose-файл может лежать в исходниках, которые клонирует dwe deploy run, и в свежем чекауте его может не быть до первого деплоя. Персональные пути compose.extra из local.yml здесь не проверяются: отсутствующий общепроектный файл, как и файл включённого сервиса, уже отвергает LoadConfig.
config.healthcheck_start_period — информационная заметка о compose-сервисе из активной цепочки -f (только включённые сервисы), чей healthcheck выполняет test, но не объявляет start_period. dwe run поднимает стек через docker compose up --wait, и без start_period каждая неудачная проверка медленно стартующего сервиса во время загрузки засчитывается в retries — при значениях по умолчанию (interval: 30s, retries: 3) сервис, которому нужно больше времени, помечается unhealthy, и весь запуск падает. Healthcheck сливается по цепочке так же, как это делает compose, — по ключам: start_period из более раннего файла по-прежнему учитывается, !reset удаляет healthcheck, !override заменяет его. Проверка молчит, если healthcheck выключен (disable: true, test: ["NONE"]) или не объявляет test:; явный start_period: 0s считается объявленным. Healthcheck, зашитый в образ, не виден. Это info, а не предупреждение: большинство сервисов успевают стартовать за interval × retries, заметка называет возможный сценарий отказа, а не дефект, поэтому --strict из-за неё не падает.
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.
config.deploy, config.lifecycle и config.reset применяют ту же трёхсостоянийную модель к файлам пайплайнов. Отсутствующий workspace/deploy.yml / workspace/lifecycle.yml репортится как no <file> — built-in default pipeline is active с SeverityInfo. Файл, состоящий только из комментариев или пустой — то состояние, которое создаёт dwe init, — репортится как <file> has no active content (all comments or empty) — built-in default pipeline is active, тоже с SeverityInfo: файл существует, но реально выполняется встроенный дефолтный пайплайн, поэтому SeverityOK заявлял бы о пайплайне, которого нет в силе. Файл, который парсится, но не несёт пайплайна, — тот же случай в другой обёртке и тоже репортится с SeverityInfo: deploy.yml / reset.yml с пустым или отсутствующим phases: даёт <file> declares no phases — built-in default pipeline is active, а lifecycle.yml без одной из половин — lifecycle.yml declares no run: section — built-in default run pipeline is active (аналогично для stop:, по одной строке на каждую отсутствующую секцию). SeverityOK получает только файл, содержимое которого действительно доходит до движка. Кросс-проверка плана (plan resolution failed: …) выполняется в любом состоянии, поскольку разрешённый план — это ровно то, что выполняет deploy.
Для deploy.yml и reset.yml у этих двух Info-состояний есть прямое лекарство: dwe deploy eject --out workspace/deploy.yml --force (или dwe reset eject --out workspace/reset.yml --force) записывает встроенный дефолт поверх инертного файла, превращая его в настоящий редактируемый пайплайн — без --out документ уходит в stdout и ни один файл не трогается. --force здесь обязателен, потому что инертный файл уже существует: eject отказывается перезаписывать любой существующий файл без него, включая авторский пайплайн. Сообщение отказа опирается ровно на те же два условия, что и этот валидатор, — оно называет файл инертным только тогда, когда он действительно инертен, — так что обе команды всегда согласны в том, какие файлы инертны. У lifecycle.yml аналога eject нет — см. deploy/index.md.
templates.ai / templates.ide / templates.git предупреждают об отсутствующем template-паке только после того, как сервис явно выставил render.<kind>.enabled. Сервис type: app на неявном значении по умолчанию, у которого пака на диске нет, — это состояние после скаффолда, а не дефект, и он молчит. templates.git применяет то же правило к своему уведомлению «нет src/.git» — репозиторий может быть наполнен deploy-шагом ещё до того, как отработает render.
templates.workspace проверяет весь список render.workspace совместно через план workspace-паков. Без записи файлов проверяются имена и дубликаты, манифесты, источники, выполнение .tmpl, защищённые назначения, коллизии между паками и безопасность файловой системы. Отсутствующий пак из списка — ошибка; отсутствующий или пустой список даёт info-диагностику. dwe validate templates workspace запускает только этот валидатор. В preflight он не участвует.
config.reset — единственное исключение из абзаца выше в части отсутствующего файла: при отсутствии workspace/reset.yml он молчит полностью. В отличие от deploy.yml / lifecycle.yml, этот файл никогда не создаётся скаффолдом, поэтому его отсутствие — универсальное состояние почти любого проекта, а не намеренный отказ. Если 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.*,secrets.*,env.*и всеchecks.*. Опциональный позиционный scope сужает запуск (например,dwe validate env,dwe validate checks ghcr-login,dwe validate bridge,dwe validate secrets). Строка итога называет активный scope —(scope: all),(scope: config),(scope: config/services), — а--output jsonнесёт то же значение вsummary.scope, так что суженный запуск отличим от полного не только числом диагностик.dwe validate secrets— только три валидатораsecrets.*: наличие и форма recipient, повреждённые payload’ы маркеров, то, расшифровывается ли здесь каждое зашифрованное значение и каждый источник пака.age, и то, не затеняет ли маркер плейнтекст из вышележащего слоя. Работает даже когда конфиг не загрузился (некорректныйsecrets.recipient— ровно этот случай), потому что валидатор recipient сам грузит файлы слоёв. Ту же картину в виде отчёта, который ничего не блокирует, даётdwe secrets status.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 не запускаются.