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
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, ничего не деплоит.
87 lines
5 KiB
Bash
87 lines
5 KiB
Bash
#!/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
|