Files
anti-plagiarism/scripts/deploy.sh
jze9 263a521d63
Some checks failed
Deploy / deploy (push) Has been cancelled
Deploy / test (push) Has been cancelled
fix(ops): перезапускать worker-indexer, когда менялись парсеры
Парсеры не вшиты в образ, а смонтированы в worker-indexer, поэтому деплой
их не пересобирал — и не перезапускал контейнер, раз `services/` не менялся.
Но воркер держит модуль парсера в памяти с прошлого прогона: новый файл
лежит в контейнере, а работает старый код.

Поймано вживую на CORE: заливка после деплоя ушла ноль документов за 208
секунд, и только тело запросов в логах показало, что она всё ещё ходит по
старой схеме — с огромным OR-запросом, который на глубоком смещении трижды
упал по таймауту. Со стороны выглядело как «источник исчерпан», и сторож
чуть не выключил источник насовсем.

Теперь при изменениях в `scripts/parsers/` деплой перезапускает
worker-indexer — если образ и так не пересобирается на этом прогоне.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 18:32:11 +05:00

169 lines
8.6 KiB
Bash
Executable File
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.
#!/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)"