#!/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