gendesign/scripts/check-deploy-drift.sh
bot-backend de56b8ae01
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
feat(ops): сторож расхождения «прод ↔ main» — молчаливый недоехавший деплой становится видимым (#3029)
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, ничего не деплоит.
2026-08-27 12:35:43 +03:00

87 lines
5 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Сторож расхождения «код на проде ↔ main» (#3029).
#
# ЗАЧЕМ. 27.08.2026 прод сутки жил на коммите позавчерашней давности, и никто
# этого не заметил: замена IP сервера (#3110) осиротила секрет DEPLOY_HOST,
# deploy.yml падал на `dial tcp ***:***: i/o timeout`, а CI при этом оставался
# зелёным и PR продолжали мержиться. Расхождение нашлось только руками — сверкой
# двух `git log`. Проверок, которые смотрят НЕ «прошёл ли прогон», а «доехал ли
# код», не было ни одной.
#
# Скрипт намеренно НЕ ходит по сети: он получает уже снятые значения и выносит
# вердикт. SSH живёт в workflow (там секреты), логика — здесь (её можно
# исполнить в тесте, а не пересказать).
#
# Вход — переменные окружения:
# DEPLOYED_SHA SHA, прочитанный с прод-хоста (может быть коротким)
# EXPECTED_SHA SHA, которого ждём (tip main)
# EXPECTED_COMMIT_EPOCH время коммита EXPECTED_SHA, unix-time (необязательно)
# NOW_EPOCH «сейчас» (необязательно; для тестов)
# GRACE_MINUTES сколько минут расхождение считается нормой (по умолчанию 30)
#
# Коды возврата:
# 0 совпало, либо расхождение моложе GRACE_MINUTES (деплой в полёте)
# 1 расхождение старше GRACE_MINUTES — прод отстал, надо смотреть
# 2 состояние прода прочитать не удалось (пустой DEPLOYED_SHA)
#
# ПОЧЕМУ 2, А НЕ 1. «Хост недоступен» и «хост отдал не тот код» — разные
# аварии с разной первой командой в разборе. Ровно на этом различии сегодня и
# погорели: недоступность выглядела как обычный красный прогон.
set -euo pipefail
GRACE_MINUTES="${GRACE_MINUTES:-30}"
NOW_EPOCH="${NOW_EPOCH:-$(date +%s)}"
DEPLOYED_SHA="${DEPLOYED_SHA:-}"
EXPECTED_SHA="${EXPECTED_SHA:-}"
EXPECTED_COMMIT_EPOCH="${EXPECTED_COMMIT_EPOCH:-}"
if [ -z "$EXPECTED_SHA" ]; then
echo "ОШИБКА: EXPECTED_SHA пуст — сверять не с чем." >&2
exit 2
fi
# Пустой DEPLOYED_SHA = хост не ответил / каталога нет / ключ не подошёл.
if [ -z "$DEPLOYED_SHA" ]; then
echo "НЕИЗВЕСТНО: состояние прод-хоста прочитать не удалось."
echo " Это НЕ «прод отстал» — это «мы не знаем, что на проде»."
echo " Смотреть: доступность хоста, DEPLOY_HOST/DEPLOY_KNOWN_HOSTS, каталог /opt/gendesign."
exit 2
fi
# Сравнение по общему префиксу: с хоста приходит короткий SHA, из git — полный.
# Требуем минимум 7 знаков, иначе сравнение бессмысленно.
len_dep=${#DEPLOYED_SHA}
len_exp=${#EXPECTED_SHA}
cmp_len=$(( len_dep < len_exp ? len_dep : len_exp ))
if [ "$cmp_len" -lt 7 ]; then
echo "НЕИЗВЕСТНО: SHA короче семи знаков (прод='$DEPLOYED_SHA', ждём='$EXPECTED_SHA')."
exit 2
fi
if [ "${DEPLOYED_SHA:0:$cmp_len}" = "${EXPECTED_SHA:0:$cmp_len}" ]; then
echo "OK: на проде ${DEPLOYED_SHA} — это и есть текущий main."
exit 0
fi
# Расходятся. Пока коммит свежий, это скорее всего деплой в полёте, а не авария.
if [ -n "$EXPECTED_COMMIT_EPOCH" ]; then
age_s=$(( NOW_EPOCH - EXPECTED_COMMIT_EPOCH ))
grace_s=$(( GRACE_MINUTES * 60 ))
if [ "$age_s" -lt 0 ]; then
age_s=0
fi
if [ "$age_s" -lt "$grace_s" ]; then
echo "ПОКА НОРМА: прод на ${DEPLOYED_SHA}, main на ${EXPECTED_SHA}."
echo " Коммиту ${age_s}с, льготный период ${grace_s}с — вероятно, деплой ещё идёт."
exit 0
fi
echo "РАСХОЖДЕНИЕ: прод на ${DEPLOYED_SHA}, main на ${EXPECTED_SHA} уже ${age_s}с."
else
echo "РАСХОЖДЕНИЕ: прод на ${DEPLOYED_SHA}, main на ${EXPECTED_SHA}."
fi
echo " Код, лежащий в main, на проде не работает. Смотреть последний прогон deploy.yml:"
echo " зелёный CI и смерженный PR сами по себе НЕ означают, что деплой доехал."
exit 1