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, ничего не деплоит.
117 lines
5.5 KiB
YAML
117 lines
5.5 KiB
YAML
# Сторож расхождения «код на проде ↔ 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
|