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:
@@ -4,7 +4,7 @@ import asyncio
|
||||
import base64
|
||||
import json
|
||||
import subprocess
|
||||
from collections.abc import Callable
|
||||
from collections.abc import AsyncIterator, Callable
|
||||
from dataclasses import replace
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
@@ -24,6 +24,7 @@ from app.config import RunnerSettings
|
||||
from app.db.models import Alert, AlertKind
|
||||
from app.db.session import SessionMaker
|
||||
from app.main import build_dispatcher
|
||||
from app.pipeline.watcher import PipelineWatcher
|
||||
from app.repo.git import UploadsRepo
|
||||
from app.runner.client import RunnerClient, RunnerResult
|
||||
from app.runner.deployer import Deployer, Outcome, changed_services
|
||||
@@ -168,14 +169,23 @@ async def test_one_broken_service_does_not_stop_others(repo: UploadsRepo) -> Non
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tg(texts: Texts, sessions: SessionMaker, access: AccessService, repo: UploadsRepo, fake_host: Path) -> TgClient:
|
||||
async def tg(
|
||||
texts: Texts, sessions: SessionMaker, access: AccessService, repo: UploadsRepo, fake_host: Path
|
||||
) -> AsyncIterator[TgClient]:
|
||||
loops: list[asyncio.Task[None]] = []
|
||||
|
||||
def dispatcher(bot: Bot) -> Dispatcher:
|
||||
alerts = AlertService(
|
||||
sessions, TelegramNotifier(bot, access, sessions, texts), load_alerts_config(ROOT / "config")
|
||||
)
|
||||
return build_dispatcher(texts, sessions, access, repo, alerts=alerts, runner=client())
|
||||
# no CI configured: the watcher deploys right after push
|
||||
watcher = PipelineWatcher(sessions, bot, texts, None, Deployer(repo, client(), alerts), poll_interval=0.05)
|
||||
loops.append(asyncio.create_task(watcher.run()))
|
||||
return build_dispatcher(texts, sessions, access, repo, alerts=alerts, runner=client(), pipeline=watcher)
|
||||
|
||||
return TgClient(dispatcher)
|
||||
yield TgClient(dispatcher)
|
||||
for loop in loops:
|
||||
loop.cancel()
|
||||
|
||||
|
||||
async def test_push_deploys_touched_service(tg: TgClient, texts: Texts, fake_host: Path, remote: Path) -> None:
|
||||
@@ -189,12 +199,15 @@ async def test_push_deploys_touched_service(tg: TgClient, texts: Texts, fake_hos
|
||||
await tg.send(ADMIN, "add util")
|
||||
await tg.press(ADMIN, CommitCb(a=Act.PUSH).pack())
|
||||
await tg.press(ADMIN, CommitCb(a=Act.PUSH_GO).pack())
|
||||
await until(lambda: any("работает" in (m.text or "") for m in tg.session.sent(SendMessage)))
|
||||
await until(lambda: any("развёрнуты" in (m.text or "") for m in tg.session.sent(SendMessage)))
|
||||
|
||||
pushed = [e for e in tg.session.sent(EditMessageText) if "Запушено" in (e.text or "")]
|
||||
assert pushed and "🚀 Разворачиваю: hello…" in pushed[-1].text
|
||||
[done] = [m for m in tg.session.sent(SendMessage) if "работает" in (m.text or "")]
|
||||
assert done.text.startswith("🚀 <b>hello</b> работает · ")
|
||||
pushed = [e.text for e in tg.session.sent(EditMessageText) if "Запушено" in (e.text or "")]
|
||||
assert "CI" not in pushed[0] and pushed[0].endswith(texts.pipeline.cd_running.format(services="hello"))
|
||||
await until(
|
||||
lambda: tg.session.sent(EditMessageText)[-1].text.endswith(texts.pipeline.cd_success.format(services="hello"))
|
||||
)
|
||||
[done] = [m for m in tg.session.sent(SendMessage) if "развёрнуты" in (m.text or "")]
|
||||
assert done.text.startswith("🚀 CD: сервисы hello развёрнуты, ревизия ")
|
||||
assert "tgit-svc-hello" in docker_state(fake_host)["containers"] # type: ignore[operator]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user