feat(pipeline): уведомления CI/CD — PipelineWatcher, CI загрузок на Gitea Actions
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:
2026-10-19 00:24:08 +03:00
co-authored by Claude Opus 5.5
parent eda5694270
commit 95a72437ec
29 changed files with 1293 additions and 117 deletions
+4 -2
View File
@@ -5,7 +5,7 @@ import pytest
from sqlalchemy import select
from app.access.service import AccessService
from app.db.models import ChangeOp, Push, User, utcnow
from app.db.models import ChangeOp, PipelineStatus, Push, User, utcnow
from app.db.session import SessionMaker
from app.repo.drafts import Change, DraftView
from app.repo.git import UploadsRepo
@@ -129,11 +129,13 @@ async def test_rate_limit_and_record(repo: UploadsRepo, sessions: SessionMaker,
publisher = Publisher(repo, sessions)
draft = await draft_on(repo, Change("x.txt", ChangeOp.ADD, b"x"))
result = await publisher.push(member, draft, "x")
await publisher.record(member, result, "x", draft, chat_id=ADMIN, message_id=77)
status = (PipelineStatus.PENDING, PipelineStatus.SKIPPED)
await publisher.record(member, result, "x", draft, chat_id=ADMIN, message_id=77, status=status)
async with sessions() as s:
push = await s.scalar(select(Push))
assert push is not None and push.sha == result.sha and push.paths == ["x.txt"] and push.message_id == 77
assert (push.ci_status, push.cd_status) == status
for i in range(PUSHES_PER_HOUR):
s.add(
Push(user_id=ADMIN, sha=f"{i:040d}", message="m", chat_id=1, created_at=utcnow() - timedelta(minutes=1))
+1
View File
@@ -27,6 +27,7 @@ def test_bad_names(name: str) -> None:
("path", "protected"),
[
(".sourcecraft/ci.yaml", True),
(".gitea/workflows/check.yml", True),
(".tgit/x.py", True),
("ruff.toml", True),
(".sourcecraft", True),