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
Сегодня подлинность хоста не проверяется нигде: 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
165 lines
10 KiB
YAML
165 lines
10 KiB
YAML
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
|