gendesign/.forgejo/workflows/deploy-drift.yml
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

117 lines
5.5 KiB
YAML
Raw Permalink 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.

# Сторож расхождения «код на проде ↔ main» (#3029).
#
# ЗАЧЕМ ОТДЕЛЬНЫЙ ПРОГОН, А НЕ ШАГ В deploy.yml. Шаг внутри деплоя проверяет
# только тот деплой, который запустился. 27.08.2026 прод сутки жил на старом
# коммите ровно потому, что деплой НЕ доезжал: замена IP сервера (#3110)
# осиротила секрет DEPLOY_HOST, deploy.yml падал на i/o timeout, а CI оставался
# зелёным и PR продолжали мержиться. Проверять надо не «прошёл ли прогон», а
# «совпадает ли то, что лежит на проде, с тем, что в main» — это независимый
# вопрос, и задавать его надо по часам, а не по событию деплоя.
#
# Read-only: один SSH и `git rev-parse`. Ничего не деплоит и не меняет.
name: deploy-drift
on:
workflow_dispatch: {}
schedule:
# Ежечасно в :23 — вне ровного часа, чтобы не толкаться со сторожами
# бэкапов (те ходят в :00).
- cron: '23 * * * *'
# Правка самого сторожа проверяется сразу, а не через час.
push:
branches: [main]
paths:
- 'scripts/check-deploy-drift.sh'
- '.forgejo/workflows/deploy-drift.yml'
concurrency:
group: deploy-drift
cancel-in-progress: false
jobs:
drift:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Что ждём увидеть на проде
id: expected
run: |
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
echo "epoch=$(git log -1 --format=%ct)" >> "$GITHUB_OUTPUT"
echo "На main: $(git rev-parse --short HEAD) $(git log -1 --format=%s | cut -c1-60)"
- name: Что лежит на проде
id: deployed
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
# known_hosts, а не SHA256-отпечаток: здесь обычный openssh-клиент.
# Секрет привязан к АДРЕСУ — после смены IP его надо переснять
# (`ssh-keyscan -p <порт> <хост>`), иначе шаг молча уедет в «неизвестно».
DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
run: |
SSH_KEY_FILE=$(mktemp)
echo "$DEPLOY_SSH_KEY" > "$SSH_KEY_FILE"
chmod 600 "$SSH_KEY_FILE"
KNOWN_HOSTS_FILE=$(mktemp)
if [ -n "${DEPLOY_KNOWN_HOSTS:-}" ]; then
printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > "$KNOWN_HOSTS_FILE"
chmod 600 "$KNOWN_HOSTS_FILE"
SSH_HOST_OPTS=(-o StrictHostKeyChecking=yes -o "UserKnownHostsFile=$KNOWN_HOSTS_FILE")
else
SSH_HOST_OPTS=(-o StrictHostKeyChecking=no)
echo "::warning title=SSH без проверки подлинности хоста::DEPLOY_KNOWN_HOSTS не задан (#3029)."
fi
# `safe.directory` — каталог принадлежит деплой-пользователю, а git с
# 2.35 отказывается работать в чужом репозитории. Без этого шаг вернул
# бы пусто, и сторож сказал бы «неизвестно» вместо правды.
RAW=$(ssh -i "$SSH_KEY_FILE" \
"${SSH_HOST_OPTS[@]}" \
-o ConnectTimeout=10 \
-o BatchMode=yes \
-p "${DEPLOY_PORT:-22}" \
"${DEPLOY_USER}@${DEPLOY_HOST}" \
"git -c safe.directory=/opt/gendesign -C /opt/gendesign rev-parse HEAD 2>/dev/null || true" \
2>/dev/null | tr -d '[:space:]' || true)
rm -f "$SSH_KEY_FILE" "$KNOWN_HOSTS_FILE"
echo "sha=${RAW}" >> "$GITHUB_OUTPUT"
if [ -n "$RAW" ]; then
echo "На проде: ${RAW:0:12}"
else
echo "На проде: прочитать не удалось"
fi
- name: Вердикт
env:
DEPLOYED_SHA: ${{ steps.deployed.outputs.sha }}
EXPECTED_SHA: ${{ steps.expected.outputs.sha }}
EXPECTED_COMMIT_EPOCH: ${{ steps.expected.outputs.epoch }}
# Деплой продукта идёт около десяти минут; тридцать — запас, чтобы
# ежечасный сторож не кричал на деплой, который просто ещё в полёте.
GRACE_MINUTES: '30'
run: |
chmod +x scripts/check-deploy-drift.sh
set +e
./scripts/check-deploy-drift.sh
rc=$?
set -e
case "$rc" in
0) exit 0 ;;
2)
echo "::warning title=Состояние прода неизвестно::Сторож не смог прочитать SHA с прод-хоста (#3029)."
exit 1
;;
*)
echo "::error title=Прод отстал от main::Код из main на проде не работает (#3029)."
exit 1
;;
esac