Парсеры не вшиты в образ, а смонтированы в worker-indexer, поэтому деплой их не пересобирал — и не перезапускал контейнер, раз `services/` не менялся. Но воркер держит модуль парсера в памяти с прошлого прогона: новый файл лежит в контейнере, а работает старый код. Поймано вживую на CORE: заливка после деплоя ушла ноль документов за 208 секунд, и только тело запросов в логах показало, что она всё ещё ходит по старой схеме — с огромным OR-запросом, который на глубоком смещении трижды упал по таймауту. Со стороны выглядело как «источник исчерпан», и сторож чуть не выключил источник насовсем. Теперь при изменениях в `scripts/parsers/` деплой перезапускает worker-indexer — если образ и так не пересобирается на этом прогоне. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
169 lines
8.6 KiB
Bash
Executable File
169 lines
8.6 KiB
Bash
Executable File
#!/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)"
|