mirror of https://github.com/Chizi123/Arch-autobuild-repo.git

Joel Grunbaum
2 days ago dc96e98618fe4739210d10ee546b105e36433d02
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
"""Notification system with email and extensible webhook support."""
 
import asyncio
import smtplib
import ssl
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import Any
 
import aiohttp
 
from archbuild.builder import BuildResult, BuildStatus
from archbuild.config import Config, EmailConfig, WebhookConfig
from archbuild.logging import get_logger
 
logger = get_logger("notifications")
 
 
@dataclass
class BuildSummary:
    """Summary of build results for notifications."""
 
    total: int
    success: int
    failed: int
    skipped: int
    failed_packages: list[str]
    duration: float
    timestamp: datetime
 
    @classmethod
    def from_results(cls, results: list[BuildResult]) -> "BuildSummary":
        """Create summary from build results."""
        return cls(
            total=len(results),
            success=sum(1 for r in results if r.status == BuildStatus.SUCCESS),
            failed=sum(1 for r in results if r.status == BuildStatus.FAILED),
            skipped=sum(1 for r in results if r.status == BuildStatus.SKIPPED),
            failed_packages=[r.package for r in results if r.status == BuildStatus.FAILED],
            duration=sum(r.duration for r in results),
            timestamp=datetime.now(),
        )
 
 
class NotificationBackend(ABC):
    """Abstract base class for notification backends."""
 
    @abstractmethod
    async def send(self, summary: BuildSummary, config: Config) -> bool:
        """Send notification.
 
        Args:
            summary: Build summary
            config: Application config
 
        Returns:
            True if sent successfully
        """
        pass
 
 
class EmailBackend(NotificationBackend):
    """Email notification backend."""
 
    def __init__(self, email_config: EmailConfig):
        """Initialize email backend.
 
        Args:
            email_config: Email configuration
        """
        self.config = email_config
 
    def _format_message(self, summary: BuildSummary, repo_name: str) -> str:
        """Format email message body.
 
        Args:
            summary: Build summary
            repo_name: Repository name
 
        Returns:
            Formatted message string
        """
        lines = [
            f"Build Report for {repo_name}",
            f"Time: {summary.timestamp.strftime('%Y-%m-%d %H:%M:%S')}",
            "",
            f"Total packages: {summary.total}",
            f"  Successful: {summary.success}",
            f"  Failed: {summary.failed}",
            f"  Skipped: {summary.skipped}",
            f"Total duration: {summary.duration:.1f}s",
        ]
 
        if summary.failed_packages:
            lines.extend([
                "",
                "Failed packages:",
            ])
            for pkg in summary.failed_packages:
                lines.append(f"  - {pkg}")
 
        return "\n".join(lines)
 
    async def send(self, summary: BuildSummary, config: Config) -> bool:
        """Send email notification."""
        if not self.config.enabled:
            return True
 
        if not self.config.to:
            logger.warning("Email notification enabled but no recipient configured")
            return False
 
        # Only send on failures
        if summary.failed == 0:
            logger.debug("No failures, skipping email notification")
            return True
 
        try:
            msg = MIMEMultipart()
            msg["From"] = self.config.from_addr or f"archbuild@localhost"
            msg["To"] = self.config.to
            msg["Subject"] = f"Build Errors - {config.repository.name}"
 
            body = self._format_message(summary, config.repository.name)
            msg.attach(MIMEText(body, "plain"))
 
            # Send email
            loop = asyncio.get_event_loop()
            await loop.run_in_executor(None, self._send_email, msg)
 
            logger.info(f"Sent email notification to {self.config.to}")
            return True
 
        except Exception as e:
            logger.error(f"Failed to send email: {e}")
            return False
 
    def _send_email(self, msg: MIMEMultipart) -> None:
        """Send email synchronously (called from executor)."""
        if self.config.use_tls:
            context = ssl.create_default_context()
            with smtplib.SMTP_SSL(
                self.config.smtp_host,
                self.config.smtp_port,
                context=context,
            ) as server:
                if self.config.username and self.config.password:
                    server.login(self.config.username, self.config.password)
                server.send_message(msg)
        else:
            with smtplib.SMTP(self.config.smtp_host, self.config.smtp_port) as server:
                if self.config.username and self.config.password:
                    server.login(self.config.username, self.config.password)
                server.send_message(msg)
 
 
class WebhookBackend(NotificationBackend):
    """Generic webhook notification backend.
    
    Extensible for Discord, Slack, ntfy, Gotify, etc.
    """
 
    def __init__(self, webhook_config: WebhookConfig):
        """Initialize webhook backend.
 
        Args:
            webhook_config: Webhook configuration
        """
        self.config = webhook_config
 
    def _format_payload(self, summary: BuildSummary, repo_name: str) -> dict[str, Any]:
        """Format webhook payload based on webhook type.
 
        Args:
            summary: Build summary
            repo_name: Repository name
 
        Returns:
            Payload dictionary
        """
        base_content = (
            f"**Build Report for {repo_name}**\n"
            f"✅ Success: {summary.success} | "
            f"❌ Failed: {summary.failed} | "
            f"⏭️ Skipped: {summary.skipped}"
        )
 
        if summary.failed_packages:
            base_content += f"\n\nFailed: {', '.join(summary.failed_packages)}"
 
        if self.config.type == "discord":
            return {
                "content": base_content,
                "embeds": [{
                    "title": f"Build Report - {repo_name}",
                    "color": 0xFF0000 if summary.failed else 0x00FF00,
                    "fields": [
                        {"name": "Success", "value": str(summary.success), "inline": True},
                        {"name": "Failed", "value": str(summary.failed), "inline": True},
                        {"name": "Skipped", "value": str(summary.skipped), "inline": True},
                    ],
                    "timestamp": summary.timestamp.isoformat(),
                }],
            }
 
        elif self.config.type == "slack":
            return {
                "text": base_content,
                "blocks": [
                    {
                        "type": "section",
                        "text": {"type": "mrkdwn", "text": base_content},
                    }
                ],
            }
 
        elif self.config.type == "ntfy":
            return {
                "topic": repo_name,
                "title": f"Build Report - {repo_name}",
                "message": base_content,
                "priority": 5 if summary.failed else 3,
                "tags": ["package", "failed"] if summary.failed else ["package"],
            }
 
        else:  # generic
            return {
                "repository": repo_name,
                "summary": {
                    "total": summary.total,
                    "success": summary.success,
                    "failed": summary.failed,
                    "skipped": summary.skipped,
                },
                "failed_packages": summary.failed_packages,
                "timestamp": summary.timestamp.isoformat(),
            }
 
    async def send(self, summary: BuildSummary, config: Config) -> bool:
        """Send webhook notification."""
        if not self.config.enabled:
            return True
 
        if not self.config.url:
            logger.warning("Webhook notification enabled but no URL configured")
            return False
 
        # Only send on failures (configurable in future)
        if summary.failed == 0:
            logger.debug("No failures, skipping webhook notification")
            return True
 
        try:
            payload = self._format_payload(summary, config.repository.name)
 
            async with aiohttp.ClientSession() as session:
                async with session.post(
                    self.config.url,
                    json=payload,
                    timeout=aiohttp.ClientTimeout(total=30),
                ) as response:
                    response.raise_for_status()
 
            logger.info(f"Sent webhook notification to {self.config.url}")
            return True
 
        except Exception as e:
            logger.error(f"Failed to send webhook: {e}")
            return False
 
 
class NotificationManager:
    """Manage multiple notification backends."""
 
    def __init__(self, config: Config):
        """Initialize notification manager.
 
        Args:
            config: Application configuration
        """
        self.config = config
        self.backends: list[NotificationBackend] = []
 
        # Add email backend if configured
        if config.notifications.email.enabled:
            self.backends.append(EmailBackend(config.notifications.email))
 
        # Add webhook backends
        for webhook in config.notifications.webhooks:
            if webhook.enabled:
                self.backends.append(WebhookBackend(webhook))
 
    async def notify(self, results: list[BuildResult]) -> dict[str, bool]:
        """Send notifications for build results.
 
        Args:
            results: List of build results
 
        Returns:
            Dict mapping backend type to success status
        """
        summary = BuildSummary.from_results(results)
        statuses: dict[str, bool] = {}
 
        for backend in self.backends:
            name = type(backend).__name__
            try:
                success = await backend.send(summary, self.config)
                statuses[name] = success
            except Exception as e:
                logger.error(f"Notification backend {name} failed: {e}")
                statuses[name] = False
 
        return statuses
 
    async def test(self) -> dict[str, bool]:
        """Send test notification through all backends.
 
        Returns:
            Dict mapping backend type to success status
        """
        # Create dummy test summary
        test_summary = BuildSummary(
            total=3,
            success=2,
            failed=1,
            skipped=0,
            failed_packages=["test-package"],
            duration=123.45,
            timestamp=datetime.now(),
        )
 
        statuses: dict[str, bool] = {}
        for backend in self.backends:
            name = type(backend).__name__
            try:
                success = await backend.send(test_summary, self.config)
                statuses[name] = success
            except Exception as e:
                logger.error(f"Test notification failed for {name}: {e}")
                statuses[name] = False
 
        return statuses