diff --git a/.forgejo/workflows/deploy-metrics.yml b/.forgejo/workflows/deploy-metrics.yml index 86a702b9..80a648ae 100644 --- a/.forgejo/workflows/deploy-metrics.yml +++ b/.forgejo/workflows/deploy-metrics.yml @@ -91,8 +91,12 @@ jobs: METRICS_TELEGRAM_INFRA_TOPIC_ID: ${{ secrets.METRICS_TELEGRAM_INFRA_TOPIC_ID }} METRICS_TELEGRAM_ONCALL: ${{ secrets.METRICS_TELEGRAM_ONCALL }} ALERT_ACK_GLITCHTIP_SECRET: ${{ secrets.ALERT_ACK_GLITCHTIP_SECRET }} + # #3471: секрет ретранслятора Telegram Bot API (tg-relay). Пусто — + # профиль relay не включаем (см. PROFILES ниже), а не падаем в + # рестарт-луп: контейнер сам делает SystemExit на пустом секрете. + TG_RELAY_SECRET: ${{ secrets.TG_RELAY_SECRET }} with: - envs: METRICS_TELEGRAM_BOT_TOKEN,METRICS_TELEGRAM_CHAT_ID,METRICS_TELEGRAM_TOPIC_ID,METRICS_TELEGRAM_INFRA_TOPIC_ID,METRICS_TELEGRAM_ONCALL,ALERT_ACK_GLITCHTIP_SECRET + envs: METRICS_TELEGRAM_BOT_TOKEN,METRICS_TELEGRAM_CHAT_ID,METRICS_TELEGRAM_TOPIC_ID,METRICS_TELEGRAM_INFRA_TOPIC_ID,METRICS_TELEGRAM_ONCALL,ALERT_ACK_GLITCHTIP_SECRET,TG_RELAY_SECRET host: ${{ secrets.INFRA_DEPLOY_HOST || secrets.DEPLOY_HOST }} username: ${{ secrets.INFRA_DEPLOY_USER || secrets.DEPLOY_USER }} key: ${{ secrets.INFRA_DEPLOY_SSH_KEY || secrets.DEPLOY_SSH_KEY }} @@ -259,6 +263,19 @@ jobs: echo "::warning title=Алерты выключены::METRICS_TELEGRAM_BOT_TOKEN/CHAT_ID не заданы. Метрики и логи собираются, но при срабатывании правила НИКТО не будет уведомлён. Канал доставки — открытый вопрос #3078." fi + # Ретранслятор Telegram Bot API (#3471, PR #3487 сломал прод: сервис + # без profiles уходил в SystemExit на пустом секрете и висел в + # Restarting). Профиль relay включаем НЕЗАВИСИМО от alerts — это + # разные каналы (один шлёт алерты боту, другой ретранслирует + # продуктовый Bot API трафик с Selectel). PROFILES — список через + # запятую, как того требует COMPOSE_PROFILES. + if [ -n "${TG_RELAY_SECRET:-}" ]; then + PROFILES="${PROFILES:+$PROFILES,}relay" + echo "Ретранслятор Telegram: секрет задан, профиль relay включён." + else + echo "::warning title=Резервный ретранслятор Telegram выключен::TG_RELAY_SECRET пуст — tg-relay не поднимается (профиль relay выключен). Продуктовый Telegram-трафик пойдёт напрямую с Selectel, где теряется примерно каждый четвёртый короткий запрос." + fi + # ── Цели file_sd для Prometheus (#3155) ──────────────────────── # Включатель профиля и цель для Prometheus обязаны стоять в ОДНОМ # условии. Пока они жили порознь, вышло так: 27.08 профиль alerts @@ -277,19 +294,27 @@ jobs: AM_TARGETS_FILE=ops/metrics/prometheus/alertmanager_targets.gen.yml : > "$AM_TARGETS_FILE" echo "# Файл рендерится деплоем (deploy-metrics.yml), правки руками затрутся." >> "$AM_TARGETS_FILE" - if [ "$PROFILES" = "alerts" ]; then + # Сравнение через case, а не "=": PROFILES теперь может быть + # комбинацией через запятую ("alerts,relay") с тех пор, как #3471 + # завёл независимый профиль relay — точное равенство строке + # "alerts" сломалось бы молча в тот момент, когда оба профиля + # включены разом. + case ",$PROFILES," in + *,alerts,*) echo '- targets: ["alertmanager:9093"]' >> "$AM_TARGETS_FILE" echo " labels:" >> "$AM_TARGETS_FILE" echo " host: infra" >> "$AM_TARGETS_FILE" echo "Prometheus: приёмник alertmanager:9093 прописан в целях." - else + ;; + *) # Пустой список, а НЕ отсутствующий файл: одиночный бинд-маунт # несуществующего пути docker подменяет каталогом, и Prometheus # не стартует вовсе. echo "# Профиль alerts выключен — приёмников нет." >> "$AM_TARGETS_FILE" echo "[]" >> "$AM_TARGETS_FILE" echo "Prometheus: профиль alerts выключен — целей нет, это штатно." - fi + ;; + esac # ── read-only роль для датасорса GlitchTip ───────────────────── # Идемпотентно. Прав на запись не выдаём вовсе: датасорс Grafana @@ -301,6 +326,43 @@ jobs: COMPOSE_PROFILES="$PROFILES" \ docker compose -p gendesign-metrics -f docker-compose.metrics.yml up -d --remove-orphans + # ── alert-ack / tg-relay: код монтируется с хоста ──────────────── + # Тот же класс бага, что у Alertmanager (см. ниже) и Caddyfile: + # `up -d` сравнивает ОПИСАНИЕ сервиса, а не содержимое бинд-маунта. + # alert-ack и tg-relay получают код именно бинд-маунтом файла + # (./ops/metrics/{alert-ack,tg-relay}/app.py:/app/app.py:ro), а не + # сборкой образа — правка app.py оставляет уже запущенный + # контейнер работать на СТАРОМ коде в памяти интерпретатора сколько + # угодно, и `up -d` этого не видит вовсе. + # + # Пойман на проде 12.09.2026: PR #3490 (фикс alert-ack) слился, + # `git reset --hard` обновил файл на диске (grep по новому + # комментарию находил его), а gendesign-alert-ack, запущенный за + # 25 минут до этого, продолжал отвечать по старой логике — + # зелёный деплой, тихо неверное поведение. Починил только ручной + # `docker restart gendesign-alert-ack`. force-recreate здесь — + # замена этому ручному шагу. + # + # case ",$PROFILES," — пересоздаём только если профиль сервиса + # реально включён в ЭТОМ прогоне, иначе force-recreate ругается на + # несуществующий контейнер (сервис не создан вовсе). + case ",$PROFILES," in + *,alerts,*) + COMPOSE_PROFILES="$PROFILES" \ + docker compose -p gendesign-metrics -f docker-compose.metrics.yml \ + up -d --force-recreate alert-ack + echo "alert-ack: контейнер пересоздан — код монтируется с хоста, up -d его не подхватывает (#3490)." + ;; + esac + case ",$PROFILES," in + *,relay,*) + COMPOSE_PROFILES="$PROFILES" \ + docker compose -p gendesign-metrics -f docker-compose.metrics.yml \ + up -d --force-recreate tg-relay + echo "tg-relay: контейнер пересоздан — код монтируется с хоста, up -d его не подхватывает (#3490)." + ;; + esac + # ── Alertmanager: пересоздать, если конфиг перерисовали ───────── # `up -d` выше СЧИТАЕТ alertmanager неизменившимся: он сравнивает # описание сервиса, а содержимое бинд-маунта в это сравнение не diff --git a/docker-compose.metrics.yml b/docker-compose.metrics.yml index 967bdad9..db68e27c 100644 --- a/docker-compose.metrics.yml +++ b/docker-compose.metrics.yml @@ -236,6 +236,11 @@ services: tg-relay: image: python:3.12-slim container_name: gendesign-tg-relay + # #3471 (PR #3487 инцидент): без profiles сервис поднимался ВСЕГДА, а при + # пустом TG_RELAY_SECRET делает SystemExit — то есть уходит в бесконечный + # Restarting сразу после деплоя. Профиль включает deploy-metrics.yml, и + # только когда секрет реально задан (см. PROFILES там). + profiles: ["relay"] restart: unless-stopped user: "65534:65534" command: ["python", "-u", "/app/app.py"] diff --git a/tradein-mvp/docker-compose.prod.yml b/tradein-mvp/docker-compose.prod.yml index b5151794..191dfd8a 100644 --- a/tradein-mvp/docker-compose.prod.yml +++ b/tradein-mvp/docker-compose.prod.yml @@ -425,6 +425,13 @@ services: # (env_file ниже); пусто/нет токена = бот молча не стартует (см. tgbot_main.py). # ⚠️ НЕ дублировать TELEGRAM_* в блоке environment: (см. предупреждение у backend # выше про environment: перекрывающий env_file при пустой host-env). + # #3471: TELEGRAM_RELAY_BASE_URL/TELEGRAM_RELAY_SECRET (клиент резервного + # ретранслятора tg-relay на инфраструктурном хосте) заводятся тем же + # способом — строкой в backend/.env.runtime на ЭТОМ (продуктовом, Selectel) + # хосте. deploy-tradein.yml секреты приложения в CI не инжектит вовсе — + # только читает уже лежащий на хосте .env.runtime, поэтому Forgejo-секрет + # TELEGRAM_RELAY_SECRET сюда сам по себе не долетит: файл на хосте нужно + # дополнить вручную (см. README-АДМИНУ.md). tgbot: image: ghcr.io/lekss361/gendesign-tradein-backend:${IMAGE_TAG:-latest} container_name: tradein-tgbot