All checks were successful
Deploy / changes (push) Successful in 8s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy Infra Host / sync-infra-host (push) Successful in 4s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy / deploy (push) Successful in 1m35s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 9s
184 lines
14 KiB
YAML
184 lines
14 KiB
YAML
name: Deploy Infra Host
|
||
|
||
# Синхронизация /opt/gendesign на ХОСТЕ, КОТОРЫЙ ОСТАЁТСЯ (#3059, #3057).
|
||
#
|
||
# ЗАЧЕМ. Сегодня Beget — и прод, и инфраструктура одновременно, поэтому его
|
||
# рабочее дерево обновляет обычный `deploy.yml` (шаг `git reset --hard
|
||
# origin/main` по SSH на `secrets.DEPLOY_HOST`). После переезда 30.08
|
||
# `DEPLOY_HOST` станет указывать на Selectel — и /opt/gendesign на Beget
|
||
# перестанет обновляться СОВСЕМ. Молча.
|
||
#
|
||
# А из этого каталога на Beget продолжат работать:
|
||
# - cron-скрипты бэкапов: ops/backup.sh, ops/backup-forgejo.sh,
|
||
# ops/check-backup-staleness.sh, ops/lib-backup.sh, ops/docker-prune.sh
|
||
# - docker-compose.prod.yml для Forgejo / GlitchTip / CouchDB
|
||
# - Caddyfile + caddy/sites/infra.caddy — единственный публичный вход для
|
||
# git.gendsgn.ru, errors.gendsgn.ru, obsidian.gendsgn.ru (#3062)
|
||
#
|
||
# То есть любая будущая правка этих файлов легла бы в main и никогда не доехала
|
||
# до машины, которая их исполняет. Это ровно класс #2887 («скрипт запускается по
|
||
# cron из /opt/gendesign, куда попадает только через git reset --hard шага
|
||
# деплоя»), но не на уровне одного файла, а на уровне целого хоста.
|
||
#
|
||
# ПОЧЕМУ ОТДЕЛЬНЫЙ WORKFLOW, А НЕ JOB В deploy.yml. Разные адресаты и разные
|
||
# вердикты: «выкатили приложение на Selectel» и «синхронизировали инфраструктуру
|
||
# на Beget» — два независимых факта, и падение второго не должно читаться как
|
||
# неудавшийся деплой продукта. Плюс триггеры разные: инфра-хосту не нужны
|
||
# пересборки backend/frontend.
|
||
#
|
||
# ИНЕРТЕН, ПОКА НЕ ЗАДАН INFRA_DEPLOY_HOST. Сейчас, до переезда, Beget и есть
|
||
# DEPLOY_HOST — второй проход по тому же хосту был бы лишним и мог бы состязаться
|
||
# с основным деплоем за докер-демон (#2950). Поэтому job не делает ничего, пока
|
||
# секрет пуст: включается ОДНОЙ настройкой в момент, когда хосты разъедутся.
|
||
|
||
# ── ПОДЛИННОСТЬ ХОСТА (#3029) ────────────────────────────────────────────────
|
||
# Переезд 30.08 (#3057) уводит цель деплоя на Selectel, а раннеры оставляет на
|
||
# Beget — SSH становится междоузловым, через интернет. Поэтому у вызова
|
||
# appleboy/ssh-action ниже появился вход `fingerprint`.
|
||
# ЧТО ЗАДАТЬ: секрет INFRA_DEPLOY_SSH_FINGERPRINT =
|
||
# ssh-keyscan -t ecdsa -p <порт> <хост> | ssh-keygen -lf - | awk '{print $2}'
|
||
# (значение с префиксом `SHA256:`; именно ecdsa — см. разбор в deploy.yml).
|
||
# ПОБАЙТОВО: значение сравнивается как есть, без trim — лишний пробел/перевод
|
||
# строки при копипасте включает проверку и роняет ssh-шаг с `host key
|
||
# fingerprint mismatch`.
|
||
# ПОКА СЕКРЕТ НЕ ЗАДАН — поведение прежнее: пустой fingerprint у easyssh-proxy
|
||
# v1.5.0 означает ssh.InsecureIgnoreHostKey(), то есть ровно как до этого PR.
|
||
# Включается одной настройкой, как INFRA_DEPLOY_HOST (#3059) и fail-open у
|
||
# TRADEIN_INTERNAL_AUTH_SECRET (#2989).
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
paths:
|
||
# Ровно то, что исполняется НА ОСТАЮЩЕМСЯ хосте. Намеренно НЕ включены
|
||
# backend/** и frontend/** — их образы туда не едут.
|
||
- "ops/*.sh"
|
||
# ops/*.cron — эталоны crontab. Деплой их не исполняет, но после
|
||
# разъезда хостов (#3057) правка crontab-beget.cron иначе доезжала бы
|
||
# только до продового хоста: строка ops/*.cron есть лишь в deploy.yml.
|
||
# Одиночная звёздочка не пересекает `/`, поэтому это именно файлы в
|
||
# корне ops/, как и ops/*.sh рядом.
|
||
- "ops/*.cron"
|
||
- "Caddyfile"
|
||
- "caddy/**"
|
||
- "docker-compose.prod.yml"
|
||
- "docker-compose.obsidian.yml"
|
||
- ".forgejo/workflows/deploy-infra.yml"
|
||
workflow_dispatch:
|
||
|
||
jobs:
|
||
sync-infra-host:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Проверить, разъехались ли хосты
|
||
id: gate
|
||
env:
|
||
INFRA_HOST: ${{ secrets.INFRA_DEPLOY_HOST }}
|
||
run: |
|
||
set -euo pipefail
|
||
if [ -z "${INFRA_HOST:-}" ]; then
|
||
echo "enabled=false" >> "$GITHUB_OUTPUT"
|
||
echo "INFRA_DEPLOY_HOST не задан — хосты ещё не разъехались."
|
||
echo "Инфраструктуру обновляет обычный deploy.yml. Ничего не делаю."
|
||
else
|
||
echo "enabled=true" >> "$GITHUB_OUTPUT"
|
||
echo "INFRA_DEPLOY_HOST задан — синхронизирую остающийся хост."
|
||
fi
|
||
|
||
# #3029: ВИДИМОСТЬ, А НЕ БЛОКИРОВКА. Отсутствие проверки хоста обязано быть
|
||
# громким: easyssh-proxy v1.5.0 при пустом fingerprint молча оставляет
|
||
# ssh.InsecureIgnoreHostKey(), и незащищённый деплой выглядит ровно как
|
||
# защищённый — зелёным. Шаг намеренно НЕ падает: секрета сегодня нет ни у
|
||
# кого, отказ сломал бы деплой в момент мержа этого PR, а правило здесь —
|
||
# «инертно по умолчанию, включается одной настройкой». Заведут секрет —
|
||
# предупреждение исчезнет само.
|
||
- name: Подлинность хоста — статус проверки (#3029)
|
||
if: steps.gate.outputs.enabled == 'true'
|
||
env:
|
||
HOST_FINGERPRINT: ${{ secrets.INFRA_DEPLOY_SSH_FINGERPRINT }}
|
||
run: |
|
||
set -euo pipefail
|
||
if [ -n "${HOST_FINGERPRINT:-}" ]; then
|
||
echo "Подлинность хоста: сверяется по INFRA_DEPLOY_SSH_FINGERPRINT."
|
||
else
|
||
echo '::warning title=SSH без проверки подлинности хоста::INFRA_DEPLOY_SSH_FINGERPRINT не задан — ключ остающегося хоста НЕ проверяется (#3029). Фолбэка на DEPLOY_SSH_FINGERPRINT здесь нет и быть не должно: это другая машина. По каналу едет INFRA_DEPLOY_SSH_KEY и выполняется git reset на /opt/gendesign. Как снять отпечаток — см. шапку этого файла.'
|
||
echo '###############################################################'
|
||
echo '# ВНИМАНИЕ (#3029): INFRA_DEPLOY_SSH_FINGERPRINT не задан.'
|
||
echo '# Ключ хоста НЕ проверяется — канал уязвим к MITM.'
|
||
echo '# Как снять отпечаток — см. шапку этого файла.'
|
||
echo '###############################################################'
|
||
fi
|
||
|
||
- name: Синхронизировать /opt/gendesign на остающемся хосте
|
||
if: steps.gate.outputs.enabled == 'true'
|
||
uses: appleboy/ssh-action@v1.0.3
|
||
with:
|
||
host: ${{ secrets.INFRA_DEPLOY_HOST }}
|
||
username: ${{ secrets.INFRA_DEPLOY_USER || secrets.DEPLOY_USER }}
|
||
key: ${{ secrets.INFRA_DEPLOY_SSH_KEY || secrets.DEPLOY_SSH_KEY }}
|
||
port: ${{ secrets.INFRA_DEPLOY_PORT || secrets.DEPLOY_PORT }}
|
||
# #3029: БЕЗ фолбэка на DEPLOY_SSH_FINGERPRINT — в отличие от user/key/port
|
||
# выше. Те у двух хостов совпадают, а отпечаток — это идентичность
|
||
# КОНКРЕТНОЙ машины: после переезда INFRA_DEPLOY_HOST=Beget, а
|
||
# DEPLOY_HOST=Selectel, и фолбэк означал бы сверку ключа Beget'а с
|
||
# отпечатком Selectel'а — гарантированный отказ ровно у того workflow,
|
||
# который чинит остающийся хост. Пусто → проверка пропускается.
|
||
fingerprint: ${{ secrets.INFRA_DEPLOY_SSH_FINGERPRINT }}
|
||
script: |
|
||
set -euo pipefail
|
||
cd /opt/gendesign
|
||
git fetch origin main
|
||
git reset --hard origin/main
|
||
|
||
# Тот же chmod, что и в deploy.yml: cron зовёт скрипты через `bash`,
|
||
# но restore-drill и ручные запуски рассчитывают на +x.
|
||
chmod +x ops/*.sh 2>/dev/null || true
|
||
|
||
# Caddy здесь несёт ТОЛЬКО остающиеся домены (CADDY_SITES=infra).
|
||
# reload, а не recreate: конфиг примонтирован read-only, контейнер
|
||
# читает тот же файл, который только что обновил git. Если конфиг
|
||
# невалиден — reload откажет ГРОМКО, а старый останется работать,
|
||
# то есть падение здесь не роняет git/errors/obsidian.
|
||
if docker ps --format '{{.Names}}' | grep -q '^gendesign-caddy-1$'; then
|
||
docker compose -p gendesign -f docker-compose.prod.yml exec -T caddy \
|
||
caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
|
||
echo "✓ конфиг прокси перезагружен"
|
||
else
|
||
echo "⚠ контейнер caddy не найден — пропускаю reload"
|
||
fi
|
||
|
||
# infra-postgres (#3061): применить изменения конфигурации сервиса.
|
||
# После разъезда хостов этот workflow — ЕДИНСТВЕННОЕ, что доставляет
|
||
# docker-compose.prod.yml на Beget, а лёгкий кластер БД описан именно
|
||
# там. Без этой строки любой будущий бамп postgres:16-alpine, правка
|
||
# mem_limit или healthcheck'а легли бы в main, workflow отрапортовал
|
||
# бы «дерево синхронизировано», а контейнер продолжил бы жить со
|
||
# старой конфигурацией по `restart: unless-stopped` — молча, ровно
|
||
# класс #2887, ради которого этот workflow и написан.
|
||
#
|
||
# `up -d` с ЯВНЫМ именем сервиса, а не общий: трогается только
|
||
# infra-postgres, до Forgejo / GlitchTip / CouchDB дела нет. Явное
|
||
# имя заодно активирует профиль `infra` само по себе, без оглядки на
|
||
# COMPOSE_PROFILES. Если конфигурация не менялась — compose ничего не
|
||
# пересоздаёт, шаг стоит доли секунды.
|
||
#
|
||
# СОЗДАВАТЬ кластер отсюда мы НЕ хотим — отсюда проверка на
|
||
# существующий контейнер. Первый старт — осознанный ручной шаг окна
|
||
# переезда (шаг 2 в docker-compose.prod.yml), и делается он с уже
|
||
# заполненными INFRA_PG_PASSWORD / FORGEJO_DB_PASS. Стартуй мы вслепую
|
||
# — пустой пароль дал бы отравленный том, который потом не
|
||
# переинициализировать.
|
||
if docker ps -a --format '{{.Names}}' | grep -qx 'gendesign-infra-postgres'; then
|
||
docker compose -p gendesign -f docker-compose.prod.yml up -d infra-postgres
|
||
echo "✓ infra-postgres приведён к конфигурации из main"
|
||
else
|
||
echo "⚠ контейнер gendesign-infra-postgres не найден — кластер ещё не поднят вручную, пропускаю"
|
||
fi
|
||
|
||
# НАМЕРЕННО НЕ ДЕЛАЕТСЯ:
|
||
# - docker image prune: конкурирует с деплоем продукта за leases
|
||
# докер-демона (#2950). Прун на этом хосте остаётся за
|
||
# еженедельным ops/docker-prune.sh.
|
||
# - перезапуск Forgejo / GlitchTip / CouchDB: правка ops-скрипта
|
||
# не повод ронять git. Их обновление — осознанное действие.
|
||
echo "✓ рабочее дерево синхронизировано: $(git rev-parse --short HEAD)"
|