devs-webhook 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devs_webhook/__init__.py +21 -0
- devs_webhook/app.py +321 -0
- devs_webhook/cli/__init__.py +6 -0
- devs_webhook/cli/worker.py +296 -0
- devs_webhook/config.py +319 -0
- devs_webhook/core/__init__.py +1 -0
- devs_webhook/core/claude_dispatcher.py +420 -0
- devs_webhook/core/container_pool.py +1109 -0
- devs_webhook/core/deduplication.py +113 -0
- devs_webhook/core/repository_manager.py +197 -0
- devs_webhook/core/task_processor.py +286 -0
- devs_webhook/core/test_dispatcher.py +448 -0
- devs_webhook/core/webhook_config.py +16 -0
- devs_webhook/core/webhook_handler.py +57 -0
- devs_webhook/github/__init__.py +15 -0
- devs_webhook/github/app_auth.py +226 -0
- devs_webhook/github/client.py +472 -0
- devs_webhook/github/models.py +424 -0
- devs_webhook/github/parser.py +325 -0
- devs_webhook/main_cli.py +386 -0
- devs_webhook/sources/__init__.py +7 -0
- devs_webhook/sources/base.py +24 -0
- devs_webhook/sources/sqs_source.py +306 -0
- devs_webhook/sources/webhook_source.py +82 -0
- devs_webhook/utils/__init__.py +1 -0
- devs_webhook/utils/async_utils.py +86 -0
- devs_webhook/utils/github.py +43 -0
- devs_webhook/utils/logging.py +34 -0
- devs_webhook/utils/serialization.py +102 -0
- devs_webhook-0.1.0.dist-info/METADATA +664 -0
- devs_webhook-0.1.0.dist-info/RECORD +35 -0
- devs_webhook-0.1.0.dist-info/WHEEL +5 -0
- devs_webhook-0.1.0.dist-info/entry_points.txt +3 -0
- devs_webhook-0.1.0.dist-info/licenses/LICENSE +21 -0
- devs_webhook-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"""Test runner dispatcher for executing CI tests in containers."""
|
|
2
|
+
|
|
3
|
+
from typing import NamedTuple, Optional
|
|
4
|
+
import structlog
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from devs_common.core.project import Project
|
|
8
|
+
from devs_common.core.container import ContainerManager
|
|
9
|
+
from devs_common.core.workspace import WorkspaceManager
|
|
10
|
+
from ..config import get_config
|
|
11
|
+
from ..github.models import WebhookEvent, PushEvent, PullRequestEvent, DevsOptions
|
|
12
|
+
from ..github.client import GitHubClient
|
|
13
|
+
|
|
14
|
+
logger = structlog.get_logger()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestResult(NamedTuple):
|
|
18
|
+
"""Result of a test execution."""
|
|
19
|
+
success: bool
|
|
20
|
+
output: str
|
|
21
|
+
error: Optional[str] = None
|
|
22
|
+
exit_code: Optional[int] = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestDispatcher:
|
|
26
|
+
"""Dispatches test commands to containers and reports results via GitHub Checks API."""
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
"""Initialize test dispatcher."""
|
|
30
|
+
self.config = get_config()
|
|
31
|
+
|
|
32
|
+
self.github_client = GitHubClient(self.config)
|
|
33
|
+
|
|
34
|
+
logger.info("Test dispatcher initialized")
|
|
35
|
+
|
|
36
|
+
async def execute_tests(
|
|
37
|
+
self,
|
|
38
|
+
dev_name: str,
|
|
39
|
+
repo_path: Path,
|
|
40
|
+
event: WebhookEvent,
|
|
41
|
+
devs_options: Optional[DevsOptions] = None
|
|
42
|
+
) -> TestResult:
|
|
43
|
+
"""Execute tests using container and report results via GitHub Checks API.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
dev_name: Name of dev container (e.g., eamonn)
|
|
47
|
+
repo_path: Path to repository on host (already calculated by container_pool)
|
|
48
|
+
event: Original webhook event
|
|
49
|
+
devs_options: Options from DEVS.yml file
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Test execution result
|
|
53
|
+
"""
|
|
54
|
+
check_run_id = None
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
logger.info("Starting test execution",
|
|
58
|
+
container=dev_name,
|
|
59
|
+
repo=event.repository.full_name,
|
|
60
|
+
repo_path=str(repo_path))
|
|
61
|
+
|
|
62
|
+
# Determine the commit SHA to test
|
|
63
|
+
commit_sha = self._get_commit_sha(event)
|
|
64
|
+
logger.info("Commit SHA determination result", commit_sha=commit_sha)
|
|
65
|
+
|
|
66
|
+
if not commit_sha:
|
|
67
|
+
logger.error("Could not determine commit SHA, using fallback for testing")
|
|
68
|
+
# Use a fallback SHA for testing - in real scenarios this shouldn't happen
|
|
69
|
+
commit_sha = "HEAD" # Fallback to HEAD for now
|
|
70
|
+
|
|
71
|
+
# Create GitHub check run
|
|
72
|
+
# Safely extract installation ID, handling potential encoding issues
|
|
73
|
+
installation_id = None
|
|
74
|
+
try:
|
|
75
|
+
if event.installation and hasattr(event.installation, 'id') and event.installation.id is not None:
|
|
76
|
+
installation_id = str(event.installation.id)
|
|
77
|
+
logger.info("Extracted installation ID from event", installation_id=installation_id)
|
|
78
|
+
else:
|
|
79
|
+
logger.warning("No installation ID found in event")
|
|
80
|
+
except (AttributeError, TypeError, ValueError) as e:
|
|
81
|
+
logger.warning("Could not extract installation ID", error=str(e))
|
|
82
|
+
installation_id = None
|
|
83
|
+
|
|
84
|
+
# Skip GitHub API calls for test events or in dev mode
|
|
85
|
+
if hasattr(event, 'is_test') and event.is_test:
|
|
86
|
+
logger.info("Skipping GitHub check run creation for test event")
|
|
87
|
+
check_run_id = None
|
|
88
|
+
else:
|
|
89
|
+
logger.info("About to create GitHub check run",
|
|
90
|
+
repo=event.repository.full_name,
|
|
91
|
+
commit_sha=commit_sha,
|
|
92
|
+
installation_id=installation_id)
|
|
93
|
+
|
|
94
|
+
check_run_id = await self.github_client.create_check_run(
|
|
95
|
+
repo=event.repository.full_name,
|
|
96
|
+
name="devs-ci",
|
|
97
|
+
head_sha=commit_sha,
|
|
98
|
+
status="in_progress",
|
|
99
|
+
installation_id=installation_id
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
logger.info("GitHub check run creation attempt completed",
|
|
103
|
+
check_run_id=check_run_id,
|
|
104
|
+
success=check_run_id is not None)
|
|
105
|
+
|
|
106
|
+
if check_run_id:
|
|
107
|
+
logger.info("Created GitHub check run",
|
|
108
|
+
repo=event.repository.full_name,
|
|
109
|
+
check_run_id=check_run_id,
|
|
110
|
+
commit_sha=commit_sha)
|
|
111
|
+
|
|
112
|
+
# Execute tests
|
|
113
|
+
success, output, error, exit_code = self._execute_tests_sync(
|
|
114
|
+
repo_path,
|
|
115
|
+
dev_name,
|
|
116
|
+
event,
|
|
117
|
+
devs_options
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Build result
|
|
121
|
+
result = TestResult(
|
|
122
|
+
success=success,
|
|
123
|
+
output=output,
|
|
124
|
+
error=error if not success else None,
|
|
125
|
+
exit_code=exit_code
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Report results to GitHub
|
|
129
|
+
if check_run_id:
|
|
130
|
+
# Safely extract installation ID, handling potential encoding issues
|
|
131
|
+
installation_id = None
|
|
132
|
+
try:
|
|
133
|
+
if event.installation and hasattr(event.installation, 'id') and event.installation.id is not None:
|
|
134
|
+
installation_id = str(event.installation.id)
|
|
135
|
+
except (AttributeError, TypeError, ValueError) as e:
|
|
136
|
+
logger.warning("Could not extract installation ID", error=str(e))
|
|
137
|
+
installation_id = None
|
|
138
|
+
|
|
139
|
+
await self._report_test_results(
|
|
140
|
+
event.repository.full_name,
|
|
141
|
+
check_run_id,
|
|
142
|
+
result,
|
|
143
|
+
installation_id
|
|
144
|
+
)
|
|
145
|
+
elif hasattr(event, 'is_test') and event.is_test:
|
|
146
|
+
logger.info("Skipping GitHub check run result reporting for test event")
|
|
147
|
+
|
|
148
|
+
if result.success:
|
|
149
|
+
logger.info("Test execution completed successfully",
|
|
150
|
+
container=dev_name,
|
|
151
|
+
repo=event.repository.full_name,
|
|
152
|
+
exit_code=exit_code)
|
|
153
|
+
else:
|
|
154
|
+
logger.error("Test execution failed",
|
|
155
|
+
container=dev_name,
|
|
156
|
+
repo=event.repository.full_name,
|
|
157
|
+
exit_code=exit_code,
|
|
158
|
+
error=result.error)
|
|
159
|
+
|
|
160
|
+
return result
|
|
161
|
+
|
|
162
|
+
except Exception as e:
|
|
163
|
+
error_msg = f"Test execution failed: {str(e)}"
|
|
164
|
+
logger.error("Test execution error",
|
|
165
|
+
container=dev_name,
|
|
166
|
+
error=error_msg,
|
|
167
|
+
exc_info=True)
|
|
168
|
+
|
|
169
|
+
# Report failure to GitHub if we created a check run
|
|
170
|
+
if check_run_id:
|
|
171
|
+
# Safely extract installation ID, handling potential encoding issues
|
|
172
|
+
installation_id = None
|
|
173
|
+
try:
|
|
174
|
+
if event.installation and hasattr(event.installation, 'id') and event.installation.id is not None:
|
|
175
|
+
installation_id = str(event.installation.id)
|
|
176
|
+
except (AttributeError, TypeError, ValueError) as e:
|
|
177
|
+
logger.warning("Could not extract installation ID", error=str(e))
|
|
178
|
+
installation_id = None
|
|
179
|
+
|
|
180
|
+
await self.github_client.complete_check_run_failure(
|
|
181
|
+
repo=event.repository.full_name,
|
|
182
|
+
check_run_id=check_run_id,
|
|
183
|
+
title="Test execution failed",
|
|
184
|
+
summary=f"An error occurred during test execution: {error_msg}",
|
|
185
|
+
installation_id=installation_id
|
|
186
|
+
)
|
|
187
|
+
elif hasattr(event, 'is_test') and event.is_test:
|
|
188
|
+
logger.info("Skipping GitHub check run failure reporting for test event")
|
|
189
|
+
|
|
190
|
+
return TestResult(success=False, output="", error=error_msg)
|
|
191
|
+
|
|
192
|
+
def _execute_tests_sync(
|
|
193
|
+
self,
|
|
194
|
+
repo_path: Path,
|
|
195
|
+
dev_name: str,
|
|
196
|
+
event: WebhookEvent,
|
|
197
|
+
devs_options: Optional[DevsOptions] = None
|
|
198
|
+
) -> tuple[bool, str, str, int]:
|
|
199
|
+
"""Execute tests synchronously in container.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
repo_path: Path to repository
|
|
203
|
+
dev_name: Development environment name
|
|
204
|
+
event: Webhook event
|
|
205
|
+
devs_options: Options from DEVS.yml
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Tuple of (success, stdout, stderr, exit_code)
|
|
209
|
+
"""
|
|
210
|
+
try:
|
|
211
|
+
# 1. Create project, workspace manager, and container manager
|
|
212
|
+
project = Project(repo_path)
|
|
213
|
+
workspace_manager = WorkspaceManager(project, self.config)
|
|
214
|
+
container_manager = ContainerManager(project, self.config)
|
|
215
|
+
|
|
216
|
+
logger.info("Created project and managers for tests",
|
|
217
|
+
container=dev_name,
|
|
218
|
+
project_name=project.info.name)
|
|
219
|
+
|
|
220
|
+
# 2. Ensure workspace exists
|
|
221
|
+
workspace_dir = workspace_manager.create_workspace(dev_name, reset_contents=True)
|
|
222
|
+
|
|
223
|
+
logger.info("Workspace created/reset for tests",
|
|
224
|
+
container=dev_name,
|
|
225
|
+
workspace_dir=str(workspace_dir))
|
|
226
|
+
|
|
227
|
+
# 3. Ensure container is running with environment variables from DEVS.yml
|
|
228
|
+
extra_env = None
|
|
229
|
+
if devs_options:
|
|
230
|
+
extra_env = devs_options.get_env_vars(dev_name)
|
|
231
|
+
|
|
232
|
+
if not container_manager.ensure_container_running(
|
|
233
|
+
dev_name,
|
|
234
|
+
workspace_dir,
|
|
235
|
+
debug=self.config.dev_mode,
|
|
236
|
+
extra_env=extra_env
|
|
237
|
+
):
|
|
238
|
+
return False, "", f"Failed to start container for {dev_name}", 1
|
|
239
|
+
|
|
240
|
+
# 4. Checkout appropriate commit if this is a PR or push
|
|
241
|
+
commit_sha = self._get_commit_sha(event)
|
|
242
|
+
if commit_sha:
|
|
243
|
+
logger.info("Checking out commit for tests",
|
|
244
|
+
container=dev_name,
|
|
245
|
+
commit_sha=commit_sha)
|
|
246
|
+
|
|
247
|
+
checkout_success, checkout_stdout, checkout_stderr, checkout_code = self._exec_command_in_container(
|
|
248
|
+
project=project,
|
|
249
|
+
dev_name=dev_name,
|
|
250
|
+
workspace_dir=workspace_dir,
|
|
251
|
+
command=f"git checkout {commit_sha}",
|
|
252
|
+
debug=self.config.dev_mode
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
if not checkout_success:
|
|
256
|
+
logger.error("Failed to checkout commit",
|
|
257
|
+
container=dev_name,
|
|
258
|
+
commit_sha=commit_sha,
|
|
259
|
+
stderr=checkout_stderr)
|
|
260
|
+
return False, checkout_stdout, f"Failed to checkout commit {commit_sha}: {checkout_stderr}", checkout_code
|
|
261
|
+
|
|
262
|
+
# 5. Determine test command
|
|
263
|
+
test_command = "runtests.sh" # Default
|
|
264
|
+
if devs_options and devs_options.ci_test_command:
|
|
265
|
+
test_command = devs_options.ci_test_command
|
|
266
|
+
|
|
267
|
+
logger.info("Executing test command",
|
|
268
|
+
container=dev_name,
|
|
269
|
+
test_command=test_command)
|
|
270
|
+
|
|
271
|
+
# 6. Execute tests
|
|
272
|
+
success, stdout, stderr, exit_code = self._exec_command_in_container(
|
|
273
|
+
project=project,
|
|
274
|
+
dev_name=dev_name,
|
|
275
|
+
workspace_dir=workspace_dir,
|
|
276
|
+
command=test_command,
|
|
277
|
+
debug=self.config.dev_mode
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
logger.info("Test command completed",
|
|
281
|
+
container=dev_name,
|
|
282
|
+
success=success,
|
|
283
|
+
exit_code=exit_code,
|
|
284
|
+
output_length=len(stdout) if stdout else 0,
|
|
285
|
+
error_length=len(stderr) if stderr else 0)
|
|
286
|
+
|
|
287
|
+
return success, stdout, stderr, exit_code
|
|
288
|
+
|
|
289
|
+
except Exception as e:
|
|
290
|
+
error_msg = f"Test execution failed: {str(e)}"
|
|
291
|
+
logger.error("Test execution error",
|
|
292
|
+
container=dev_name,
|
|
293
|
+
error=error_msg,
|
|
294
|
+
exc_info=True)
|
|
295
|
+
return False, "", error_msg, 1
|
|
296
|
+
|
|
297
|
+
def _get_commit_sha(self, event: WebhookEvent) -> Optional[str]:
|
|
298
|
+
"""Get the commit SHA to test from the webhook event.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
event: Webhook event
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Commit SHA or None if not available
|
|
305
|
+
"""
|
|
306
|
+
logger.info("Extracting commit SHA from event",
|
|
307
|
+
event_type=type(event).__name__)
|
|
308
|
+
|
|
309
|
+
if isinstance(event, PushEvent):
|
|
310
|
+
sha = event.after
|
|
311
|
+
logger.info("Got commit SHA from PushEvent", sha=sha)
|
|
312
|
+
return sha
|
|
313
|
+
elif isinstance(event, PullRequestEvent):
|
|
314
|
+
sha = event.pull_request.head.get("sha")
|
|
315
|
+
logger.info("Got commit SHA from PullRequestEvent",
|
|
316
|
+
sha=sha, head_keys=list(event.pull_request.head.keys()))
|
|
317
|
+
return sha
|
|
318
|
+
else:
|
|
319
|
+
logger.warning("Event type not supported for commit SHA extraction",
|
|
320
|
+
event_type=type(event).__name__)
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
async def _report_test_results(
|
|
324
|
+
self,
|
|
325
|
+
repo_name: str,
|
|
326
|
+
check_run_id: int,
|
|
327
|
+
result: TestResult,
|
|
328
|
+
installation_id: Optional[str] = None
|
|
329
|
+
) -> None:
|
|
330
|
+
"""Report test results to GitHub via Checks API.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
repo_name: Repository name (owner/repo)
|
|
334
|
+
check_run_id: GitHub check run ID
|
|
335
|
+
result: Test execution result
|
|
336
|
+
installation_id: GitHub App installation ID if known from webhook event
|
|
337
|
+
"""
|
|
338
|
+
try:
|
|
339
|
+
if result.success:
|
|
340
|
+
await self.github_client.complete_check_run_success(
|
|
341
|
+
repo=repo_name,
|
|
342
|
+
check_run_id=check_run_id,
|
|
343
|
+
title="Tests passed",
|
|
344
|
+
summary=f"All tests completed successfully (exit code: {result.exit_code})",
|
|
345
|
+
installation_id=installation_id
|
|
346
|
+
)
|
|
347
|
+
logger.info("Reported test success to GitHub",
|
|
348
|
+
repo=repo_name,
|
|
349
|
+
check_run_id=check_run_id)
|
|
350
|
+
else:
|
|
351
|
+
# Truncate output for GitHub (limit to ~65k chars to stay under API limits)
|
|
352
|
+
error_text = result.error or result.output or "Test execution failed"
|
|
353
|
+
if len(error_text) > 65000:
|
|
354
|
+
error_text = error_text[:65000] + "\n\n[Output truncated]"
|
|
355
|
+
|
|
356
|
+
await self.github_client.complete_check_run_failure(
|
|
357
|
+
repo=repo_name,
|
|
358
|
+
check_run_id=check_run_id,
|
|
359
|
+
title="Tests failed",
|
|
360
|
+
summary=f"Tests failed with exit code: {result.exit_code}",
|
|
361
|
+
text=error_text,
|
|
362
|
+
installation_id=installation_id
|
|
363
|
+
)
|
|
364
|
+
logger.info("Reported test failure to GitHub",
|
|
365
|
+
repo=repo_name,
|
|
366
|
+
check_run_id=check_run_id,
|
|
367
|
+
exit_code=result.exit_code)
|
|
368
|
+
|
|
369
|
+
except Exception as e:
|
|
370
|
+
logger.error("Failed to report test results to GitHub",
|
|
371
|
+
repo=repo_name,
|
|
372
|
+
check_run_id=check_run_id,
|
|
373
|
+
error=str(e))
|
|
374
|
+
|
|
375
|
+
def _exec_command_in_container(
|
|
376
|
+
self,
|
|
377
|
+
project: Project,
|
|
378
|
+
dev_name: str,
|
|
379
|
+
workspace_dir: Path,
|
|
380
|
+
command: str,
|
|
381
|
+
debug: bool = False
|
|
382
|
+
) -> tuple[bool, str, str, int]:
|
|
383
|
+
"""Execute a command in the container.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
project: Project instance
|
|
387
|
+
dev_name: Development environment name
|
|
388
|
+
workspace_dir: Workspace directory path
|
|
389
|
+
command: Command to execute
|
|
390
|
+
debug: Show debug output
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Tuple of (success, stdout, stderr, exit_code)
|
|
394
|
+
"""
|
|
395
|
+
import subprocess
|
|
396
|
+
|
|
397
|
+
project_prefix = self.config.project_prefix if self.config else "dev"
|
|
398
|
+
container_name = project.get_container_name(dev_name, project_prefix)
|
|
399
|
+
workspace_name = project.get_workspace_name(dev_name)
|
|
400
|
+
container_workspace_dir = f"/workspaces/{workspace_name}"
|
|
401
|
+
|
|
402
|
+
try:
|
|
403
|
+
# Execute command in the container
|
|
404
|
+
# Use same pattern as exec_claude: cd to workspace directory then run command
|
|
405
|
+
full_cmd = f"source ~/.zshrc >/dev/stderr 2>&1 && cd {container_workspace_dir} && {command}"
|
|
406
|
+
cmd = [
|
|
407
|
+
'docker', 'exec', '-i', # -i for stdin, no TTY
|
|
408
|
+
container_name,
|
|
409
|
+
'/bin/zsh', '-c', full_cmd # Use zsh with explicit sourcing
|
|
410
|
+
]
|
|
411
|
+
|
|
412
|
+
if debug:
|
|
413
|
+
logger.info("Executing command in container",
|
|
414
|
+
container=container_name,
|
|
415
|
+
command=command,
|
|
416
|
+
full_command=' '.join(cmd))
|
|
417
|
+
|
|
418
|
+
# Execute command without streaming (for CI we want to collect all output)
|
|
419
|
+
process = subprocess.run(
|
|
420
|
+
cmd,
|
|
421
|
+
capture_output=True,
|
|
422
|
+
text=True
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
stdout = process.stdout if process.stdout else ""
|
|
426
|
+
stderr = process.stderr if process.stderr else ""
|
|
427
|
+
success = process.returncode == 0
|
|
428
|
+
exit_code = process.returncode
|
|
429
|
+
|
|
430
|
+
if debug:
|
|
431
|
+
logger.info("Command execution completed",
|
|
432
|
+
container=container_name,
|
|
433
|
+
command=command,
|
|
434
|
+
exit_code=exit_code,
|
|
435
|
+
success=success,
|
|
436
|
+
stdout_length=len(stdout),
|
|
437
|
+
stderr_length=len(stderr))
|
|
438
|
+
|
|
439
|
+
return success, stdout, stderr, exit_code
|
|
440
|
+
|
|
441
|
+
except Exception as e:
|
|
442
|
+
error_msg = f"Command execution failed: {str(e)}"
|
|
443
|
+
logger.error("Error executing command in container",
|
|
444
|
+
container=container_name,
|
|
445
|
+
command=command,
|
|
446
|
+
error=error_msg,
|
|
447
|
+
exc_info=True)
|
|
448
|
+
return False, "", error_msg, 1
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Webhook-specific configuration extending the base config."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from devs_common.config import BaseConfig
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class WebhookConfig(BaseConfig):
|
|
8
|
+
"""Configuration for webhook handler extending base config."""
|
|
9
|
+
|
|
10
|
+
def get_default_workspaces_dir(self) -> Path:
|
|
11
|
+
"""Get default workspaces directory - shared with CLI for interoperability."""
|
|
12
|
+
return Path.home() / ".devs" / "workspaces"
|
|
13
|
+
|
|
14
|
+
def get_default_project_prefix(self) -> str:
|
|
15
|
+
"""Get default project prefix - same as CLI for interoperability."""
|
|
16
|
+
return "dev"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Main webhook event handler (compatibility layer).
|
|
2
|
+
|
|
3
|
+
This module provides backward compatibility for the existing WebhookHandler API
|
|
4
|
+
by delegating to the new TaskProcessor.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Any
|
|
8
|
+
import structlog
|
|
9
|
+
|
|
10
|
+
from .task_processor import TaskProcessor
|
|
11
|
+
|
|
12
|
+
logger = structlog.get_logger()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class WebhookHandler:
|
|
16
|
+
"""Main webhook event handler that coordinates all components.
|
|
17
|
+
|
|
18
|
+
This is now a thin compatibility wrapper around TaskProcessor.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
"""Initialize webhook handler."""
|
|
23
|
+
self.task_processor = TaskProcessor()
|
|
24
|
+
self.container_pool = self.task_processor.container_pool
|
|
25
|
+
self.config = self.task_processor.config
|
|
26
|
+
|
|
27
|
+
logger.info("Webhook handler initialized (compatibility mode)",
|
|
28
|
+
mentioned_user=self.config.github_mentioned_user,
|
|
29
|
+
container_pool=self.config.get_container_pool_list())
|
|
30
|
+
|
|
31
|
+
async def process_webhook(
|
|
32
|
+
self,
|
|
33
|
+
headers: Dict[str, str],
|
|
34
|
+
payload: bytes,
|
|
35
|
+
delivery_id: str
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Process a GitHub webhook event.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
headers: HTTP headers from webhook
|
|
41
|
+
payload: Raw webhook payload
|
|
42
|
+
delivery_id: GitHub delivery ID for tracking
|
|
43
|
+
"""
|
|
44
|
+
# Delegate to TaskProcessor
|
|
45
|
+
await self.task_processor.process_webhook(headers, payload, delivery_id)
|
|
46
|
+
|
|
47
|
+
async def get_status(self) -> Dict[str, Any]:
|
|
48
|
+
"""Get current handler status."""
|
|
49
|
+
return await self.task_processor.get_status()
|
|
50
|
+
|
|
51
|
+
async def stop_container(self, container_name: str) -> bool:
|
|
52
|
+
"""Manually stop a container."""
|
|
53
|
+
return await self.task_processor.stop_container(container_name)
|
|
54
|
+
|
|
55
|
+
async def list_containers(self) -> Dict[str, Any]:
|
|
56
|
+
"""List all managed containers."""
|
|
57
|
+
return await self.task_processor.list_containers()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""GitHub integration modules."""
|
|
2
|
+
|
|
3
|
+
from .models import WebhookEvent, IssueEvent, PullRequestEvent, CommentEvent
|
|
4
|
+
from .parser import WebhookParser
|
|
5
|
+
from .client import GitHubClient
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"WebhookEvent",
|
|
9
|
+
"IssueEvent",
|
|
10
|
+
"PullRequestEvent",
|
|
11
|
+
"CommentEvent",
|
|
12
|
+
"WebhookParser",
|
|
13
|
+
"GitHubClient",
|
|
14
|
+
"TestIssueEvent",
|
|
15
|
+
]
|