feat(pipeline): уведомления CI/CD — PipelineWatcher, CI загрузок на Gitea Actions
CI / ruff · mypy · pytest (pull_request) Failing after 3m40s
CI / ruff · mypy · pytest (pull_request) Failing after 3m40s
- app/pipeline: протокол CIProvider, клиент Gitea Actions (прогон по head_sha, упавший шаг, лог джоба, ретраи), PipelineWatcher — опрос только при незавершённых push, сообщение о push обновляется CI ⏳ → ✅/❌ → CD 🚀/⛔, автору — отдельные уведомления - деплой сервисов — только после зелёного CI; старый push не откатывает сервис, который развернёт более новый; после рестарта бот продолжает с того же места - CI_PROVIDER=none — прежнее поведение (деплой сразу после push) - шаблон tgit-uploads: .gitea/workflows/check.yml; .gitea/ защищён от правок из бота - миграция 0002: старые push помечены skipped, чтобы не слать им «таймаут CI» - ADR-016, ARCHITECTURE §2.5/§5.2, README, DEPLOY, .env.example Refs #11 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+36
-18
@@ -11,8 +11,8 @@
|
||||
│ tgit-bot (один процесс, asyncio) │
|
||||
│ ├─ bot/ меню, FSM, роутеры разделов, middleware доступа │
|
||||
│ ├─ access/ приглашения (QR), участники, антиперебор │
|
||||
│ ├─ repo/ дерево репозитория, черновики, Publisher (git + SSH) ────────┼──► SourceCraft
|
||||
│ ├─ pipeline/ PipelineWatcher: CI и CD по sha, уведомления автору ◄────────┼─── REST API
|
||||
│ ├─ repo/ дерево репозитория, черновики, Publisher (git + SSH) ────────┼──► tgit-uploads (Gitea)
|
||||
│ ├─ pipeline/ PipelineWatcher: CI по sha → CD, уведомления автору ◄────────┼─── Gitea Actions API
|
||||
│ ├─ runtime/ состояние контейнеров, проверки, логи │
|
||||
│ ├─ logs/ grep-движок (Python, без shell) │
|
||||
│ ├─ agent/ фильтр → LLM-страж → LLM → LogQuery ─────────────────────────┼──► LLM API
|
||||
@@ -28,7 +28,7 @@
|
||||
│ │ tgit-svc-<service> контейнеры (песочница) │
|
||||
└─────┼──────────────────────────────────────────────────────────────────────── ┘
|
||||
│
|
||||
SourceCraft CI (tgit-uploads): check → deploy ──┘
|
||||
бот (pipeline/), после зелёного CI tgit-uploads ──┘
|
||||
```
|
||||
|
||||
**Репозитории:**
|
||||
@@ -36,7 +36,7 @@
|
||||
| Репозиторий | Что внутри | CI/CD |
|
||||
|---|---|---|
|
||||
| `tgit` (Gitea → зеркало в SourceCraft) | код бота | push в любую ветку: lint + types + tests; push `main`: + деплой бота на хост |
|
||||
| `tgit-uploads` (SourceCraft) | общий репозиторий команды: `services/<name>/…` и любые файлы | push `main`: `check` → при успехе `deploy` изменённых сервисов через `tgit-runner` |
|
||||
| `tgit-uploads` (Gitea, ADR-015/016) | общий репозиторий команды: `services/<name>/…` и любые файлы | push `main`: `check` в Gitea Actions → при успехе бот разворачивает изменённые сервисы через `tgit-runner` |
|
||||
|
||||
## 2. Компоненты
|
||||
|
||||
@@ -73,10 +73,26 @@ aiogram 3. Главное меню — reply-клавиатура (📝 Комм
|
||||
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).
|
||||
CI репозитория загрузок — Gitea Actions (`.gitea/workflows/check.yml`), CD — сам бот через `tgit-runner` (ADR-016).
|
||||
|
||||
```
|
||||
push ─► pushes(ci=pending, cd=pending|skipped) ─► wake()
|
||||
PipelineWatcher: CIProvider.find(sha) каждые 10 с (бэкофф до 60 с при ошибках API)
|
||||
├ pending/running ─► правка сообщения о push
|
||||
├ success/skipped ─► «✅ CI пройден» ─► Deployer (tgit-runner) ─► 🚀 / ⛔ + алерт ⛔ всем
|
||||
├ failed ─► «❌ CI упал на шаге …» + хвост лога (длинный — ещё и файлом), CD не запускается
|
||||
└ нет вердикта 30 мин ─► ⌛ timeout, CD не запускается
|
||||
```
|
||||
- **Провайдер за протоколом** `CIProvider` (`find(sha)`, `failure_log(run)`): `gitea.py` — у прогона Gitea есть
|
||||
`head_sha`, сопоставление прямое; SourceCraft (ADR-014, куб `meta`) подключится второй реализацией. `CI_PROVIDER=none` —
|
||||
CI не ждём, сервисы разворачиваются сразу после push.
|
||||
- **Состояние — в БД**: `pushes.ci_status/cd_status` и `pipeline_runs` (`ci` — id прогона, ссылка, упавший шаг;
|
||||
`cd` — `tgit-runner`, не развёрнутые сервисы). Сообщение о push каждый раз рендерится из БД. После рестарта бот
|
||||
продолжает: ждёт CI дальше, а прерванный деплой (`cd=running`) повторяет.
|
||||
- **Опрос только по делу**: нет незавершённых push — цикл спит до `wake()` от нового push.
|
||||
- **Порядок версий**: прогоны CI могут закончиться не по порядку; старый push не разворачивает сервис, который уже
|
||||
развернёт более новый прошедший CI push — иначе откатил бы его.
|
||||
- Правка сообщения не уведомляет, поэтому исходы — отдельными сообщениями автору ответом на сообщение о push (C-30…C-32).
|
||||
|
||||
### 2.6 `app/runtime` — контейнеры и проверки
|
||||
Через `docker-socket-proxy` (только `GET`):
|
||||
@@ -134,7 +150,7 @@ pydantic-модели ответов. Методы: `list_runs`, `get_run`, `get
|
||||
закрывает. Пропущенное событие не теряет алерт, а бесконечный рестарт остаётся одним алертом.
|
||||
- **Дедупликация** (`service.py`): ключ `kind + service`. Условие, вернувшееся в течение 10 мин после закрытия, снова
|
||||
открывает тот же алерт с `count += 1`: исходные сообщения редактируются, новые не шлются.
|
||||
- **Разовые события** (🛡 атака через агента; 📦 CD и ⛔ отказ после деплоя — после #11/#13) открываются сразу
|
||||
- **Разовые события** (🛡 атака через агента; ⛔ отказ при деплое — от `Deployer`) открываются сразу
|
||||
закрытыми; повтор в окне 10 мин — тоже счётчик.
|
||||
- **Рассылка** (`broadcast.py`) — всем активным участникам, 🛡 — только администраторам; ~25 сообщений/с,
|
||||
`RetryAfter` пережидается, заблокировавший бота участник пропускается. `alert_deliveries` хранит
|
||||
@@ -186,9 +202,9 @@ SQLAlchemy 2 (async) + Alembic. PostgreSQL в Compose; в тестах — SQLit
|
||||
│
|
||||
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 → алерт всем
|
||||
«a1b2c3d ⏳ CI» ◄──────────────────────────────────────────────────────────────────────────┘ Gitea Actions check: compile · ruff · bandit · pytest
|
||||
PipelineWatcher ─ poll runs(sha) ─► «✅ CI» ─► «🚀 CD: weather…» ─► «🚀 CD: weather развёрнут» CD: бот → ssh tgit-runner deploy <svc> <sha>
|
||||
└► ❌ + лог шага автору; ⛔ CD → алерт всем
|
||||
```
|
||||
|
||||
### 3.3 Логи через агента
|
||||
@@ -204,7 +220,7 @@ SQLAlchemy 2 (async) + Alembic. PostgreSQL в Compose; в тестах — SQLit
|
||||
`probe <service> <path>` (HTTP-проба изнутри сети сервиса). Аргументы валидируются: имя `^[a-z][a-z0-9-]{1,30}$`, sha — hex.
|
||||
|
||||
`deploy`: получает архив `services/<service>` в stdin — его делает бот (`git archive <sha>:services/<service>`)
|
||||
сразу после push, так что у хоста нет доступа к репозиторию → проверяет архив (только файлы и папки, без `..`,
|
||||
после зелёного CI (§2.5), так что у хоста нет доступа к репозиторию → проверяет архив (только файлы и папки, без `..`,
|
||||
≤ 20 МБ, есть `main.py`) → собирает образ `tgit-svc-<service>:<sha>` из
|
||||
шаблона (`python:3.12-slim` + `requirements.txt`, `HEALTHCHECK` из `tgit.yaml` сервиса, если задан) → запускает:
|
||||
|
||||
@@ -235,13 +251,15 @@ docker run -d --name tgit-svc-<service> --label tgit.managed=true --label tgit.s
|
||||
| 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` один раз.
|
||||
### 5.2 Репозиторий загрузок (`HSE/tgit-uploads`, `.gitea/workflows/check.yml`)
|
||||
Шаблон хранится в `tgit/infra/uploads-template/` и ставится в `tgit-uploads` один раз (ADR-016).
|
||||
|
||||
| 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>` для каждого |
|
||||
| `check` (Gitea Actions, push в `main`) | `compileall` → `ruff check` (мягкий набор правил) → `bandit -ll` → `pytest` для сервисов с тестами; первый упавший шаг бот показывает автору |
|
||||
| CD (бот, после зелёного `check`) | изменённые `services/<name>/` → `git archive` → `ssh tgit-deploy@host deploy <name> <sha>` для каждого |
|
||||
|
||||
`.sourcecraft/ci.yaml` в шаблоне остаётся для возврата к SourceCraft.
|
||||
|
||||
## 6. Деплой
|
||||
|
||||
|
||||
@@ -137,3 +137,21 @@
|
||||
**Решение:** CI (`.gitea/workflows/ci.yml`: ruff, mypy, миграции на Postgres, pytest) и CD (`.gitea/workflows/deploy.yml` → `infra/deploy/deploy.sh`) — на Gitea Actions, как у Vobot и meditron, на общих раннерах Gitea. Деплой — на хост Vobot, `dev`/`prod` по веткам, через пользователя `tgit-deploy`. Репозиторий загрузок на сервере — временно `HSE/tgit-uploads` в Gitea.
|
||||
**Альтернативы:** ждать SourceCraft — блокирует проверку и демо.
|
||||
**Последствия:** `.sourcecraft/ci.yaml` бота остаётся для будущего возврата; при возврате к SourceCraft меняются `GIT_UPLOADS_URL` и ключ, код не трогаем. На RF-сервере Telegram — только через прокси (`TELEGRAM_PROXY_URL`).
|
||||
|
||||
## ADR-016: CI репозитория загрузок — Gitea Actions; CD — бот после зелёного CI
|
||||
**Дата:** 2026-10-19 · **Статус:** принято (меняет ADR-003/014 в части репозитория загрузок; SourceCraft — при возврате)
|
||||
|
||||
**Контекст:** после ADR-015 репозиторий загрузок живёт в Gitea (`HSE/tgit-uploads`), а CI у него не было: бот
|
||||
разворачивал сервис сразу после push, проверив только синтаксис, и «⏳ CI» в сообщении не обновлялся (#11). SourceCraft
|
||||
по-прежнему не принимает ключ бота (#7).
|
||||
**Решение:** CI загрузок — Gitea Actions (`.gitea/workflows/check.yml`: compile · ruff · bandit · pytest). Бот
|
||||
опрашивает Gitea API (`/actions/runs?head_sha=…`) — у прогона есть sha, куб `meta` не нужен. CD остаётся у бота: после
|
||||
зелёного `check` он отправляет изменённые сервисы в `tgit-runner`, как в #13. Наблюдатель зависит от протокола
|
||||
`CIProvider`, вендорский код — в `app/pipeline/gitea.py`.
|
||||
**Альтернативы:** SourceCraft — блокирован ключом бота; деплой из самого workflow (ssh на хост из раннера Gitea) — ключ
|
||||
к `tgit-runner` пришлось бы отдать общим раннерам Gitea, а бот всё равно должен знать исход деплоя для автора и алертов;
|
||||
не ждать CI — ruff/bandit/тесты из CI ничего бы не решали.
|
||||
**Последствия:** боту нужен токен с чтением Actions репозитория загрузок (`CI_TOKEN`); без `CI_PROVIDER` поведение
|
||||
прежнее (деплой сразу). Деплой ждёт CI: на общих раннерах это от ~30 с до минут при очереди, таймаут — 30 мин.
|
||||
Возврат к SourceCraft — вторая реализация `CIProvider` поверх готового `SourceCraftClient` (ADR-014), код наблюдателя
|
||||
не меняется.
|
||||
|
||||
+2
-1
@@ -12,6 +12,7 @@ CD — `.gitea/workflows/deploy.yml` (Gitea Actions, ADR-015): push в `dev` →
|
||||
| Контейнеры | Compose-проект `tgit-<env>`: `tgit-<env>-bot-1`, `tgit-<env>-postgres-1`; volumes `pgdata`, `uploads` |
|
||||
| Telegram | с сервера напрямую недоступен → `TELEGRAM_PROXY_URL` (тот же прокси, что у meditron) |
|
||||
| Репозиторий загрузок | временно `ssh://git@gitea.24studbank.ru:2222/HSE/tgit-uploads.git`, deploy key бота с записью |
|
||||
| CI загрузок | Gitea Actions `HSE/tgit-uploads` (`check.yml`); боту в `.env` — `CI_PROVIDER=gitea`, `CI_TOKEN` (чтение репозитория загрузок). Без них деплой сервисов идёт сразу после push, без CI |
|
||||
|
||||
Секреты репозитория в Gitea (Settings → Actions → Secrets): `DEPLOY_SSH_KEY` (приватный ключ `tgit-cd@gitea`), `DEPLOY_HOST` (`tgit-deploy@72.56.247.166`).
|
||||
|
||||
@@ -42,4 +43,4 @@ cd /srv/tgit/dev && rm -rf app && mv app.old app && cd app && docker compose -p
|
||||
|
||||
Нужен **отдельный токен бота** (один токен — один процесс polling): создать бота у @BotFather →
|
||||
`/srv/tgit/prod/.env` по образцу dev (`TELEGRAM_BOT_TOKEN`, `ADMIN_IDS`, `POSTGRES_PASSWORD`, `GIT_UPLOADS_URL`,
|
||||
`GIT_SSH_KEY_B64`, `TELEGRAM_PROXY_URL`, `LLM_PROVIDER`). Пока файла нет, деплой `main` пропускается.
|
||||
`GIT_SSH_KEY_B64`, `TELEGRAM_PROXY_URL`, `LLM_PROVIDER`, `CI_PROVIDER`, `CI_TOKEN`, `RUNNER_*`). Пока файла нет, деплой `main` пропускается.
|
||||
|
||||
Reference in New Issue
Block a user