gendesign/.forgejo/workflows/deploy-obsidian.yml
bot-backend 1a37c7e0f9
All checks were successful
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m10s
CI / backend-tests (pull_request) Successful in 17m38s
feat(ci): деплой проверяет, к тому ли хосту подключился
Сегодня подлинность хоста не проверяется нигде: deploy-tradein.yml явно
ставил StrictHostKeyChecking=no, а во всех пяти вызовах appleboy/ssh-action
не задан fingerprint. Пока Forgejo, раннеры и прод жили на одной машине,
это было почти бесплатно. После переезда 30.08 тот же канал идёт через
интернет и уносит GHCR_PAT, OPENAI_API_KEY, OBJECTIVE_API_KEY,
GLITCHTIP_BACKEND_DSN и сам DEPLOY_SSH_KEY.

Вызовов оказалось пять, а не четыре: deploy-obsidian.yml тоже ходит на
DEPLOY_HOST с DEPLOY_SSH_KEY. Отпечаток добавлен во все.

Механизм инертен по конструкции самого действия, а не нашей ветвлёнкой:
appleboy/ssh-action@v1.0.3 -> drone-ssh 1.7.3 -> easyssh-proxy v1.5.0, где
hostKeyCallback = ssh.InsecureIgnoreHostKey() и сверка включается только при
непустом Fingerprint. Незаданный секрет = сегодняшнее поведение бит в бит.

Но инертность, безопасная для мержа, опасна для самого окна: забытый секрет
означал бы зелёный деплой через интернет без всякой проверки. Поэтому перед
каждым ssh-шагом стоит преамбула, которая при пустом секрете печатает
::warning с перечислением того, что именно едет по этому каналу. Warning, а
не отказ: секрета сегодня нет ни у кого, отказ сломал бы деплой в момент
мержа.

Ручной ssh в "Resolve deployed base SHA" отпечаток принять не может, ему
отдан отдельный секрет с known_hosts по той же схеме. Заодно расхождение
ключа перестало быть неотличимым от недоступного хоста: раньше 2>/dev/null
глотал "Host key verification failed" и печаталось обычное сообщение про
фолбэк на полную пересборку. Свойство fail-safe сохранено - при любой ошибке
RAW_SHA пустеет и включается build-all.

ops/selectel-ci-access.sh - аддитивная выдача CI-доступа на Selectel по
решению владельца "сейчас только добавляем, удаляем после переезда": ставит
отдельный ключ робота с from="адрес раннера", добавляет узкое правило ufw,
не трогая общее, и печатает план сужения, который сам не выполняет. Ни одной
команды, способной отрезать текущую сессию: sshd не перезапускается, default
policy ufw не трогается, в authorized_keys только дозапись.

Отзыв старого ключа, сужение правил и уборка секретов - в #3075, после
подтверждённого переезда.

Refs #3029, #3075, #3057, #2989
2026-08-24 19:16:56 +03:00

165 lines
10 KiB
YAML
Raw 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.

name: Deploy Obsidian
# Деплой ТОЛЬКО obsidian-стека (CouchDB).
# Триггерится при изменениях:
# - docker-compose.obsidian.yml (compose сервиса CouchDB)
# - scripts/setup-couchdb.sh (bootstrap)
# - docs/obsidian-livesync.md (документация — для history-watcher'а)
# - этот workflow
#
# Не пересобирает никаких Docker-образов (CouchDB официальный с DockerHub).
# Не трогает main-стек (backend / frontend / postgres / worker / beat / caddy).
#
# ИСТОРИЯ (2026-07-05): жил в .github/workflows/ с момента миграции с GitHub
# (16.05.2026), помечен в README как «остался на GitHub» — но живого зеркала
# на github.com с настроенными секретами не оказалось: 0 запусков за всю
# историю Forgejo Actions (12000+ прогонов остальных workflow), контейнер
# не пересоздавался с 17.05 до ручного SSH-фикса 04.07. Перенесён сюда —
# единственная директория, которую реально исполняет этот инстанс.
# См. issue #2416.
# ── ПОДЛИННОСТЬ ХОСТА (#3029) ────────────────────────────────────────────────
# Переезд 30.08 (#3057) уводит цель деплоя на Selectel, а раннеры оставляет на
# Beget — SSH становится междоузловым, через интернет. Поэтому у вызова
# appleboy/ssh-action ниже появился вход `fingerprint`.
# ЧТО ЗАДАТЬ: секрет 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).
# NB: этот workflow ходит на secrets.DEPLOY_HOST, поэтому и отпечаток берётся
# DEPLOY_SSH_FINGERPRINT — пара «хост+отпечаток» обязана меняться вместе. Но
# CouchDB по плану ОСТАЁТСЯ на Beget вместе с Forgejo/GlitchTip (#3062), а
# DEPLOY_HOST после 30.08 будет указывать на Selectel. Значит у этого файла есть
# отдельная, ещё не закрытая проблема адресата — тот же класс, что #3059. Здесь
# намеренно НЕ трогаем: правка про подлинность хоста, а не про маршрутизацию.
# При переводе на INFRA_DEPLOY_HOST не забыть переключить и отпечаток.
# ─────────────────────────────────────────────────────────────────────────────
on:
push:
branches: [main]
paths:
- "docker-compose.obsidian.yml"
- "scripts/setup-couchdb.sh"
- "docs/obsidian-livesync.md"
- ".forgejo/workflows/deploy-obsidian.yml"
workflow_dispatch:
concurrency:
group: deploy-obsidian
cancel-in-progress: false
jobs:
deploy-obsidian:
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
# #3029: ВИДИМОСТЬ, А НЕ БЛОКИРОВКА. Отсутствие проверки хоста обязано быть
# громким: easyssh-proxy v1.5.0 при пустом fingerprint молча оставляет
# ssh.InsecureIgnoreHostKey(), и незащищённый деплой выглядит ровно как
# защищённый — зелёным. Шаг намеренно НЕ падает: секрета сегодня нет ни у
# кого, отказ сломал бы деплой в момент мержа этого PR, а правило здесь —
# «инертно по умолчанию, включается одной настройкой». Заведут секрет —
# предупреждение исчезнет само.
- name: Подлинность хоста — статус проверки (#3029)
env:
HOST_FINGERPRINT: ${{ secrets.DEPLOY_SSH_FINGERPRINT }}
run: |
set -euo pipefail
if [ -n "${HOST_FINGERPRINT:-}" ]; then
echo "Подлинность хоста: сверяется по DEPLOY_SSH_FINGERPRINT."
else
echo '::warning title=SSH без проверки подлинности хоста::DEPLOY_SSH_FINGERPRINT не задан — ключ хоста НЕ проверяется (#3029). По каналу едет DEPLOY_SSH_KEY и разворачивается стек CouchDB/Obsidian. После переезда на Selectel (#3057) соединение идёт через интернет. Как снять отпечаток — см. шапку этого файла.'
echo '###############################################################'
echo '# ВНИМАНИЕ (#3029): DEPLOY_SSH_FINGERPRINT не задан.'
echo '# Ключ хоста НЕ проверяется — канал уязвим к MITM.'
echo '# Как снять отпечаток — см. шапку этого файла.'
echo '###############################################################'
fi
- name: Deploy obsidian stack via SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
port: ${{ secrets.DEPLOY_PORT || 22 }}
# #3029: подлинность хоста. Секрет НЕ задан → пустая строка → easyssh-proxy
# оставляет ssh.InsecureIgnoreHostKey(), то есть сегодняшнее поведение.
fingerprint: ${{ secrets.DEPLOY_SSH_FINGERPRINT }}
script: |
set -euo pipefail
cd /opt/gendesign
# Свежие конфиги из репо
git fetch origin main
git reset --hard origin/main
# Создать shared network если её ещё нет (idempotent).
docker network inspect gendesign_shared >/dev/null 2>&1 \
|| docker network create gendesign_shared
# Загрузить COUCHDB_USER/PASSWORD из .env.runtime до compose up
# (compose также читает .env.runtime через env_file, но
# `${COUCHDB_PASSWORD:?...}` валидация требует переменную в shell).
if [ -f backend/.env.runtime ]; then
set -a
# shellcheck source=/dev/null
source backend/.env.runtime
set +a
fi
if [ -z "${COUCHDB_PASSWORD:-}" ]; then
echo "ERROR: COUCHDB_PASSWORD не задан в backend/.env.runtime"
exit 1
fi
# Стек CouchDB поднимается с собственным project-name.
docker compose -p gendesign-obsidian \
-f docker-compose.obsidian.yml pull
docker compose -p gendesign-obsidian \
-f docker-compose.obsidian.yml up -d
# Bootstrap CouchDB (CORS, db, лимиты) — idempotent
if [ -f scripts/setup-couchdb.sh ]; then
# Загружаем COUCHDB_PASSWORD из backend/.env.runtime если есть
if [ -f backend/.env.runtime ]; then
set -a; source backend/.env.runtime; set +a
fi
# Ждём CouchDB up, потом bootstrap
for i in $(seq 1 30); do
if docker compose -p gendesign-obsidian \
-f docker-compose.obsidian.yml \
exec -T couchdb curl -fsS http://localhost:5984/_up >/dev/null 2>&1; then
break
fi
sleep 2
done
COUCHDB_HOST=http://localhost:5984 \
COUCHDB_USER="${COUCHDB_USER:-obsidian}" \
COUCHDB_PASSWORD="${COUCHDB_PASSWORD:?must be set in backend/.env.runtime}" \
docker compose -p gendesign-obsidian \
-f docker-compose.obsidian.yml \
exec -T couchdb bash -c "$(cat scripts/setup-couchdb.sh)" \
|| echo "(bootstrap warnings ignored — script is idempotent)"
fi
# Caddy в main-stack — НЕ перезапускаем тут (другой workflow),
# но reload конфига полезен на случай если в Caddyfile добавили
# новый obsidian-route только что (главное: image main caddy уже
# запущен и подключён к gendesign_shared network).
if docker compose -p gendesign -f docker-compose.prod.yml ps caddy --quiet \
| grep -q .; then
docker compose -p gendesign -f docker-compose.prod.yml \
exec -T caddy caddy reload --config /etc/caddy/Caddyfile \
|| echo "(caddy reload skipped — main stack not running)"
fi
sleep 3
curl -fsS https://obsidian.gendsgn.ru/_up | head -c 200 || true