All checks were successful
Deploy Obsidian / deploy-obsidian (push) Successful in 1m52s
208 lines
14 KiB
YAML
208 lines
14 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).
|
||
# АДРЕСАТ (#3062). CouchDB/Obsidian ОСТАЁТСЯ на Beget вместе с Forgejo и
|
||
# GlitchTip, а DEPLOY_HOST после 30.08 будет указывать на Selectel. Раньше этот
|
||
# workflow ходил на DEPLOY_HOST безусловно — то есть в день переезда молча начал
|
||
# бы разворачивать стек CouchDB не на той машине: git reset на /opt/gendesign
|
||
# продуктового хоста, а волт на Beget тем временем перестал бы обновляться.
|
||
# Отказа при этом не было бы — деплой зелёный, адресат другой.
|
||
#
|
||
# Теперь адресат берётся как INFRA_DEPLOY_HOST, а если он не задан — DEPLOY_HOST.
|
||
# До переезда это одна и та же машина, поэтому поведение не меняется; после —
|
||
# workflow сам остаётся на инфраструктурном хосте, без правки этого файла.
|
||
#
|
||
# Отпечаток идёт В ПАРЕ с адресатом и БЕЗ перекрёстного фолбэка: сверять ключ
|
||
# Beget'а с отпечатком Selectel'а — гарантированный отказ. Задан INFRA_DEPLOY_HOST
|
||
# → берётся INFRA_DEPLOY_SSH_FINGERPRINT; не задан → DEPLOY_SSH_FINGERPRINT.
|
||
# Пусто в выбранной ветке → проверка подлинности пропускается, как и раньше.
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
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: Адресат и подлинность хоста (#3062, #3029)
|
||
id: target
|
||
env:
|
||
INFRA_HOST: ${{ secrets.INFRA_DEPLOY_HOST }}
|
||
INFRA_FINGERPRINT: ${{ secrets.INFRA_DEPLOY_SSH_FINGERPRINT }}
|
||
MAIN_FINGERPRINT: ${{ secrets.DEPLOY_SSH_FINGERPRINT }}
|
||
run: |
|
||
set -euo pipefail
|
||
# Отпечаток — публичный хеш ключа хоста, не секрет: его можно
|
||
# передать через output. Приватный ключ так передавать нельзя,
|
||
# поэтому он остаётся прямой ссылкой на секрет в шаге ниже.
|
||
if [ -n "${INFRA_HOST:-}" ]; then
|
||
echo "Адресат: INFRA_DEPLOY_HOST — хосты разъехались, стек CouchDB едет на инфраструктурный хост."
|
||
HOST_FINGERPRINT="${INFRA_FINGERPRINT:-}"
|
||
FINGERPRINT_SOURCE="INFRA_DEPLOY_SSH_FINGERPRINT"
|
||
else
|
||
echo "Адресат: DEPLOY_HOST — INFRA_DEPLOY_HOST не задан, хосты ещё одна машина."
|
||
HOST_FINGERPRINT="${MAIN_FINGERPRINT:-}"
|
||
FINGERPRINT_SOURCE="DEPLOY_SSH_FINGERPRINT"
|
||
fi
|
||
# $GITHUB_OUTPUT — формат «ключ=значение» построчно, поэтому перевод
|
||
# строки внутри значения означает инъекцию произвольного output'а.
|
||
# Отпечаток однострочный по определению (SHA256:...), а вот копипаста
|
||
# в поле секрета лишний \n добавляет легко — шапка этого файла об этом
|
||
# прямо предупреждает. Не вычищаем молча: сверка побайтовая, тихий trim
|
||
# изменил бы результат проверки. Падаем с внятным текстом.
|
||
case "${HOST_FINGERPRINT}" in
|
||
*[![:print:]]*)
|
||
echo "ОШИБКА: ${FINGERPRINT_SOURCE} содержит перевод строки или непечатный символ." >&2
|
||
echo "ОШИБКА: значение должно быть одной строкой вида SHA256:xxxx — перезадай секрет без лишних символов." >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
echo "fingerprint=${HOST_FINGERPRINT}" >> "$GITHUB_OUTPUT"
|
||
if [ -n "${HOST_FINGERPRINT:-}" ]; then
|
||
echo "Подлинность хоста: сверяется по ${FINGERPRINT_SOURCE}."
|
||
else
|
||
echo "::warning title=SSH без проверки подлинности хоста::${FINGERPRINT_SOURCE} не задан — ключ хоста НЕ проверяется (#3029). По каналу едет ssh-ключ и разворачивается стек CouchDB/Obsidian. После разъезда хостов (#3057) соединение идёт через интернет. Как снять отпечаток — см. шапку этого файла."
|
||
echo '###############################################################'
|
||
echo "# ВНИМАНИЕ (#3029): ${FINGERPRINT_SOURCE} не задан."
|
||
echo '# Ключ хоста НЕ проверяется — канал уязвим к MITM.'
|
||
echo '# Как снять отпечаток — см. шапку этого файла.'
|
||
echo '###############################################################'
|
||
fi
|
||
|
||
- name: Deploy obsidian stack via SSH
|
||
uses: appleboy/ssh-action@v1.0.3
|
||
with:
|
||
# #3062: адресат — инфраструктурный хост, если хосты уже разъехались.
|
||
# До этого INFRA_DEPLOY_HOST пуст и всё идёт на DEPLOY_HOST, как раньше.
|
||
host: ${{ secrets.INFRA_DEPLOY_HOST || secrets.DEPLOY_HOST }}
|
||
# user/key/port с фолбэком: у двух хостов они совпадают, а отдельные
|
||
# INFRA_*-секреты может и не завести — тогда работают общие.
|
||
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 || 22 }}
|
||
# #3029: подлинность хоста. Отпечаток выбран шагом выше В ПАРЕ с
|
||
# адресатом — перекрёстного фолбэка здесь быть не должно, иначе после
|
||
# переезда ключ Beget'а сверялся бы с отпечатком Selectel'а.
|
||
# Пусто → easyssh-proxy оставляет ssh.InsecureIgnoreHostKey(), как сегодня.
|
||
fingerprint: ${{ steps.target.outputs.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
|