CI / ruff · mypy · pytest (pull_request) Successful in 26m33s
- README: цели и задачи, что реализовано, развёртывание (локально за 5 минут и полный стек), переменные, команды и данные, используемые API, ограничения и лимиты, что не сделано, структура репозитория - examples/: hello (healthy + материал для grep/агента), crash (💥 и 🔁), cpu_hog (🔥), unhealthy (🩺), syntax_error/lint_fail для коммита, вопросы агенту и атаки; make demo-up / demo-down в том же Compose-проекте - make local-proxy: read-only Docker API для `make run` - ARCHITECTURE: краш при restart-политике и гистерезис алертов Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
287 lines
27 KiB
Markdown
287 lines
27 KiB
Markdown
# Архитектура tgit
|
||
|
||
> Требования — в [`SPEC.md`](SPEC.md) (v2), решения и их обоснования — в [`DECISIONS.md`](DECISIONS.md).
|
||
|
||
## 1. Общая схема
|
||
|
||
```
|
||
Telegram ◄──long polling──┐
|
||
│
|
||
┌────────────┴──────────────────────────────── хост (VPS / ноутбук) ───────────┐
|
||
│ tgit-bot (один процесс, asyncio) │
|
||
│ ├─ bot/ меню, FSM, роутеры разделов, middleware доступа │
|
||
│ ├─ access/ приглашения (QR), участники, антиперебор │
|
||
│ ├─ repo/ дерево репозитория, черновики, Publisher (git + SSH) ────────┼──► SourceCraft
|
||
│ ├─ pipeline/ PipelineWatcher: CI и CD по sha, уведомления автору ◄────────┼─── REST API
|
||
│ ├─ runtime/ состояние контейнеров, проверки, логи │
|
||
│ ├─ logs/ grep-движок (Python, без shell) │
|
||
│ ├─ agent/ фильтр → LLM-страж → LLM → LogQuery ─────────────────────────┼──► LLM API
|
||
│ └─ alerts/ детекторы, антиспам, рассылка всем, восстановление │
|
||
│ │ │ │
|
||
│ ┌────┴─────┐ ┌────────┴────────┐ │
|
||
│ │ postgres │ │ docker-proxy │ (только GET: containers/logs/ │
|
||
│ └──────────┘ │ │ events/stats) │
|
||
│ └────────┬────────┘ │
|
||
│ │ /var/run/docker.sock │
|
||
│ tgit-runner (скрипт на хосте) ─┤ │
|
||
│ ▲ ssh, forced command ▼ │
|
||
│ │ tgit-svc-<service> контейнеры (песочница) │
|
||
└─────┼──────────────────────────────────────────────────────────────────────── ┘
|
||
│
|
||
SourceCraft CI (tgit-uploads): check → deploy ──┘
|
||
```
|
||
|
||
**Репозитории:**
|
||
|
||
| Репозиторий | Что внутри | CI/CD |
|
||
|---|---|---|
|
||
| `tgit` (Gitea → зеркало в SourceCraft) | код бота | push в любую ветку: lint + types + tests; push `main`: + деплой бота на хост |
|
||
| `tgit-uploads` (SourceCraft) | общий репозиторий команды: `services/<name>/…` и любые файлы | push `main`: `check` → при успехе `deploy` изменённых сервисов через `tgit-runner` |
|
||
|
||
## 2. Компоненты
|
||
|
||
### 2.1 `app/bot` — интерфейс Telegram
|
||
aiogram 3. Главное меню — reply-клавиатура (📝 Коммит · 🩺 Статус · 📜 Логи · 🚨 Алерты · 👥 Участники для админа);
|
||
внутри разделов — инлайн-кнопки, сообщение раздела редактируется на месте.
|
||
|
||
- **Middleware доступа** — первым в цепочке, на `message` и `callback_query`: пускает только участников; исключение —
|
||
`/start <token>`. Неавторизованным — одна фраза раз в сутки, остальное игнорируется.
|
||
- **FSM** — `aiogram` storage поверх Postgres (своя реализация `BaseStorage`, таблица `fsm_state`) → состояние переживает рестарт.
|
||
- **callback_data ≤ 64 байт** — в кнопки не кладём пути: `CallbackData` с коротким id, а соответствие id → путь
|
||
хранится в данных FSM текущего экрана.
|
||
|
||
### 2.2 `app/access` — приглашения и участники
|
||
- `create_invite(admin) -> (link, png)`: `secrets.token_urlsafe(16)` → в БД `sha256(token)`, `expires_at`, `created_by`;
|
||
QR — библиотека `segno` (PNG в памяти).
|
||
- `redeem(token, tg_user)`: хэш → поиск → не истёк, не использован → в транзакции `used_by/used_at` + создание участника.
|
||
- Антиперебор: счётчик неудачных попыток по `tg_id` за час (таблица `access_attempts`).
|
||
- `revoke(user)`: `is_active = false`, удаление черновика, сброс FSM.
|
||
|
||
### 2.3 `app/repo` — дерево, черновики, пуш
|
||
- **Локальный клон** `tgit-uploads` в volume. Перед открытием браузера — `git fetch` (не чаще раза в 10 с);
|
||
дерево — `git ls-tree origin/main <path>`, содержимое — `git show origin/main:<path>`. Быстро и без API.
|
||
- **Черновик** (`drafts` + `draft_changes`): `base_sha` (HEAD при начале), список изменений `add|modify|delete` с
|
||
содержимым. Виртуальные новые папки живут только в FSM, пока в них не добавлен файл.
|
||
- **Валидация** (`repo/validate.py`): имя файла, размер, UTF-8, NUL, `ast.parse` для `.py`, запрет `.sourcecraft/` и служебных файлов.
|
||
- **Publisher** — единственная очередь (`asyncio.Queue`, один воркер), чтобы пуши не конфликтовали:
|
||
`fetch` → проверка конфликта (`git diff --name-only base_sha origin/main` ∩ пути черновика) → `reset --hard origin/main`
|
||
→ применить изменения → `commit` с трейлером `Telegram-User` → `push`; отказ push → `pull --rebase` (до 3 раз).
|
||
git — через `asyncio.create_subprocess_exec` (аргументы списком), `GIT_SSH_COMMAND` с ключом бота.
|
||
|
||
### 2.4 `app/sourcecraft` — клиент API
|
||
`httpx.AsyncClient`, Bearer-токен, ретраи на сетевые ошибки и 5xx (экспоненциально, до 5 попыток),
|
||
pydantic-модели ответов. Методы: `list_runs`, `get_run`, `get_cube_logs` (постранично), `run_commit_sha` — sha из лога куба `check/meta` (у прогона в API нет коммита, ADR-014). Подробности — [`dev/SOURCECRAFT_API.md`](dev/SOURCECRAFT_API.md).
|
||
|
||
### 2.5 `app/pipeline` — наблюдатель CI/CD
|
||
Фоновая задача. Пока есть незавершённые пуши — опрашивает `/cicd/runs` (10 с, бэкофф до 60 с), сопоставляет прогон с
|
||
пушем по sha (из куба `meta`, ADR-014). Отслеживает задачи `check` (CI) и `deploy` (CD) workflow `check-and-deploy` отдельно; на смене статуса — редактирует сообщение
|
||
о пуше и шлёт автору уведомление (успех / падение + лог упавшего куба). Падение `deploy` → событие для `alerts`.
|
||
Таймаут — 30 мин. Webhooks у SourceCraft нет (ADR-003).
|
||
|
||
### 2.6 `app/runtime` — контейнеры и проверки
|
||
Через `docker-socket-proxy` (только `GET`):
|
||
- **Список** — контейнеры с меткой `tgit.managed=true` (сервисы и служебные: bot, postgres, proxy).
|
||
- **Карточка** — `inspect` (state, health, restart count, started_at, метки `tgit.sha`, `tgit.deployed_at`) + `stats`.
|
||
- **Проверки** (`runtime/checks.py`) — набор независимых функций `Check -> CheckResult(ok, detail)`:
|
||
`running`, `docker_health`, `restarts_stable` (сравнение с 5 мин назад, история из БД), `cpu_ok`, `mem_ok`,
|
||
`no_errors_in_logs` (5 мин, шаблон из `config/checks.yaml`), `http_probe` (если у контейнера есть метка
|
||
`tgit.health_url`; пока нет runner (#13) — проба из сети бота). Каждая проверка — со своим таймаутом: упавшая или
|
||
зависшая даёт ❌ с причиной, остальные выполняются. Пороги и окно — в `config/checks.yaml`.
|
||
- **Изоляция** — прокси отдаёт контейнеры всего хоста, поэтому бот сам фильтрует по метке `tgit.managed=true` и
|
||
отказывает в `inspect`/`stats`/`logs` по id не из этого списка ещё до запроса.
|
||
- **Мониторинг** — поток `events` (`die`, `oom`, `health_status`, `restart`) и опрос `stats` раз в 15 с → события для `alerts`.
|
||
|
||
### 2.7 `app/logs` — grep-движок
|
||
- `parse_grep(expr) -> GrepQuery` — подмножество синтаксиса grep: шаблон, `-i`, `-v`, `-E`, `-c`, `-C N`; всё
|
||
остальное — ошибка с подсказкой. Разбор `shlex.split`, **никакого исполнения**.
|
||
- `run(query, source)` — источник `runtime.logs(container, since, tail)`; поиск модулем `regex` с таймаутом на
|
||
строку; лимиты: шаблон ≤ 200, вход ≤ 5 МБ, вывод ≤ 500 строк.
|
||
- `to_command(query)` — человекочитаемый `grep …` для показа пользователю.
|
||
|
||
### 2.8 `app/agent` — LLM-агент для логов
|
||
```
|
||
промт ─► prefilter ─✗─► отказ + алерт 🛡
|
||
│✓
|
||
▼
|
||
guard (LLM, JSON {verdict, reason}) ─ attack / невалидный JSON ─► отказ + алерт 🛡
|
||
│safe
|
||
▼
|
||
planner (LLM, JSON LogQuery: container, since, pattern, flags, limit) ─ невалидно ─► «не смог построить запрос»
|
||
│
|
||
▼
|
||
validate (pydantic, белый список контейнеров, компиляция regex, анализ бэктрекинга)
|
||
│
|
||
▼
|
||
logs.run(query) ─► ответ: grep-команда + строки
|
||
```
|
||
- `prefilter.py` — детерминированные правила из `config/agent_rules.yaml` (длина, шаблоны атак).
|
||
- `guard.py` и `planner.py` — **отдельные** вызовы с узкими системными промптами (`config/prompts/`); в planner
|
||
передаются только промт пользователя и список контейнеров — **логи в LLM не попадают** (ADR-007, ADR-013).
|
||
- Провайдер — `LLMProvider` + фабрика (ADR-008); в тестах `mock`.
|
||
- Журнал: `agent_requests` (промт, вердикт, запрос, токены); выполненный поиск — ещё и в `log_queries` (`mode=agent`).
|
||
- Квота — 30 вопросов на участника за скользящие сутки, считается по `agent_requests`.
|
||
- Недоступный LLM — «попробуйте позже», а не атака; невалидный ответ стража — атака; невалидный ответ планировщика —
|
||
«не смог построить запрос».
|
||
- Алерт 🛡 пока отправляется администраторам напрямую; с #15 он станет алертом `agent_attack` в общем журнале.
|
||
- При `LLM_PROVIDER=mock` агент выключен: mock отвечает стражу `{}`, и каждый вопрос считался бы атакой.
|
||
|
||
### 2.9 `app/alerts` — алерты
|
||
- **Детекторы** (`detectors.py`) — чистые функции «наблюдение за раунд + история снапшотов → условия, которые
|
||
выполняются сейчас»: 💥 краш (выход ≠ 0), 🧠 OOM, 🔁 ≥ 3 рестарта за 10 мин, 🩺 unhealthy, 🔥 CPU > 80 % или
|
||
RAM > 90 % на протяжении минуты, 💾 < 10 % свободного места на разделе тома бота, 🔌 Docker API недоступен > 5 мин.
|
||
Пороги — `config/alerts.yaml`. Монитор (`monitor.py`) вызывается после каждого раунда снапшотов (15 с).
|
||
- **По состоянию, а не по событиям**: сервис сверяет условия с открытыми алертами — новое открывает алерт, пропавшее
|
||
закрывает. Пропущенное событие не теряет алерт, а бесконечный рестарт остаётся одним алертом.
|
||
- **Дедупликация** (`service.py`): ключ `kind + service`. Условие, вернувшееся в течение 10 мин после закрытия, снова
|
||
открывает тот же алерт с `count += 1`: исходные сообщения редактируются, новые не шлются.
|
||
- **Разовые события** (🛡 атака через агента; 📦 CD и ⛔ отказ после деплоя — после #11/#13) открываются сразу
|
||
закрытыми; повтор в окне 10 мин — тоже счётчик.
|
||
- **Рассылка** (`broadcast.py`) — всем активным участникам, 🛡 — только администраторам; ~25 сообщений/с,
|
||
`RetryAfter` пережидается, заблокировавший бота участник пропускается. `alert_deliveries` хранит
|
||
`chat_id`/`message_id`, чтобы обновлять счётчик и отвечать «✅ восстановлено» на исходное сообщение.
|
||
- **Сообщение**: тип, сервис, время, хвост лога (30 строк), кнопки «📜 Логи», «🩺 Статус», «📋 Список алертов».
|
||
Кнопки открывают экран новым сообщением, чтобы не затирать алерт.
|
||
- **Список**: активные сверху, затем история за 7 дней, по 10 на страницу; 🛡 в общий список не попадает.
|
||
- **Если Docker недоступен**, оцениваются только 🔌 и 💾: алерты контейнеров не закрываются вслепую.
|
||
- **Краш при restart-политике**: контейнер поднимается за секунду и между раундами не виден в `exited` — поэтому
|
||
краш распознаётся и по росту `RestartCount` относительно предыдущего раунда; каждый новый — счётчик.
|
||
- **Гистерезис**: алерт закрывается, когда условия нет `resolve_after` (60 с), а правки со счётчиком — не чаще раза
|
||
в минуту: флапающий сервис не заваливает чат.
|
||
|
||
### 2.10 `app/db` — хранилище
|
||
SQLAlchemy 2 (async) + Alembic. PostgreSQL в Compose; в тестах — SQLite.
|
||
|
||
| Таблица | Поля (основные) |
|
||
|---|---|
|
||
| `users` | `tg_id` PK, `username`, `full_name`, `role` (`member`/`admin`), `is_active`, `invited_by`, `joined_at`, `last_seen_at` |
|
||
| `invites` | `id`, `token_hash` UNIQUE, `created_by`, `created_at`, `expires_at`, `used_by`, `used_at` |
|
||
| `access_attempts` | `tg_id`, `at`, `ok` |
|
||
| `fsm_state` | `key` PK, `state`, `data` JSON |
|
||
| `drafts` | `id`, `user_id` UNIQUE, `base_sha`, `message`, `created_at`, `expires_at` |
|
||
| `draft_changes` | `id`, `draft_id`, `path`, `op` (`add`/`modify`/`delete`), `content` BYTEA |
|
||
| `pushes` | `id`, `user_id`, `sha`, `message`, `paths` JSON, `chat_id`, `message_id`, `ci_status`, `cd_status`, `created_at` |
|
||
| `pipeline_runs` | `id`, `push_id`, `stage` (`ci`/`cd` — задачи одного workflow `check-and-deploy`), `run_slug`, `status`, `failed_cube`, `url`, `updated_at` |
|
||
| `container_snapshots` | `container`, `at`, `restart_count`, `cpu_pct`, `mem_pct`, `health` — для проверок «за 5 мин» (TTL 1 сутки) |
|
||
| `alerts` | `id`, `kind`, `service`, `payload` JSON, `count`, `opened_at`, `last_at`, `resolved_at` |
|
||
| `alert_deliveries` | `alert_id`, `chat_id`, `message_id` |
|
||
| `log_queries` | `id`, `user_id`, `container`, `mode` (`tail`/`grep`/`agent`), `query` JSON, `lines`, `at` |
|
||
| `agent_requests` | `id`, `user_id`, `prompt`, `verdict`, `reason`, `log_query` JSON, `tokens`, `at` |
|
||
|
||
### 2.11 Конфигурация
|
||
`app/config.py` — `pydantic-settings`, класс на подсистему (`TelegramSettings`, `AccessSettings`, `SourceCraftSettings`,
|
||
`GitSettings`, `DockerSettings`, `LLMSettings`, `AlertSettings`), всё из `.env`. Пороги, тексты, правила, промпты — `config/*.yaml`.
|
||
|
||
## 3. Потоки данных
|
||
|
||
### 3.1 Вход по приглашению
|
||
```
|
||
админ ─ «Пригласить» ─► access.create_invite ─► QR (PNG) + ссылка
|
||
новый пользователь ─ скан QR ─► t.me/<bot>?start=<token> ─► /start <token> ─► redeem ─✓─► users + главное меню
|
||
└✗─► отказ (+ счётчик попыток)
|
||
```
|
||
|
||
### 3.2 Коммит → CI → CD
|
||
```
|
||
📝 Коммит ─► дерево origin/main ─► файл/папка ─► изменения в черновик ─► 💬 сообщение ─► 🚀 Push ─► подтверждение
|
||
│
|
||
Publisher (очередь) ─ fetch, конфликт?, commit, push ──► tgit-uploads@main
|
||
│ SourceCraft CI
|
||
«a1b2c3d ⏳ CI» ◄──────────────────────────────────────────────────────────────────────────┘ check: py_compile · ruff · pytest · bandit
|
||
PipelineWatcher ─ poll runs(sha) ─► «✅ CI» ─► «⏳ CD» ─► «🚀 CD: weather» deploy: ssh → tgit-runner deploy <svc> <sha>
|
||
└► ❌ + лог шага автору; ❌ CD → алерт всем
|
||
```
|
||
|
||
### 3.3 Логи через агента
|
||
См. §2.8; ответ — grep-команда + найденные строки; запись в `agent_requests` и `log_queries`.
|
||
|
||
### 3.4 Алерт
|
||
`runtime`/`pipeline`/хост → детектор → дедупликация → рассылка всем участникам → … → условие прошло → «✅ восстановлено».
|
||
|
||
## 4. Песочница сервисов (`tgit-runner`)
|
||
|
||
`infra/runner/tgit-runner` — bash-скрипт на хосте; единственная команда, доступная ключам CI и бота (`command="…"` в
|
||
`authorized_keys` пользователя `tgit-deploy`). Подкоманды: `deploy <service> <sha>`, `restart <service>`,
|
||
`probe <service> <path>` (HTTP-проба изнутри сети сервиса). Аргументы валидируются: имя `^[a-z][a-z0-9-]{1,30}$`, sha — hex.
|
||
|
||
`deploy`: забирает архив `services/<service>` по sha из `tgit-uploads` → собирает образ `tgit-svc-<service>:<sha>` из
|
||
шаблона (`python:3.12-slim` + `requirements.txt`, `HEALTHCHECK` из `tgit.yaml` сервиса, если задан) → запускает:
|
||
|
||
```
|
||
docker run -d --name tgit-svc-<service> --label tgit.managed=true --label tgit.service=<service>
|
||
--label tgit.sha=<sha> --label tgit.deployed_at=<iso>
|
||
--user 10001:10001 --read-only --tmpfs /tmp:size=16m
|
||
--cap-drop ALL --security-opt no-new-privileges
|
||
--memory 128m --memory-swap 128m --cpus 0.25 --pids-limit 64
|
||
--network none # или tgit-svc-egress, если сеть разрешена администратором
|
||
--restart on-failure:3
|
||
--log-opt max-size=5m --log-opt max-file=2
|
||
```
|
||
Если новый контейнер не стал healthy за 60 с — откат на предыдущий образ и ненулевой код выхода (CD падает → алерт ⛔).
|
||
|
||
## 5. CI/CD
|
||
|
||
### 5.1 Репозиторий бота (`tgit/.sourcecraft/ci.yaml`)
|
||
|
||
Код живёт в Gitea, SourceCraft получает его push-зеркалом и запускает CI/CD (ADR-010).
|
||
|
||
| Триггер | Workflow | Шаги |
|
||
|---|---|---|
|
||
| push в любую ветку (через зеркало Gitea; PR живут в Gitea — ADR-010) | `check` | `uv sync --frozen` → `ruff check` + `ruff format --check` → `mypy app` → `pytest --cov=app --cov-fail-under=70` |
|
||
| push в `main` | `check` + `deploy` | → `infra/deploy/deploy.sh` по SSH (секреты `DEPLOY_SSH_KEY`, `DEPLOY_HOST`) → smoke: healthcheck контейнера |
|
||
|
||
### 5.2 Репозиторий загрузок (`tgit-uploads/.sourcecraft/ci.yaml`)
|
||
Шаблон хранится в `tgit/infra/uploads-template/` и ставится в `tgit-uploads` один раз.
|
||
|
||
| Workflow | Шаги |
|
||
|---|---|
|
||
| `check` | изменённые файлы (`git diff --name-only HEAD~1`) → `py_compile` · `ruff check` (мягкий набор правил) · `pytest` для затронутых сервисов · `bandit` |
|
||
| `deploy` (после `check`) | изменённые `services/<name>/` → `ssh tgit-deploy@host deploy <name> <sha>` для каждого |
|
||
|
||
## 6. Деплой
|
||
|
||
| Где | Как |
|
||
|---|---|
|
||
| Любая машина | `git clone` → `cp .env.example .env` → заполнить → `make up` |
|
||
| VPS vobot (основной) | push в `main` → CI → `infra/deploy/deploy.sh` → `/srv/tgit` (пользователь `tgit-deploy`, Compose-проект `tgit`) |
|
||
|
||
`deploy.sh` (по образцу meditron): `git archive` → `scp` → распаковка в `app.new` → перенос `.env` → атомарная замена
|
||
`app` → `docker compose -p tgit up -d --build` → ожидание healthcheck → запись `REVISION`. Все SSH-шаги с ретраями
|
||
(хост режет часть соединений). Хост задаётся `DEPLOY_HOST` — тот же скрипт работает для любого сервера.
|
||
|
||
Бюджет ресурсов на VPS (свободно ~1,9 ГБ RAM): бот ≤ 256 МБ, postgres ≤ 256 МБ, proxy ≤ 32 МБ,
|
||
сервисы ≤ 10 × 128 МБ.
|
||
|
||
## 7. Структура репозитория
|
||
|
||
```
|
||
app/ код бота: bot, access, repo, sourcecraft, pipeline, runtime, logs, agent, alerts, db, config.py, main.py
|
||
config/ alerts.yaml, checks.yaml, agent_rules.yaml, texts.yaml, prompts/
|
||
infra/
|
||
deploy/ deploy.sh, docker-compose.server.yml
|
||
runner/ tgit-runner, шаблон Dockerfile сервиса, authorized_keys.example
|
||
uploads-template/ .sourcecraft/ci.yaml, ruff.toml, README для tgit-uploads
|
||
examples/ демо-сервисы: ok, syntax error, lint fail, crash, cpu hog, unhealthy, escape attempts
|
||
tests/ pytest: unit + интеграционные с фейками
|
||
docs/ SPEC, ARCHITECTURE, DECISIONS, dev/, презентация
|
||
Dockerfile · docker-compose.yml · Makefile · pyproject.toml · .env.example
|
||
```
|
||
|
||
## 8. Что важно учесть
|
||
|
||
| Риск | Что делаем |
|
||
|---|---|
|
||
| Случайные люди и спам | Только одноразовые приглашения с TTL; middleware на каждом апдейте; антиперебор токенов; лимиты пушей и запросов |
|
||
| Утечка QR | Одноразовость + 24 ч; админ видит, кто вошёл по какому приглашению, и может отозвать доступ |
|
||
| Участник ломает CI из бота | Запрет правок `.sourcecraft/` и служебных файлов (C-16) |
|
||
| Чужой код на одном сервере с прод-Vobot | Песочница §4, лимиты, сеть выключена; общий лимит 10 сервисов; в перспективе — отдельный хост (ADR-005) |
|
||
| Docker-сокет = root на хосте | Бот — только через read-only proxy; запуск — через `tgit-runner` с forced command |
|
||
| Пользователь `tgit-deploy` в группе `docker` ≈ root | Ключ ограничен одной командой; `tgit-runner` валидирует аргументы |
|
||
| Атака через агента (injection, exfiltration) | Фильтр → LLM-страж → структурированный `LogQuery` → валидация; логи в LLM не передаются; алерт админам |
|
||
| ReDoS в regex (свой grep и от LLM) | Модуль `regex` с таймаутом, лимит длины шаблона и объёма логов |
|
||
| Утечка секретов | Секреты не логируются и не идут в LLM; маскирование в выводе P2 |
|
||
| Нестабильный SourceCraft API | Ретраи, идемпотентность, проверка перед повторным POST |
|
||
| Гонки пушей и конфликты | Одна очередь Publisher; проверка изменений с `base_sha`; `pull --rebase` при отказе |
|
||
| Лимиты Telegram (4096 символов, 64 байта callback_data, ~30 msg/s) | Длинное — файлом; короткие id в кнопках; очередь рассылки алертов |
|
||
| Шторм алертов | Дедупликация `kind+service`, счётчик вместо новых сообщений, «восстановлено» ответом |
|
||
| Диск хоста занят на 75 % | Ротация логов контейнеров, `docker image prune` старых ревизий в `tgit-runner`, алерт 💾 |
|