#!/usr/bin/env bash # ───────────────────────────────────────────────────────────────────────────── # Деплой anti-plagiarism (умная пересборка — только изменённых сервисов). # # Запускается Gitea Actions runner'ом в host-режиме прямо на app-хосте 1.32 # (см. .gitea/workflows/deploy.yml). Ожидает: # - cwd = ПОЛНЫЙ чекаут репозитория (нужен git-лог для diff); # - переменную PVE_PASSWORD (секрет) для доступа к Proxmox-хосту → CT 102; # - INFISICAL_CLIENT_ID / INFISICAL_CLIENT_SECRET (Machine Identity # "claude-ai", Universal Auth) — .env на проде теперь генерируется из # Infisical (prod) на каждом деплое, а не правится руками на сервере. # # Пересобирается только то, чей код изменился с прошлого деплоя (маркер SHA в # $APP/.last_deploy_sha). Это критично: worker-gpu тянет torch/faiss (~2 ГБ), # его нельзя пересобирать без нужды. Если менялся docker-compose.prod.yml — # пересобираются все бэкенд-сервисы. # ───────────────────────────────────────────────────────────────────────────── set -euo pipefail APP=/home/user/anti-plagiarism SRC="$(pwd)" PVE_HOST=192.168.20.254 COMPOSE="docker compose -f docker-compose.prod.yml" ALL_BACKEND="api worker-gpu worker-indexer worker-notifier worker-gost" MARKER="$APP/.last_deploy_sha" CUR_SHA="$(git -C "$SRC" rev-parse HEAD)" # ── Определить, что менялось ────────────────────────────────────────────────── FULL=0 CHANGED="" if [ -f "$MARKER" ] && PREV="$(cat "$MARKER")" && [ -n "$PREV" ] \ && git -C "$SRC" cat-file -e "$PREV" 2>/dev/null; then CHANGED="$(git -C "$SRC" diff --name-only "$PREV" "$CUR_SHA")" echo "Изменения с $PREV:" echo "$CHANGED" | sed 's/^/ /' # Правка общего compose затрагивает все образы echo "$CHANGED" | grep -qE '^docker-compose\.prod\.yml$' && FULL=1 else echo "Маркер прошлого деплоя недоступен — полная пересборка (разовая)" FULL=1 fi REBUILD="" FRONTEND_CHANGED=0 PARSERS_CHANGED=0 if [ "$FULL" = 1 ]; then REBUILD="$ALL_BACKEND" FRONTEND_CHANGED=1 else for svc in $ALL_BACKEND; do echo "$CHANGED" | grep -qE "^services/$svc/" && REBUILD="$REBUILD $svc" done echo "$CHANGED" | grep -qE '^services/frontend/' && FRONTEND_CHANGED=1 # Парсеры не вшиты в образ, а смонтированы в worker-indexer (см. compose), # поэтому пересобирать нечего. Но процесс воркера держит модуль парсера в # памяти с прошлого прогона: без перезапуска правка молча не применяется — # заливка продолжает ходить по старому коду, и это не видно ниоткуда, кроме # тела запросов в логах. Ловилось на CORE 16.09.2026. echo "$CHANGED" | grep -qE '^scripts/parsers/' && PARSERS_CHANGED=1 fi REBUILD="$(echo "$REBUILD" | xargs || true)" echo "==> [1/6] Секреты из Infisical (prod) → .env" INFISICAL_BIN="$APP/.infisical-cli" if [ ! -x "$INFISICAL_BIN" ]; then echo " ставлю infisical CLI (разово)" curl -sSL -o /tmp/infisical-cli.tar.gz \ "https://github.com/Infisical/infisical/releases/download/infisical-cli%2Fv0.31.1/infisical_0.31.1_linux_amd64.tar.gz" tar xzf /tmp/infisical-cli.tar.gz -C /tmp infisical mv /tmp/infisical "$INFISICAL_BIN" rm -f /tmp/infisical-cli.tar.gz fi INFISICAL_DOMAIN=https://infisical.jze9.ru/api INFISICAL_PROJECT_ID=a2e7505e-573f-43e9-872b-4ed7a6bed7b8 INFISICAL_TOKEN="$("$INFISICAL_BIN" login --method=universal-auth \ --client-id="${INFISICAL_CLIENT_ID:?INFISICAL_CLIENT_ID не задан}" \ --client-secret="${INFISICAL_CLIENT_SECRET:?INFISICAL_CLIENT_SECRET не задан}" \ --domain="$INFISICAL_DOMAIN" --plain --silent)" TMP_ENV="$(mktemp)" "$INFISICAL_BIN" export --token="$INFISICAL_TOKEN" --domain="$INFISICAL_DOMAIN" \ --projectId="$INFISICAL_PROJECT_ID" --env=prod --format=dotenv --expand=false \ --silent > "$TMP_ENV" # Не затирать рабочий .env, если Infisical вернул мало строк (недоступен/пуст) — # иначе следующий шаг раскатит прод с пустым конфигом. ENV_LINES="$(grep -cE '^[A-Za-z_][A-Za-z0-9_]*=' "$TMP_ENV" || true)" if [ "$ENV_LINES" -lt 20 ]; then echo "ОШИБКА: Infisical export вернул только $ENV_LINES строк (ожидалось ~38) — сервис недоступен или пуст. Деплой прерван, .env на проде не тронут." rm -f "$TMP_ENV" exit 1 fi mv "$TMP_ENV" "$APP/.env" echo " OK: $ENV_LINES секретов" echo "==> [2/6] Синхронизация кода в $APP (сохраняя .env и compose)" rsync -a \ --exclude='__pycache__/' --exclude='*.pyc' --exclude='.pytest_cache/' \ --exclude='node_modules/' --exclude='dist/' \ "$SRC/services" "$SRC/scripts" "$APP/" cd "$APP" if [ -n "$REBUILD" ]; then echo "==> [3/6] Пересборка и перезапуск: $REBUILD" # shellcheck disable=SC2086 $COMPOSE up -d --build $REBUILD else echo "==> [3/6] Бэкенд-сервисы не менялись — пропуск сборки" fi # Секреты в .env могли поменяться без изменения кода (правка в Infisical) — # docker compose сам не пересоздаст контейнер, чей env_file изменился, если # просто повторно не позвать up -d. Дёшево и идемпотентно: compose пересоздаёт # только контейнеры с реально изменившимся конфигом, остальные не трогает. echo "==> [4/6] Применение секретов к неизменившимся сервисам (если менялись)" # shellcheck disable=SC2086 $COMPOSE up -d $ALL_BACKEND # Правка парсера доезжает монтированием, но модуль уже загружен в память # воркера — перезапускаем, если образ и так не пересобирался выше. if [ "$PARSERS_CHANGED" = 1 ] && ! echo " $REBUILD " | grep -q " worker-indexer "; then echo "==> [4.5/6] Парсеры менялись — перезапуск worker-indexer" $COMPOSE restart worker-indexer fi echo "==> [5/6] Миграции БД (с ретраем)" for i in $(seq 1 10); do $COMPOSE exec -T api alembic upgrade head && break echo " api ещё не готов, повтор $i/10..."; sleep 5 done if [ "$FRONTEND_CHANGED" = 1 ]; then echo "==> [6/6] Сборка фронтенда" docker run --rm -v "$APP/services/frontend":/app -w /app node:20-slim \ sh -c "npm install --no-audit --no-fund --loglevel=error && npm run build" echo "==> [6/6] Выкладка статики на CT 102 (через $PVE_HOST)" cd "$APP/services/frontend/dist" tar czf /tmp/academic-dist.tgz . export SSHPASS="${PVE_PASSWORD:?PVE_PASSWORD не задан}" SSHOPT="-o StrictHostKeyChecking=no -o ConnectTimeout=10" # shellcheck disable=SC2086 sshpass -e scp $SSHOPT /tmp/academic-dist.tgz "root@${PVE_HOST}:/tmp/academic-dist.tgz" # shellcheck disable=SC2086 sshpass -e ssh $SSHOPT "root@${PVE_HOST}" 'bash -s' <<'REMOTE' set -e pct push 102 /tmp/academic-dist.tgz /tmp/academic-dist.tgz pct exec 102 -- bash -c ' set -e cd /var/www cp -a academic "academic.bak.$(date +%s)" cd academic rm -rf assets index.html tar xzf /tmp/academic-dist.tgz chown -R 1000:1000 . nginx -t && nginx -s reload ' rm -f /tmp/academic-dist.tgz REMOTE rm -f /tmp/academic-dist.tgz else echo "==> [6/6] Фронтенд не менялся — пропуск сборки и выкладки" fi echo "$CUR_SHA" > "$MARKER" echo "==> Деплой завершён успешно (маркер обновлён: $CUR_SHA)"