diff --git a/.forgejo/workflows/deploy-tradein.yml b/.forgejo/workflows/deploy-tradein.yml index 894dcb02..102dfde0 100644 --- a/.forgejo/workflows/deploy-tradein.yml +++ b/.forgejo/workflows/deploy-tradein.yml @@ -294,6 +294,9 @@ jobs: uses: appleboy/ssh-action@v1.0.3 env: IMAGE_TAG: latest + # Нужен на VPS, чтобы спросить у демона ID подтянутого образа и не + # уходить в drain, когда пересоздавать нечего (см. ниже, #2679). + IMAGE_BACKEND: ${{ env.IMAGE_BACKEND }} GHCR_PAT: ${{ secrets.GHCR_PAT }} # #2679: backend / scraper / tgbot — ОДИН И ТОТ ЖЕ образ # gendesign-tradein-backend (см. docker-compose.prod.yml: три сервиса, @@ -316,7 +319,7 @@ jobs: username: ${{ secrets.DEPLOY_USER }} key: ${{ secrets.DEPLOY_SSH_KEY }} port: ${{ secrets.DEPLOY_PORT }} - envs: IMAGE_TAG,GHCR_PAT,SCRAPER_RECREATE,GITHUB_SHA + envs: IMAGE_TAG,IMAGE_BACKEND,GHCR_PAT,SCRAPER_RECREATE,GITHUB_SHA script: | set -euo pipefail cd /opt/gendesign @@ -497,8 +500,42 @@ jobs: # с browser/backend/frontend, отдельного graceful-drain не требует. SERVICES="browser backend frontend tgbot" SCRAPER_STOP_TS="" + scraper_stale="" if [ "${SCRAPER_RECREATE:-true}" = "true" ]; then - echo "→ backend-образ мог пересобраться — scraper пересоздаётся вместе с backend (#2679);" + # Пересоздавать нечего — и ждать нечего (#2679). SCRAPER_RECREATE + # истинно и на infra-правках (compose / workflow / deploy/**), а те + # почти всегда собирают ТОТ ЖЕ образ по кэшу: digest не меняется, + # `up -d` выходит no-op — и платить за него пятиминутным drain'ом, + # прерывая многочасовой сбор, не за что. Сравниваем, на том ли + # образе бежит scraper, что уже лежит в локальном демоне. + # ПОРЯДОК ВАЖЕН: только ПОСЛЕ `docker compose pull` (шаг выше) — + # до pull'а под тегом :latest ещё старый образ, сравнение всегда + # «совпало» и drain пропускался бы как раз тогда, когда он нужен. + # Заодно чинит ложный startup-reap: чекпоинт/reap ниже завязаны на + # ЭТОТ же признак и больше не выполняются, когда recreate'а не было + # (иначе живой прогон с heartbeat старше чекпоинта помечался бы + # 'cancelled', продолжая работать). + pulled_image=$(docker image inspect -f '{{.Id}}' "$IMAGE_BACKEND:$IMAGE_TAG" 2>/dev/null || echo "") + running_image=$(docker inspect -f '{{.Image}}' tradein-scraper 2>/dev/null || echo "") + if [ -n "$pulled_image" ] && [ "$pulled_image" = "$running_image" ]; then + echo "→ образ scraper'а не изменился ($pulled_image) — пересоздавать нечего," + echo " drain пропускаем, in-flight прогоны не трогаем" + else + scraper_stale="yes" + fi + # scraper в $SERVICES в обоих случаях: при совпавшем образе `up -d` + # — no-op, но правка самого compose (env/лимиты сервиса) так всё же + # доезжает. Ceiling: такой config-only recreate идёт БЕЗ drain'а — + # страхуют SIGTERM-drain (#1182) + stop_grace_period 120s, а строку + # прогона подчистит периодический 6h zombie-reaper. + SERVICES="$SERVICES scraper" + else + echo "→ backend-образ в этом деплое не пересобирался — tradein-scraper не трогаем" + echo " (сверка образов ниже всё равно проверит, что он не отстал)" + fi + + if [ -n "$scraper_stale" ]; then + echo "→ новый backend-образ — scraper пересоздаётся вместе с backend (#2679);" echo " ждём слива in-flight scrape_runs (до 5 мин)" drained="" for i in $(seq 1 30); do @@ -536,16 +573,11 @@ jobs: SCRAPER_STOP_TS="$(docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \ psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -tAc "SELECT NOW();" 2>/dev/null | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" || SCRAPER_STOP_TS="" echo "→ scraper checkpoint ts (DB clock): ${SCRAPER_STOP_TS:-unknown}" - - SERVICES="$SERVICES scraper" - else - echo "→ backend-образ в этом деплое не пересобирался — tradein-scraper не трогаем" - echo " (сверка образов ниже всё равно проверит, что он не отстал)" fi docker compose -p gendesign-tradein -f docker-compose.prod.yml up -d --no-deps $SERVICES - if [ "${SCRAPER_RECREATE:-true}" = "true" ] && [ -n "${SCRAPER_STOP_TS:-}" ]; then + if [ -n "$scraper_stale" ] && [ -n "${SCRAPER_STOP_TS:-}" ]; then echo "→ Startup-reap (#1951): помечаем orphaned running-строки, замороженные recreate'ом" # NB: psql `-c` НЕ поддерживает `:'var'`-подстановку (переменная доходит до # сервера как литерал → syntax error, см. комментарий выше про TRADEIN_READER_PASSWORD) @@ -707,20 +739,31 @@ jobs: # узнать об этом надо в момент деплоя, а не через месяц. exit 1 идёт # ДО записи .tradein-deployed-sha → следующий прогон возьмёт ту же # базу и пересоберёт всё накопленное (тот же приём, что в health-check). - backend_image=$(docker inspect -f '{{.Image}}' tradein-backend 2>/dev/null || echo "missing") + # «Контейнера нет» и «контейнер отстал» — разные аварии и чинятся + # по-разному, поэтому сообщения различаются явно. + backend_image=$(docker inspect -f '{{.Image}}' tradein-backend 2>/dev/null || echo "") image_mismatch="" + if [ -z "$backend_image" ]; then + echo "ERROR: контейнера tradein-backend нет — сверять образы не с чем." + image_mismatch="yes" + fi for svc in scraper tgbot; do - svc_image=$(docker inspect -f '{{.Image}}' "tradein-$svc" 2>/dev/null || echo "missing") - if [ "$svc_image" != "$backend_image" ]; then - echo "ERROR: tradein-$svc на образе $svc_image, tradein-backend — на $backend_image" + svc_image=$(docker inspect -f '{{.Image}}' "tradein-$svc" 2>/dev/null || echo "") + if [ -z "$svc_image" ]; then + echo "ERROR: контейнера tradein-$svc НЕТ (удалён или не создавался) — это не отставший" + echo " образ, а неполный стек: сервис не работает вообще." + image_mismatch="yes" + elif [ -n "$backend_image" ] && [ "$svc_image" != "$backend_image" ]; then + echo "ERROR: tradein-$svc ОТСТАЛ: работает на $svc_image, tradein-backend — на $backend_image" image_mismatch="yes" fi done if [ -n "$image_mismatch" ]; then - echo "ERROR: образы backend-семейства разошлись — деплой FAILED (#2679)." - echo " Лечение вручную: docker compose -p gendesign-tradein \\" - echo " -f /opt/gendesign/tradein-mvp/docker-compose.prod.yml \\" - echo " up -d --force-recreate --no-deps scraper tgbot" + echo "ERROR: backend-семейство не на одном образе — деплой FAILED (#2679)." + echo " Лечение вручную (поднимет отсутствующие, пересоздаст отставшие):" + echo " docker compose -p gendesign-tradein \\" + echo " -f /opt/gendesign/tradein-mvp/docker-compose.prod.yml \\" + echo " up -d --force-recreate --no-deps backend scraper tgbot" exit 1 fi echo "→ образы совпадают: backend/scraper/tgbot на $backend_image."