fix(deploy): Caddy пересоздаётся только когда правка иначе не доедет (#3443) #3506

Merged
bot-backend merged 2 commits from fix/3443-caddy-selfdowntime into main 2026-09-12 16:16:04 +00:00
Collaborator

Closes #3443.

Полный деплой ПТИЦЫ пересоздавал сам Caddy безусловно и вместе с контейнером уносил единственный процесс, слушающий 80/443: 67 с code=000 на всех доменах хоста (замер 05.09 в #3274).

Что требует пересоздания, а что нет — с уликами

1. Зачем вообще стоял --force-recreate. git log -S даёт ровно один коммит — 11e78d73 от 17.05.2026, «force-recreate caddy чтобы новые compose mounts применились». В теле: reload не пересоздаёт контейнер, поэтому новый bind-маунт ./preview (PR #268) не появился в работающем контейнере. Довод не подтвердился. docker compose up -d БЕЗ флага пересоздаёт контейнер сам, как только меняется описание сервиса или образ — проверено на живом демоне, сервис с двумя маунтами:

маунт добавлен в compose → Container … Starting/Started, id НОВЫЙ
тег переставлен на другой образ → id НОВЫЙ
не менялось ничего → Container … Running, id ТОТ ЖЕ

Оговорка про стенд: этот замер сделан на docker 28.4.0 / compose v2.39.4 (macOS), а прод — docker 29.7.2 / compose v5.5.0. Ревьюер подтвердил вывод на прод-комбинации: метка com.docker.compose.config-hash живого контейнера совпадает с compose config --hash=caddy, а up --help на проде дословно говорит, что умолчание уже пересоздаёт контейнер при смене конфигурации или образа. Одну версию за другую не выдаём.

2. Маунты Caddy по типу (docker inspect gendesign-caddy-1, прод, 12.09):

тип путь инод держит
файл Caddyfile да
файл caddy/users.caddy.snippet да
файл caddy/metrics-ui.caddy.snippet да
файл caddy/metrics-ingest.caddy.snippet да
файл caddy/deploy-window.caddy.snippet да
каталог caddy/sites нет
каталог caddy/local нет
каталог preview нет
том caddy_data, caddy_config, caddy_logs

git reset --hard не правит файл на месте: проверено — инод 65870621 → 65870670. Пофайловый bind-маунт остаётся на прежнем иноде, и caddy reload перечитывает СТАРЫЙ текст (тот же механизм уже ловили на Alertmanager 27.08 и на Alloy #3380). Для каталогов этого нет — правка внутри видна контейнеру сразу.

3. Что умеет caddy reload по факту. Подхватывает всё, что лежит в КАТАЛОГАХ (caddy/sites/** — самый частый случай, 15 правок apps.caddy за 180 суток; caddy/local/**). НЕ подхватывает: смену образа, смену описания сервиса (маунты/env/порты) и содержимое пяти пофайловых маунтов.

4. Побочно закрытая дыра — ещё НЕ сработавшая. Из п.3 следует, что быстрый путь deploy-caddy с голым exec caddy reload после git reset --hard не доставил бы правку Caddyfile или сниппета: на диске новая версия, прокси работает по старой, джоба зелёная. Важно не преувеличить: за 180 суток было 33 коммита «только конфиг прокси», из них 25 трогают пофайловый маунт, но быстрый путь был мёртв до сегодняшнего дня (#3448) — все 25 ехали полным деплоем и доезжали. Дыра открылась сегодня в 11:43Z вместе с мержем #3465 и ни разу не сработала. Честная формулировка: первый же caddy-only мерж потерял бы правку.

Форма правки и почему такая

Оба пути деплоя зовут ops/caddy-apply.sh; условие пересоздания вычисляемое, а не «всегда» и не «никогда»:

  1. caddy validate одноразовым контейнером по файлам с хоста (docker run --rm -v $PWD:/work:ro caddy:2) — тот же образец, что на PR-гейте ci.yml. exec caddy validate тут не годится: он проверил бы старый инод. Битый конфиг → скрипт падает, работающий Caddy не тронут.
  2. up -d --no-deps caddy без --force-recreate — пересоздаст сам, если сменились образ/маунты/env/порты. Если пересоздал — на этом всё.
  3. Контейнер тот же → сверка sha256sum каждого пофайлового bind-маунта (список берётся из docker inspect, поэтому новый маунт в compose попадает в сверку автоматически) с тем, что видит контейнер. Число сверенных файлов печатается.
  4. Расхождение → --force-recreate (единственный способ доставить), с печатью того, какие именно пути не доехали. Нет расхождения → caddy reload, соединения не рвутся.

Сверка по содержимому, а не по иноду: файл, переписанный тем же текстом, пересоздания не требует. Любой отказ самой сверки (docker inspect молчит/упал, ноль пофайловых маунтов, файла на хосте нет, docker exec не ответил) считается расхождением — fail-safe в прежнее поведение.

Граница проверки, которую правка НЕ закрывает: в полном деплое общий up -d $UP_SERVICES (deploy.yml:959) поднимает в том числе caddy за ~110 строк до вызова скрипта. Правка, которая одновременно ломает Caddyfile И меняет блок caddy в docker-compose.prod.yml, пересоздаст контейнер там — раньше валидации. Первая линия против этого остаётся прежней: гейт caddy validate на PR (#2913). Записано в комментарии скрипта, чинить в этом PR не предлагается.

Архитектурные варианты из issue (второй экземпляр Caddy с передачей сокета, socket activation) не реализованы: после разбора они нужны только для случая «содержимое пофайлового маунта изменилось», который стал редким. Следующий по дешевизне шаг, если и это окно мешает, — смонтировать ./caddy каталогом вместо четырёх сниппетов пофайлово (заодно снимает класс #3102 «забыли маунт»); остался бы только Caddyfile. Не делаю здесь: трогает compose и scripts/check-caddy-snippet-mounts.py, а окно того не стоит без замера.

Правки по итогам deep-ревью (коммит 2)

  • M1 — провал сверки давал fail-OPEN. stale=$(docker inspect … | while …) под set -eu без pipefail (в POSIX-sh его нет) отдаёт статус while, то есть всегда 0: отказ docker inspect давал пустой список → «всё доехало» → reload → зелёная джоба с надписью «окна недоступности нет» при прокси на старом конфиге. Список теперь читается отдельной командой; провал, пустой вывод и «ноль пофайловых маунтов» считаются расхождением; в лог печатается число сверенных файлов.
  • M2 — гейт не побежал бы на правке самого скрипта. В фильтре backend (ci.yml) не было ops/**, а все содержательные регрессии живут в ops/caddy-apply.sh. Добавлена одна строка с объяснением — тот же класс, что #2950/#3448/#3467.
  • [ -d "$src" ] && continue вместо [ -f "$src" ] || continue: пропуск по -f склеивал «это каталог» и «файла на хосте нет» (а в контейнере в этот момент живёт старый инод).
  • Сообщение об отказе caddy validate больше не называет причиной битый конфиг, когда упасть мог и сам запуск проверочного контейнера.

Проверено

  • docker run --rm -v "$PWD:/work:ro" -w /work caddy:2 caddy validate … на дереве репозитория → Valid configuration. На проде условия те же: CADDY_SITES в .env не задан (дефолт *, оба site-файла), в caddy/local только .gitignore.
  • docker inspect --format '{{range .Mounts}}…' и docker exec gendesign-caddy-1 sha256sum — исполнены на проде (чтение): шаблон отдаёт src|dst ровно по bind-маунтам, sha256sum в caddy:2 есть, и сегодня хост и контейнер совпадают по всем пяти файлам → на следующем деплое скрипт пошёл бы веткой reload.
  • Контроль пробой (scripts/probe-deploy-window.sh, с хоста, 1555 образцов, шаг 0,54 с, окно без деплоя): 1555 × 200, максимальная серия не-200 — 0. Сам тракт пробы чист, и 000 в окне деплоя будет свойством деплоя, а не пробы.
  • Гейт backend/tests/ops/test_3443_caddy_reload_not_recreate.py — 22 проверки, зелёные; соседние test_3448, test_2950, test_3029, test_3467 — тоже.

Мутационный прогон гейта (11 из 11 краснеют)

Дефект возвращался руками в рабочем дереве и убирался обратно копией файла (не стэшем — стэш общий на репозиторий).

мутант красный тест
A1 if [ -n "$stale" ]if true (пересоздавать всегда) test_nothing_changed_reloads_without_recreate, test_directory_mount_change_does_not_recreate
A2 → if false (не пересоздавать никогда) 9 тестов, включая test_changed_file_mount_forces_recreate и все четыре сниппета
A3 безусловный up -d --force-recreate --no-deps caddy вернулся в job deploy test_full_deploy_has_no_unconditional_caddy_recreate
A4 быстрый путь вернулся к голому exec caddy reload test_deploy_applies_caddy_config_through_the_script[deploy-caddy]
M1 docker inspect упал → stale="" (fail-open) test_unreadable_mount_list_falls_back_to_recreate
M1b ноль пофайловых маунтов считается успехом test_no_file_mounts_is_not_silence
M2 -d обратно на -f (исчезнувший файл молча пропущен) test_missing_host_file_is_not_skipped_as_a_directory
M3 caddy reload … || true test_failures_are_not_swallowed
M3b sh ops/caddy-apply.sh || true в полном деплое test_failures_are_not_swallowed
M4 ops/** убран из фильтра backend в ci.yml test_gate_runs_on_changes_to_the_script_itself
M5 число сверенных маунтов не печатается test_log_says_how_many_mounts_were_compared

Контроль после восстановления файлов — 22 passed.

Приёмка — ПОСЛЕ мержа, не выполнена

Критерий записан до правки и проверяем только на живом деплое:

  • на окне следующего полного деплоя ПТИЦЫ непрерывная проба с хоста (ssh poincare 'nohup /opt/gendesign/scripts/probe-deploy-window.sh https://meraocenka.ru/ 0.2 900 /tmp/probe-3443.tsv &') даёт максимальную серию 000 меньше 2 с против нынешних 67 с;
  • в логе джобы deploy видно, какой веткой пошёл скрипт, и число сверенных маунтов («сверено пофайловых маунтов …: 5»);
  • docker inspect gendesign-caddy-1 --format '{{.Created}}' НЕ меняется на деплое, который не трогал конфиг прокси. Оговорка: deploy.yml:794 делает compose pull без списка сервисов, поэтому свежий upstream-релиз caddy:2 законно сменит Created и даст ложное «не сработало» — сверять вместе с логом Caddy (при пересоздании там холодный старт serving initial configuration, при reload — сообщение о перезагрузке конфига);
  • отдельно — правка одного Caddyfile быстрым путём: содержимое обязано доехать (либо reload по совпавшим суммам, либо явное пересоздание с названными путями).

Дата постановки приёмки — 12.09.2026; до её снятия считать правку непроверенной на проде.

Closes #3443. Полный деплой ПТИЦЫ пересоздавал сам Caddy безусловно и вместе с контейнером уносил единственный процесс, слушающий 80/443: 67 с `code=000` на всех доменах хоста (замер 05.09 в #3274). ## Что требует пересоздания, а что нет — с уликами **1. Зачем вообще стоял `--force-recreate`.** `git log -S` даёт ровно один коммит — `11e78d73` от 17.05.2026, «force-recreate caddy чтобы новые compose mounts применились». В теле: `reload` не пересоздаёт контейнер, поэтому новый bind-маунт `./preview` (PR #268) не появился в работающем контейнере. **Довод не подтвердился.** `docker compose up -d` БЕЗ флага пересоздаёт контейнер сам, как только меняется описание сервиса или образ — проверено на живом демоне, сервис с двумя маунтами: ``` маунт добавлен в compose → Container … Starting/Started, id НОВЫЙ тег переставлен на другой образ → id НОВЫЙ не менялось ничего → Container … Running, id ТОТ ЖЕ ``` **Оговорка про стенд:** этот замер сделан на docker 28.4.0 / compose v2.39.4 (macOS), а прод — docker 29.7.2 / compose v5.5.0. Ревьюер подтвердил вывод на прод-комбинации: метка `com.docker.compose.config-hash` живого контейнера совпадает с `compose config --hash=caddy`, а `up --help` на проде дословно говорит, что умолчание уже пересоздаёт контейнер при смене конфигурации или образа. Одну версию за другую не выдаём. **2. Маунты Caddy по типу** (`docker inspect gendesign-caddy-1`, прод, 12.09): | тип | путь | инод держит | |---|---|---| | файл | `Caddyfile` | да | | файл | `caddy/users.caddy.snippet` | да | | файл | `caddy/metrics-ui.caddy.snippet` | да | | файл | `caddy/metrics-ingest.caddy.snippet` | да | | файл | `caddy/deploy-window.caddy.snippet` | да | | каталог | `caddy/sites` | нет | | каталог | `caddy/local` | нет | | каталог | `preview` | нет | | том | `caddy_data`, `caddy_config`, `caddy_logs` | — | `git reset --hard` не правит файл на месте: проверено — инод 65870621 → 65870670. Пофайловый bind-маунт остаётся на прежнем иноде, и `caddy reload` перечитывает СТАРЫЙ текст (тот же механизм уже ловили на Alertmanager 27.08 и на Alloy #3380). Для каталогов этого нет — правка внутри видна контейнеру сразу. **3. Что умеет `caddy reload` по факту.** Подхватывает всё, что лежит в КАТАЛОГАХ (`caddy/sites/**` — самый частый случай, 15 правок `apps.caddy` за 180 суток; `caddy/local/**`). НЕ подхватывает: смену образа, смену описания сервиса (маунты/env/порты) и содержимое пяти пофайловых маунтов. **4. Побочно закрытая дыра — ещё НЕ сработавшая.** Из п.3 следует, что быстрый путь `deploy-caddy` с голым `exec caddy reload` после `git reset --hard` не доставил бы правку `Caddyfile` или сниппета: на диске новая версия, прокси работает по старой, джоба зелёная. Важно не преувеличить: за 180 суток было 33 коммита «только конфиг прокси», из них 25 трогают пофайловый маунт, но **быстрый путь был мёртв до сегодняшнего дня** (#3448) — все 25 ехали полным деплоем и доезжали. Дыра открылась сегодня в 11:43Z вместе с мержем #3465 и ни разу не сработала. Честная формулировка: **первый же caddy-only мерж потерял бы правку**. ## Форма правки и почему такая Оба пути деплоя зовут `ops/caddy-apply.sh`; условие пересоздания вычисляемое, а не «всегда» и не «никогда»: 1. `caddy validate` одноразовым контейнером по файлам **с хоста** (`docker run --rm -v $PWD:/work:ro caddy:2`) — тот же образец, что на PR-гейте ci.yml. `exec caddy validate` тут не годится: он проверил бы старый инод. Битый конфиг → скрипт падает, работающий Caddy не тронут. 2. `up -d --no-deps caddy` без `--force-recreate` — пересоздаст сам, если сменились образ/маунты/env/порты. Если пересоздал — на этом всё. 3. Контейнер тот же → сверка `sha256sum` каждого пофайлового bind-маунта (список берётся из `docker inspect`, поэтому новый маунт в compose попадает в сверку автоматически) с тем, что видит контейнер. Число сверенных файлов печатается. 4. Расхождение → `--force-recreate` (единственный способ доставить), с печатью того, какие именно пути не доехали. Нет расхождения → `caddy reload`, соединения не рвутся. Сверка по содержимому, а не по иноду: файл, переписанный тем же текстом, пересоздания не требует. Любой отказ самой сверки (`docker inspect` молчит/упал, ноль пофайловых маунтов, файла на хосте нет, `docker exec` не ответил) считается расхождением — fail-safe в прежнее поведение. **Граница проверки, которую правка НЕ закрывает:** в полном деплое общий `up -d $UP_SERVICES` (`deploy.yml:959`) поднимает в том числе caddy за ~110 строк до вызова скрипта. Правка, которая одновременно ломает `Caddyfile` И меняет блок caddy в `docker-compose.prod.yml`, пересоздаст контейнер там — раньше валидации. Первая линия против этого остаётся прежней: гейт `caddy validate` на PR (#2913). Записано в комментарии скрипта, чинить в этом PR не предлагается. Архитектурные варианты из issue (второй экземпляр Caddy с передачей сокета, socket activation) **не** реализованы: после разбора они нужны только для случая «содержимое пофайлового маунта изменилось», который стал редким. Следующий по дешевизне шаг, если и это окно мешает, — смонтировать `./caddy` каталогом вместо четырёх сниппетов пофайлово (заодно снимает класс #3102 «забыли маунт»); остался бы только `Caddyfile`. Не делаю здесь: трогает compose и `scripts/check-caddy-snippet-mounts.py`, а окно того не стоит без замера. ## Правки по итогам deep-ревью (коммит 2) * **M1 — провал сверки давал fail-OPEN.** `stale=$(docker inspect … | while …)` под `set -eu` без `pipefail` (в POSIX-sh его нет) отдаёт статус `while`, то есть всегда 0: отказ `docker inspect` давал пустой список → «всё доехало» → `reload` → зелёная джоба с надписью «окна недоступности нет» при прокси на старом конфиге. Список теперь читается отдельной командой; провал, пустой вывод и «ноль пофайловых маунтов» считаются расхождением; в лог печатается число сверенных файлов. * **M2 — гейт не побежал бы на правке самого скрипта.** В фильтре `backend` (ci.yml) не было `ops/**`, а все содержательные регрессии живут в `ops/caddy-apply.sh`. Добавлена одна строка с объяснением — тот же класс, что #2950/#3448/#3467. * `[ -d "$src" ] && continue` вместо `[ -f "$src" ] || continue`: пропуск по `-f` склеивал «это каталог» и «файла на хосте нет» (а в контейнере в этот момент живёт старый инод). * Сообщение об отказе `caddy validate` больше не называет причиной битый конфиг, когда упасть мог и сам запуск проверочного контейнера. ## Проверено * `docker run --rm -v "$PWD:/work:ro" -w /work caddy:2 caddy validate …` на дереве репозитория → `Valid configuration`. На проде условия те же: `CADDY_SITES` в `.env` не задан (дефолт `*`, оба site-файла), в `caddy/local` только `.gitignore`. * `docker inspect --format '{{range .Mounts}}…'` и `docker exec gendesign-caddy-1 sha256sum` — исполнены на проде (чтение): шаблон отдаёт `src|dst` ровно по bind-маунтам, `sha256sum` в `caddy:2` есть, и сегодня хост и контейнер совпадают по всем пяти файлам → на следующем деплое скрипт пошёл бы веткой `reload`. * Контроль пробой (`scripts/probe-deploy-window.sh`, с хоста, 1555 образцов, шаг 0,54 с, окно без деплоя): 1555 × 200, максимальная серия не-200 — 0. Сам тракт пробы чист, и `000` в окне деплоя будет свойством деплоя, а не пробы. * Гейт `backend/tests/ops/test_3443_caddy_reload_not_recreate.py` — 22 проверки, зелёные; соседние `test_3448`, `test_2950`, `test_3029`, `test_3467` — тоже. ## Мутационный прогон гейта (11 из 11 краснеют) Дефект возвращался руками в рабочем дереве и убирался обратно копией файла (не стэшем — стэш общий на репозиторий). | мутант | красный тест | |---|---| | A1 `if [ -n "$stale" ]` → `if true` (пересоздавать всегда) | `test_nothing_changed_reloads_without_recreate`, `test_directory_mount_change_does_not_recreate` | | A2 → `if false` (не пересоздавать никогда) | 9 тестов, включая `test_changed_file_mount_forces_recreate` и все четыре сниппета | | A3 безусловный `up -d --force-recreate --no-deps caddy` вернулся в job `deploy` | `test_full_deploy_has_no_unconditional_caddy_recreate` | | A4 быстрый путь вернулся к голому `exec caddy reload` | `test_deploy_applies_caddy_config_through_the_script[deploy-caddy]` | | M1 `docker inspect` упал → `stale=""` (fail-open) | `test_unreadable_mount_list_falls_back_to_recreate` | | M1b ноль пофайловых маунтов считается успехом | `test_no_file_mounts_is_not_silence` | | M2 `-d` обратно на `-f` (исчезнувший файл молча пропущен) | `test_missing_host_file_is_not_skipped_as_a_directory` | | M3 `caddy reload … \|\| true` | `test_failures_are_not_swallowed` | | M3b `sh ops/caddy-apply.sh \|\| true` в полном деплое | `test_failures_are_not_swallowed` | | M4 `ops/**` убран из фильтра `backend` в ci.yml | `test_gate_runs_on_changes_to_the_script_itself` | | M5 число сверенных маунтов не печатается | `test_log_says_how_many_mounts_were_compared` | Контроль после восстановления файлов — 22 passed. ## Приёмка — ПОСЛЕ мержа, не выполнена Критерий записан до правки и проверяем только на живом деплое: - [ ] на окне следующего полного деплоя ПТИЦЫ непрерывная проба с хоста (`ssh poincare 'nohup /opt/gendesign/scripts/probe-deploy-window.sh https://meraocenka.ru/ 0.2 900 /tmp/probe-3443.tsv &'`) даёт максимальную серию `000` **меньше 2 с** против нынешних 67 с; - [ ] в логе джобы `deploy` видно, какой веткой пошёл скрипт, и число сверенных маунтов («сверено пофайловых маунтов …: 5»); - [ ] `docker inspect gendesign-caddy-1 --format '{{.Created}}'` НЕ меняется на деплое, который не трогал конфиг прокси. **Оговорка:** `deploy.yml:794` делает `compose pull` без списка сервисов, поэтому свежий upstream-релиз `caddy:2` законно сменит `Created` и даст ложное «не сработало» — сверять вместе с логом Caddy (при пересоздании там холодный старт `serving initial configuration`, при reload — сообщение о перезагрузке конфига); - [ ] отдельно — правка одного `Caddyfile` быстрым путём: содержимое обязано доехать (либо reload по совпавшим суммам, либо явное пересоздание с названными путями). Дата постановки приёмки — 12.09.2026; до её снятия считать правку непроверенной на проде.
bot-backend added 1 commit 2026-09-12 14:41:12 +00:00
fix(deploy): Caddy пересоздаётся только когда правка иначе не доедет (#3443)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m56s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Successful in 17m31s
9fedaa4582
Полный деплой ПТИЦЫ каждый раз делал `up -d --force-recreate --no-deps caddy`
и сносил единственный процесс, слушающий 80/443: замер 05.09 (#3274) — 67 с
`code=000` на ВСЕХ доменах хоста, включая публичный лендинг МЕРЫ. Заглушка
окна деплоя здесь бессильна по построению: её отдаёт тот же Caddy.

Что установлено, а не принято на веру:

* Безусловный флаг появился 17.05 (11e78d73) ради нового bind-маунта
  `./preview` — «иначе новые volume mounts не появляются». Довод неверен:
  `docker compose up -d` БЕЗ `--force-recreate` пересоздаёт контейнер сам при
  смене описания сервиса или образа. Проверено на живом демоне (docker 28.4):
  добавлен volume → новый id; тег переставлен на другой образ → новый id;
  не менялось ничего → `Container … Running`, id тот же.
* Compose не видит только одного: СОДЕРЖИМОГО пофайлового bind-маунта.
  `git reset --hard` пишет новый инод, контейнер держит прежний, и `caddy
  reload` перечитывает старый текст (тот же механизм — Alertmanager 27.08 и
  Alloy #3380). У Caddy так смонтированы пять путей: Caddyfile и четыре
  сниппета; каталоги (caddy/sites, caddy/local, preview) этим не страдают —
  самый частый случай, caddy/sites/apps.caddy, пересоздания НЕ требует.
* Отсюда же второй, беззвучный дефект: быстрый путь `deploy-caddy` делал голый
  `exec caddy reload` после `git reset --hard`, то есть правка Caddyfile или
  сниппета до контейнера не доезжала вовсе, а джоба уходила зелёной.

Оба пути деплоя теперь зовут ops/caddy-apply.sh: `caddy validate` одноразовым
контейнером по файлам С ХОСТА (битый конфиг не применяется и прокси не
трогает) → `up -d` без `--force-recreate` → если контейнер тот же, сверка
sha256 каждого пофайлового маунта с тем, что видит контейнер → пересоздание
ТОЛЬКО при расхождении, иначе `caddy reload` без разрыва соединений.
Не прочиталось — считаем расхождением: fail-safe в сторону прежнего поведения.

Гейт backend/tests/ops/test_3443_caddy_reload_not_recreate.py исполняет скрипт
с подставным `docker` и смотрит на совершённые действия, а не на его текст:
ничего не менялось → reload без пересоздания; правлен Caddyfile или любой из
сниппетов → пересоздание; правка в каталоге → без пересоздания; битый конфиг →
не тронуто ничего; compose пересоздал сам → второго пересоздания нет.
Отдельно — проводка в deploy.yml и запрет безусловного `--force-recreate` для
Caddy в полном деплое.

Приёмка (#3443) снимается ПОСЛЕ мержа, на живом деплое: непрерывная проба
`scripts/probe-deploy-window.sh` с хоста — максимальная серия `000` меньше 2 с
против нынешних 67 с.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Light1YT added 1 commit 2026-09-12 15:49:45 +00:00
fix(deploy): сверка маунтов Caddy не может провалиться втихую (#3443, ревью)
All checks were successful
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 1m8s
CI / openapi-codegen-check (pull_request) Successful in 2m3s
CI / backend-tests (pull_request) Successful in 17m40s
d5f0557ca8
Две дыры из deep-ревью PR #3506 — обе про «отказ выглядит как успех».

M1. `stale=$(docker inspect … | while …)` под `set -eu` без `pipefail`
(в POSIX-sh его нет) отдаёт статус `while`, то есть всегда 0. Провал
`docker inspect` или пустой вывод давали пустой список → ветка «всё
доехало» → `caddy reload` → зелёная джоба с надписью «окна недоступности
нет» при прокси, работающем по СТАРОМУ конфигу. Ровно тот беззвучный
отказ, ради которого написан скрипт. Теперь список читается отдельной
командой, провал и пустой вывод считаются расхождением (fail-safe в
прежнее поведение), число сверенных файлов печатается — «сверили пять» и
«сверили ноль» в логе больше не выглядят одинаково. Ноль пофайловых
маунтов (например, если Caddyfile переведут на именованный том) — тоже
расхождение, а не тавтологически успешная сверка.

M2. В фильтре `backend` (ci.yml) не было `ops/**`, а все содержательные
регрессии живут в самом ops/caddy-apply.sh: гейт его ИСПОЛНЯЕТ. PR,
правящий только скрипт, давал backend=false — джоба пропускается, гейт не
исполняется, «пересоздавать всегда» уезжает в main зелёным. Тот же класс,
что уже осуждён комментариями рядом (#2950/#3448/#3467).

Мелочи оттуда же:
* `[ -d "$src" ] && continue` вместо `[ -f "$src" ] || continue` — пропуск
  по `-f` склеивал «это каталог» (пропустить верно) и «файла на хосте
  нет», для которого в контейнере как раз живёт старый инод;
* сообщение об отказе `caddy validate` больше не называет причиной
  битый конфиг, когда упасть мог и сам запуск проверочного контейнера;
* в комментарии к проверке записана её граница: в полном деплое общий
  `up -d $UP_SERVICES` (deploy.yml:959) поднимает и caddy за ~110 строк
  до вызова скрипта, поэтому правка, которая одновременно ломает Caddyfile
  и меняет блок caddy в compose, пересоздаст контейнер раньше проверки.

Гейт дорос с 16 до 22 проверок: `docker inspect` не ответил → пересоздание,
ноль пофайловых маунтов → пересоздание, исчезнувший файл на хосте →
пересоздание, число сверенных маунтов печатается, `caddy reload`/вызов
скрипта не проглочены `|| true`, ci.yml-фильтр покрывает ops/**.
Все 11 мутантов (7 новых + 4 прежних) краснеют, контроль зелёный.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bot-backend merged commit 68495907d5 into main 2026-09-12 16:16:04 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3506
No description provided.