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:
+10
-35
@@ -4,7 +4,6 @@ import base64
|
||||
import html
|
||||
import io
|
||||
import logging
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
@@ -19,6 +18,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from app.bot.menu import Nav, NavAction, free_input
|
||||
from app.db.models import ChangeOp, User
|
||||
from app.pipeline.watcher import PipelineWatcher, initial_status, render
|
||||
from app.repo.drafts import DraftFull, DraftService, DraftView
|
||||
from app.repo.git import Entry, GitError, UploadsRepo
|
||||
from app.repo.publisher import Publisher, PushResult, PushStatus
|
||||
@@ -34,8 +34,6 @@ from app.repo.validate import (
|
||||
python_syntax,
|
||||
strip_fence,
|
||||
)
|
||||
from app.runner.deployer import Deployer
|
||||
from app.runner.deployer import Outcome as DeployOutcome
|
||||
from app.texts import CommitTexts, Texts
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -220,7 +218,7 @@ def build_router(
|
||||
drafts: DraftService,
|
||||
publisher: Publisher,
|
||||
commit_url_template: str = "",
|
||||
deployer: Deployer | None = None,
|
||||
pipeline: PipelineWatcher | None = None,
|
||||
) -> Router:
|
||||
router = Router(name="commit")
|
||||
t = texts.commit
|
||||
@@ -283,21 +281,6 @@ def build_router(
|
||||
ops = {ChangeOp.ADD: t.op_add, ChangeOp.MODIFY: t.op_modify, ChangeOp.DELETE: t.op_delete}
|
||||
return "\n".join(f"{ops[c.op]} /{c.path}" for c in draft.changes)
|
||||
|
||||
def deploy_report(msg: Message, sha: str) -> Callable[[DeployOutcome], Awaitable[None]]:
|
||||
async def report(o: DeployOutcome) -> None:
|
||||
name = html.escape(o.service)
|
||||
if o.result.ok and o.removed:
|
||||
text = t.removed.format(service=name)
|
||||
elif o.result.ok:
|
||||
text = t.deployed.format(service=name, sha=sha[:7])
|
||||
else:
|
||||
text = t.deploy_failed.format(service=name, reason=html.escape(o.result.summary))
|
||||
if o.result.tail:
|
||||
text += f"\n<pre>{html.escape(o.result.tail[-3000:])}</pre>"
|
||||
await msg.answer(text, parse_mode=ParseMode.HTML)
|
||||
|
||||
return report
|
||||
|
||||
async def do_push(msg: Message, state: FSMContext, member: User, *, overwrite: bool) -> None:
|
||||
draft = await drafts.get(member.tg_id)
|
||||
if draft is None or not draft.changes or not draft.message:
|
||||
@@ -308,28 +291,20 @@ def build_router(
|
||||
result: PushResult = await publisher.push(member, draft, draft.message, overwrite=overwrite)
|
||||
match result.status:
|
||||
case PushStatus.OK:
|
||||
assert result.sha is not None
|
||||
first_line = draft.message.splitlines()[0]
|
||||
lines = [t.pushed.format(sha=result.sha[:7], message=first_line)]
|
||||
if commit_url_template:
|
||||
lines.append(commit_url_template.format(sha=result.sha))
|
||||
lines.append(t.ci_pending)
|
||||
if deployer is not None:
|
||||
services = deployer.schedule(
|
||||
result.sha, [c.path for c in draft.changes], deploy_report(msg, result.sha)
|
||||
)
|
||||
if services:
|
||||
lines.append(t.deploying.format(services=", ".join(services)))
|
||||
await msg.edit_text("\n".join(lines))
|
||||
await publisher.record(member, result, draft.message, draft, msg.chat.id, msg.message_id)
|
||||
paths = [c.path for c in draft.changes]
|
||||
status = pipeline.initial_status(paths) if pipeline else initial_status(False, False, paths)
|
||||
push = await publisher.record(member, result, draft.message, draft, msg.chat.id, msg.message_id, status)
|
||||
await msg.edit_text(render(texts, push, commit_url_template=commit_url_template))
|
||||
await drafts.clear(member.tg_id)
|
||||
await state.update_data(commit_virtual=[])
|
||||
if pipeline:
|
||||
pipeline.wake()
|
||||
case PushStatus.CONFLICT:
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text=t.overwrite, callback_data=CommitCb(a=Act.PUSH_FORCE))
|
||||
builder.button(text=texts.menu.cancel, callback_data=CommitCb(a=Act.DRAFT))
|
||||
paths = "\n".join(f"/{p}" for p in result.conflicts)
|
||||
await msg.edit_text(t.conflict.format(paths=paths), reply_markup=builder.as_markup())
|
||||
conflicts = "\n".join(f"/{p}" for p in result.conflicts)
|
||||
await msg.edit_text(t.conflict.format(paths=conflicts), reply_markup=builder.as_markup())
|
||||
case PushStatus.EMPTY:
|
||||
await drafts.clear(member.tg_id)
|
||||
await msg.edit_text(t.push_empty)
|
||||
|
||||
Reference in New Issue
Block a user