feat(ops): сторож расхождения «прод ↔ main» — недоехавший деплой перестаёт быть молчаливым (#3029) #3125

Merged
lekss361 merged 1 commit from chore/3029-deploy-drift-guard into main 2026-08-27 09:55:04 +00:00
Owner

Прямое следствие сегодняшней аварии. Разбор — в #3029 и #3119.

Что случилось и почему это не поймали

27.08 замена IP сервера (#3110) осиротила секрет DEPLOY_HOST. deploy.yml падал на

2026/08/27 08:47:10 dial tcp ***:***: i/o timeout

При этом CI оставался зелёным, PR мержились, а deploy-infra.yml исправно обновлял Beget — со стороны всё выглядело здоровым. Прод сутки жил на позавчерашнем коммите, и расхождение нашлось только руками, сверкой двух git log.

Проверки, которая спрашивает не «прошёл ли прогон», а «доехал ли код», не было ни одной.

Что добавляется

.forgejo/workflows/deploy-drift.yml — ежечасно (в :23, вне толкучки со сторожами бэкапов) читает HEAD с прод-хоста и сверяет с tip main. Read-only: один ssh + git rev-parse, ничего не деплоит.

scripts/check-deploy-drift.sh — вердикт, без сети. Разделение намеренное: SSH требует секретов и в тесте невоспроизводим, а логика — воспроизводима, поэтому тест исполняет настоящий скрипт, а не пересказывает его. Ошибка в самом bash (несработавшая подстановка, опечатка в сравнении) видна только при запуске.

Три исхода, а не два

Код Значение
0 совпало — либо расхождение моложе льготного периода
1 прод отстал — код из main не работает
2 состояние прода неизвестно — хост не ответил

Разделение 1 и 2 — центральное. Это разные аварии с разной первой командой в разборе, и сегодня погорели ровно на их смешении: недоступность хоста выглядела как обычный красный прогон. Тест сторожит это отдельно: если кто-то упростит до «не совпало → 1», он покраснеет.

Льготный период

30 минут. Ежечасный сторож неизбежно попадёт в окно между мержем и концом выката; без льготы он давал бы ложную тревогу несколько раз в неделю. Сторож, которого научились игнорировать, хуже отсутствующего. Граница проверена в обе стороны — и что молчит внутри льготы, и что кричит за ней.

Проверка

$ python -m pytest tests/ops/test_3029_deploy_drift.py -q
9 passed

Покрыто: совпадение; короткий SHA с хоста против полного из git (без сравнения по префиксу сторож кричал бы на каждом прогоне); старое расхождение; свежий коммит внутри льготы; истёкшая льгота; пустой и мусорный ответ хоста; пустой EXPECTED_SHA (сторож, возвращающий ноль при поломке собственного входа, создаёт ложную уверенность); связность workflow ↔ скрипт.

Чего это не делает

Не решает доставку. Красный прогон по-прежнему виден только тому, кто откроет Actions. Полноценное оповещение упирается в METRICS_TELEGRAM_BOT_TOKEN/CHAT_ID (#3078) — это за владельцем. Здесь закрыта половина «обнаружение»; без неё вторая половина всё равно нечего было бы отправлять.

Refs #3029, #3119, #3110, #3078

Прямое следствие сегодняшней аварии. Разбор — в #3029 и #3119. ## Что случилось и почему это не поймали 27.08 замена IP сервера (#3110) осиротила секрет `DEPLOY_HOST`. `deploy.yml` падал на ``` 2026/08/27 08:47:10 dial tcp ***:***: i/o timeout ``` При этом **CI оставался зелёным, PR мержились**, а `deploy-infra.yml` исправно обновлял Beget — со стороны всё выглядело здоровым. Прод сутки жил на позавчерашнем коммите, и расхождение нашлось только руками, сверкой двух `git log`. Проверки, которая спрашивает не «прошёл ли прогон», а **«доехал ли код»**, не было ни одной. ## Что добавляется **`.forgejo/workflows/deploy-drift.yml`** — ежечасно (в `:23`, вне толкучки со сторожами бэкапов) читает `HEAD` с прод-хоста и сверяет с tip `main`. Read-only: один `ssh` + `git rev-parse`, ничего не деплоит. **`scripts/check-deploy-drift.sh`** — вердикт, без сети. Разделение намеренное: SSH требует секретов и в тесте невоспроизводим, а логика — воспроизводима, поэтому тест **исполняет настоящий скрипт**, а не пересказывает его. Ошибка в самом bash (несработавшая подстановка, опечатка в сравнении) видна только при запуске. ### Три исхода, а не два | Код | Значение | |---|---| | 0 | совпало — либо расхождение моложе льготного периода | | 1 | **прод отстал** — код из main не работает | | 2 | **состояние прода неизвестно** — хост не ответил | Разделение 1 и 2 — центральное. Это разные аварии с разной первой командой в разборе, и сегодня погорели ровно на их смешении: недоступность хоста выглядела как обычный красный прогон. Тест сторожит это отдельно: если кто-то упростит до «не совпало → 1», он покраснеет. ### Льготный период 30 минут. Ежечасный сторож неизбежно попадёт в окно между мержем и концом выката; без льготы он давал бы ложную тревогу несколько раз в неделю. Сторож, которого научились игнорировать, хуже отсутствующего. Граница проверена в обе стороны — и что молчит внутри льготы, и что кричит за ней. ## Проверка ``` $ python -m pytest tests/ops/test_3029_deploy_drift.py -q 9 passed ``` Покрыто: совпадение; короткий SHA с хоста против полного из git (без сравнения по префиксу сторож кричал бы на каждом прогоне); старое расхождение; свежий коммит внутри льготы; истёкшая льгота; пустой и мусорный ответ хоста; пустой `EXPECTED_SHA` (сторож, возвращающий ноль при поломке собственного входа, создаёт ложную уверенность); связность workflow ↔ скрипт. ## Чего это не делает **Не решает доставку.** Красный прогон по-прежнему виден только тому, кто откроет Actions. Полноценное оповещение упирается в `METRICS_TELEGRAM_BOT_TOKEN`/`CHAT_ID` (#3078) — это за владельцем. Здесь закрыта половина «обнаружение»; без неё вторая половина всё равно нечего было бы отправлять. Refs #3029, #3119, #3110, #3078
lekss361 added 1 commit 2026-08-27 09:36:26 +00:00
feat(ops): сторож расхождения «прод ↔ main» — молчаливый недоехавший деплой становится видимым (#3029)
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 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m8s
CI / backend-tests (pull_request) Successful in 17m30s
de56b8ae01
27.08 прод сутки жил на позавчерашнем коммите, и это нашлось только руками.
Замена IP сервера (#3110) осиротила секрет DEPLOY_HOST: deploy.yml падал на
`dial tcp ***:***: i/o timeout`, при этом CI оставался зелёным, PR
продолжали мержиться, а deploy-infra.yml исправно обновлял Beget и создавал
впечатление, что всё в порядке.

Проверок, которые спрашивают не «прошёл ли прогон», а «доехал ли код», не
было ни одной. Этот сторож — ровно такая: ежечасно читает HEAD с прод-хоста
и сверяет с tip main.

Три исхода вместо двух. «Хост не ответил» (2) отделён от «на хосте не тот
код» (1) — это разные аварии с разной первой командой в разборе, и сегодня
погорели именно на их смешении: недоступность выглядела как обычный красный
прогон. Льготный период 30 минут гасит ложную тревогу на деплое, который
ещё в полёте: ежечасный сторож неизбежно попадёт в окно между мержем и
концом выката, а сторож, которого научились игнорировать, хуже отсутствующего.

Логика вынесена в scripts/check-deploy-drift.sh и не ходит по сети — SSH
живёт в workflow, где секреты. Благодаря этому тест ИСПОЛНЯЕТ настоящий
скрипт, а не пересказывает его: ошибка в самом bash видна только при запуске.

Read-only: один ssh и git rev-parse, ничего не деплоит.
lekss361 merged commit 70db090813 into main 2026-08-27 09:55:04 +00:00
lekss361 deleted branch chore/3029-deploy-drift-guard 2026-08-27 09:55:04 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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#3125
No description provided.