devs-webhook 0.1.0__tar.gz → 0.1.2__tar.gz
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-0.1.0 → devs_webhook-0.1.2}/PKG-INFO +21 -2
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/README.md +20 -1
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/__init__.py +4 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/cli/worker.py +15 -26
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/config.py +5 -1
- devs_webhook-0.1.2/devs_webhook/core/base_dispatcher.py +57 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/core/claude_dispatcher.py +17 -22
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/core/container_pool.py +35 -152
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/core/test_dispatcher.py +25 -31
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/github/models.py +0 -29
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/main_cli.py +30 -6
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/sources/__init__.py +2 -2
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/sources/sqs_source.py +101 -5
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/utils/serialization.py +2 -1
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook.egg-info/PKG-INFO +21 -2
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook.egg-info/SOURCES.txt +2 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/pyproject.toml +2 -2
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/tests/test_single_queue.py +68 -58
- devs_webhook-0.1.2/tests/test_sqs_burst.py +282 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/LICENSE +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/app.py +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/cli/__init__.py +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/core/__init__.py +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/core/deduplication.py +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/core/repository_manager.py +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/core/task_processor.py +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/core/webhook_config.py +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/core/webhook_handler.py +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/github/__init__.py +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/github/app_auth.py +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/github/client.py +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/github/parser.py +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/sources/base.py +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/sources/webhook_source.py +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/utils/__init__.py +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/utils/async_utils.py +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/utils/github.py +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook/utils/logging.py +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook.egg-info/dependency_links.txt +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook.egg-info/entry_points.txt +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook.egg-info/requires.txt +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/devs_webhook.egg-info/top_level.txt +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/setup.cfg +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/tests/test_allowlist.py +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/tests/test_authentication.py +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/tests/test_authorized_users.py +0 -0
- {devs_webhook-0.1.0 → devs_webhook-0.1.2}/tests/test_webhook_parser.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devs-webhook
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: GitHub webhook handler for automated devcontainer operations with Claude Code
|
|
5
5
|
Author: Dan Lester
|
|
6
6
|
License-Expression: MIT
|
|
@@ -156,6 +156,25 @@ export AWS_SQS_QUEUE_URL="https://sqs.us-east-1.amazonaws.com/..."
|
|
|
156
156
|
devs-webhook serve --source sqs
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
+
#### Burst Mode
|
|
160
|
+
|
|
161
|
+
For batch processing or scheduled jobs, use `--burst` to process all available messages and exit:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# Process all messages in queue then exit
|
|
165
|
+
devs-webhook serve --source sqs --burst
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Exit codes:
|
|
169
|
+
- **0**: Successfully processed one or more messages
|
|
170
|
+
- **42**: Queue was empty (no messages to process)
|
|
171
|
+
- **Other**: Error occurred
|
|
172
|
+
|
|
173
|
+
This is useful for:
|
|
174
|
+
- Scheduled cron jobs that drain the queue periodically
|
|
175
|
+
- Lambda-triggered batch processing
|
|
176
|
+
- Testing and debugging SQS integration
|
|
177
|
+
|
|
159
178
|
**Security**: Both modes validate GitHub webhook signatures for defense-in-depth security.
|
|
160
179
|
|
|
161
180
|
For detailed configuration and deployment guides, see:
|
|
@@ -365,7 +384,7 @@ Available options:
|
|
|
365
384
|
- **`ci_test_command`**: Command to run for CI tests
|
|
366
385
|
- Shell command executed in container for CI test runs
|
|
367
386
|
- Should exit with code 0 for success, non-zero for failure
|
|
368
|
-
- Default:
|
|
387
|
+
- Default: `./runtests.sh`
|
|
369
388
|
|
|
370
389
|
- **`ci_branches`**: Branches to run CI on for push events
|
|
371
390
|
- List of branch names to trigger CI for push events
|
|
@@ -99,6 +99,25 @@ export AWS_SQS_QUEUE_URL="https://sqs.us-east-1.amazonaws.com/..."
|
|
|
99
99
|
devs-webhook serve --source sqs
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
+
#### Burst Mode
|
|
103
|
+
|
|
104
|
+
For batch processing or scheduled jobs, use `--burst` to process all available messages and exit:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Process all messages in queue then exit
|
|
108
|
+
devs-webhook serve --source sqs --burst
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Exit codes:
|
|
112
|
+
- **0**: Successfully processed one or more messages
|
|
113
|
+
- **42**: Queue was empty (no messages to process)
|
|
114
|
+
- **Other**: Error occurred
|
|
115
|
+
|
|
116
|
+
This is useful for:
|
|
117
|
+
- Scheduled cron jobs that drain the queue periodically
|
|
118
|
+
- Lambda-triggered batch processing
|
|
119
|
+
- Testing and debugging SQS integration
|
|
120
|
+
|
|
102
121
|
**Security**: Both modes validate GitHub webhook signatures for defense-in-depth security.
|
|
103
122
|
|
|
104
123
|
For detailed configuration and deployment guides, see:
|
|
@@ -308,7 +327,7 @@ Available options:
|
|
|
308
327
|
- **`ci_test_command`**: Command to run for CI tests
|
|
309
328
|
- Shell command executed in container for CI test runs
|
|
310
329
|
- Should exit with code 0 for success, non-zero for failure
|
|
311
|
-
- Default:
|
|
330
|
+
- Default: `./runtests.sh`
|
|
312
331
|
|
|
313
332
|
- **`ci_branches`**: Branches to run CI on for push events
|
|
314
333
|
- List of branch names to trigger CI for push events
|
|
@@ -11,6 +11,8 @@ from .core.webhook_handler import WebhookHandler
|
|
|
11
11
|
from .core.container_pool import ContainerPool
|
|
12
12
|
from .core.repository_manager import RepositoryManager
|
|
13
13
|
from .core.claude_dispatcher import ClaudeDispatcher
|
|
14
|
+
from .core.test_dispatcher import TestDispatcher
|
|
15
|
+
from .core.base_dispatcher import TaskResult
|
|
14
16
|
|
|
15
17
|
__all__ = [
|
|
16
18
|
"WebhookConfig",
|
|
@@ -18,4 +20,6 @@ __all__ = [
|
|
|
18
20
|
"ContainerPool",
|
|
19
21
|
"RepositoryManager",
|
|
20
22
|
"ClaudeDispatcher",
|
|
23
|
+
"TestDispatcher",
|
|
24
|
+
"TaskResult",
|
|
21
25
|
]
|
|
@@ -16,7 +16,8 @@ from devs_common.core.project import Project
|
|
|
16
16
|
from ..config import get_config
|
|
17
17
|
from ..core.claude_dispatcher import ClaudeDispatcher
|
|
18
18
|
from ..core.test_dispatcher import TestDispatcher
|
|
19
|
-
from ..github.models import AnyWebhookEvent
|
|
19
|
+
from ..github.models import AnyWebhookEvent
|
|
20
|
+
from devs_common.devs_config import DevsOptions
|
|
20
21
|
|
|
21
22
|
logger = structlog.get_logger()
|
|
22
23
|
|
|
@@ -217,38 +218,26 @@ def _process_task_subprocess(
|
|
|
217
218
|
# Initialize appropriate dispatcher based on task type
|
|
218
219
|
if task_type == 'tests':
|
|
219
220
|
dispatcher = TestDispatcher()
|
|
220
|
-
logger.info("Executing task with Test dispatcher",
|
|
221
|
-
task_id=task_id,
|
|
222
|
-
dev_name=dev_name,
|
|
223
|
-
workspace_name=workspace_name)
|
|
224
|
-
|
|
225
|
-
# Execute tests
|
|
226
|
-
result = asyncio.run(dispatcher.execute_tests(
|
|
227
|
-
dev_name=dev_name,
|
|
228
|
-
repo_path=repo_path,
|
|
229
|
-
event=event,
|
|
230
|
-
devs_options=devs_options
|
|
231
|
-
))
|
|
232
221
|
else:
|
|
233
222
|
# Default to Claude execution
|
|
234
223
|
dispatcher = ClaudeDispatcher()
|
|
235
|
-
logger.info("Executing task with Claude dispatcher",
|
|
236
|
-
task_id=task_id,
|
|
237
|
-
dev_name=dev_name,
|
|
238
|
-
workspace_name=workspace_name)
|
|
239
224
|
|
|
240
225
|
# Ensure task_description is provided for Claude tasks
|
|
241
226
|
if not task_description:
|
|
242
227
|
raise ValueError("task_description is required for Claude tasks")
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
228
|
+
|
|
229
|
+
logger.info(f"Executing task with {dispatcher.dispatcher_name} dispatcher",
|
|
230
|
+
task_id=task_id,
|
|
231
|
+
dev_name=dev_name,
|
|
232
|
+
workspace_name=workspace_name)
|
|
233
|
+
|
|
234
|
+
result = asyncio.run(dispatcher.execute_task(
|
|
235
|
+
dev_name=dev_name,
|
|
236
|
+
repo_path=repo_path,
|
|
237
|
+
event=event,
|
|
238
|
+
devs_options=devs_options,
|
|
239
|
+
task_description=task_description
|
|
240
|
+
))
|
|
252
241
|
|
|
253
242
|
if result.success:
|
|
254
243
|
logger.info("Task execution completed successfully",
|
|
@@ -236,7 +236,11 @@ class WebhookConfig(BaseSettings, BaseConfig):
|
|
|
236
236
|
def get_default_workspaces_dir(self) -> Path:
|
|
237
237
|
"""Get default workspaces directory for webhook package."""
|
|
238
238
|
return Path.home() / ".devs" / "workspaces"
|
|
239
|
-
|
|
239
|
+
|
|
240
|
+
def get_default_bridge_dir(self) -> Path:
|
|
241
|
+
"""Get default bridge directory for webhook package."""
|
|
242
|
+
return Path.home() / ".devs" / "bridge"
|
|
243
|
+
|
|
240
244
|
def get_default_project_prefix(self) -> str:
|
|
241
245
|
"""Get default project prefix for webhook package."""
|
|
242
246
|
return "dev"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Base dispatcher class with shared functionality."""
|
|
2
|
+
|
|
3
|
+
from typing import NamedTuple, Optional
|
|
4
|
+
import structlog
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ..config import get_config
|
|
8
|
+
from ..github.models import WebhookEvent
|
|
9
|
+
from ..github.client import GitHubClient
|
|
10
|
+
from devs_common.devs_config import DevsOptions
|
|
11
|
+
|
|
12
|
+
logger = structlog.get_logger()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TaskResult(NamedTuple):
|
|
16
|
+
"""Result of a task execution (consolidates TestResult and TaskResult)."""
|
|
17
|
+
success: bool
|
|
18
|
+
output: str
|
|
19
|
+
error: Optional[str] = None
|
|
20
|
+
exit_code: Optional[int] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BaseDispatcher:
|
|
24
|
+
"""Base class for dispatchers with common functionality."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, dispatcher_type: str = "base"):
|
|
27
|
+
"""Initialize dispatcher with common setup.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
dispatcher_type: Type of dispatcher for logging
|
|
31
|
+
"""
|
|
32
|
+
self.config = get_config()
|
|
33
|
+
self.github_client = GitHubClient(self.config)
|
|
34
|
+
|
|
35
|
+
logger.info(f"{dispatcher_type.title()} dispatcher initialized")
|
|
36
|
+
|
|
37
|
+
async def execute_task(
|
|
38
|
+
self,
|
|
39
|
+
dev_name: str,
|
|
40
|
+
repo_path: Path,
|
|
41
|
+
event: WebhookEvent,
|
|
42
|
+
devs_options: Optional[DevsOptions] = None,
|
|
43
|
+
task_description: Optional[str] = None
|
|
44
|
+
) -> TaskResult:
|
|
45
|
+
"""Execute operation in container - to be implemented by subclasses.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
dev_name: Name of dev container (e.g., eamonn)
|
|
49
|
+
repo_path: Path to repository on host
|
|
50
|
+
event: Original webhook event
|
|
51
|
+
devs_options: Options from DEVS.yml file
|
|
52
|
+
task_description: Task description
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Task execution result
|
|
56
|
+
"""
|
|
57
|
+
raise NotImplementedError("Subclasses must implement execute_task")
|
|
@@ -1,44 +1,35 @@
|
|
|
1
1
|
"""Claude Code CLI integration for executing tasks in containers."""
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Optional
|
|
4
4
|
import structlog
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
from devs_common.core.project import Project
|
|
8
8
|
from devs_common.core.container import ContainerManager
|
|
9
9
|
from devs_common.core.workspace import WorkspaceManager
|
|
10
|
-
from ..
|
|
11
|
-
from
|
|
12
|
-
from
|
|
10
|
+
from ..github.models import WebhookEvent, IssueEvent, PullRequestEvent, CommentEvent
|
|
11
|
+
from devs_common.devs_config import DevsOptions
|
|
12
|
+
from .base_dispatcher import BaseDispatcher, TaskResult
|
|
13
13
|
|
|
14
14
|
logger = structlog.get_logger()
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
class
|
|
18
|
-
"""Result of a Claude Code task execution."""
|
|
19
|
-
success: bool
|
|
20
|
-
output: str
|
|
21
|
-
error: Optional[str] = None
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class ClaudeDispatcher:
|
|
17
|
+
class ClaudeDispatcher(BaseDispatcher):
|
|
25
18
|
"""Dispatches tasks to Claude Code CLI running in containers."""
|
|
26
19
|
|
|
20
|
+
dispatcher_name = "Claude"
|
|
21
|
+
|
|
27
22
|
def __init__(self):
|
|
28
23
|
"""Initialize Claude dispatcher."""
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
self.github_client = GitHubClient(self.config)
|
|
32
|
-
|
|
33
|
-
logger.info("Claude dispatcher initialized")
|
|
24
|
+
super().__init__("Claude")
|
|
34
25
|
|
|
35
26
|
async def execute_task(
|
|
36
27
|
self,
|
|
37
28
|
dev_name: str,
|
|
38
29
|
repo_path: Path,
|
|
39
|
-
task_description: str,
|
|
40
30
|
event: WebhookEvent,
|
|
41
|
-
devs_options: Optional[DevsOptions] = None
|
|
31
|
+
devs_options: Optional[DevsOptions] = None,
|
|
32
|
+
task_description: Optional[str] = None
|
|
42
33
|
) -> TaskResult:
|
|
43
34
|
"""Execute a task using Claude Code CLI in a container.
|
|
44
35
|
|
|
@@ -59,10 +50,13 @@ class ClaudeDispatcher:
|
|
|
59
50
|
repo_path=str(repo_path))
|
|
60
51
|
|
|
61
52
|
# Execute Claude directly - prompt building, workspace setup, container startup, Claude execution
|
|
53
|
+
# Use task_description if provided, otherwise extract from event
|
|
54
|
+
task_desc = task_description or "Process webhook event"
|
|
55
|
+
|
|
62
56
|
success, output, error = self._execute_claude_sync(
|
|
63
57
|
repo_path,
|
|
64
58
|
dev_name,
|
|
65
|
-
|
|
59
|
+
task_desc,
|
|
66
60
|
event,
|
|
67
61
|
devs_options
|
|
68
62
|
)
|
|
@@ -78,7 +72,8 @@ class ClaudeDispatcher:
|
|
|
78
72
|
result = TaskResult(
|
|
79
73
|
success=success,
|
|
80
74
|
output=output,
|
|
81
|
-
error=error if not success else None
|
|
75
|
+
error=error if not success else None,
|
|
76
|
+
exit_code=None # Claude doesn't provide exit codes
|
|
82
77
|
)
|
|
83
78
|
|
|
84
79
|
if result.success:
|
|
@@ -104,7 +99,7 @@ class ClaudeDispatcher:
|
|
|
104
99
|
exc_info=True)
|
|
105
100
|
|
|
106
101
|
await self._handle_task_failure(event, error_msg)
|
|
107
|
-
return TaskResult(success=False, output="", error=error_msg)
|
|
102
|
+
return TaskResult(success=False, output="", error=error_msg, exit_code=None)
|
|
108
103
|
|
|
109
104
|
def _execute_claude_sync(
|
|
110
105
|
self,
|
|
@@ -9,15 +9,15 @@ from datetime import datetime, timedelta, timezone
|
|
|
9
9
|
from typing import Dict, Optional, Any, NamedTuple
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
import structlog
|
|
12
|
-
import yaml
|
|
13
12
|
|
|
14
13
|
from devs_common.core.project import Project
|
|
15
14
|
from devs_common.core.container import ContainerManager
|
|
16
15
|
from devs_common.core.workspace import WorkspaceManager
|
|
16
|
+
from devs_common.devs_config import DevsConfigLoader, DevsOptions
|
|
17
17
|
|
|
18
18
|
from ..config import get_config
|
|
19
|
-
from ..github.models import WebhookEvent,
|
|
20
|
-
from .
|
|
19
|
+
from ..github.models import WebhookEvent, IssueEvent, PullRequestEvent, CommentEvent
|
|
20
|
+
from .base_dispatcher import TaskResult
|
|
21
21
|
from ..github.client import GitHubClient
|
|
22
22
|
|
|
23
23
|
logger = structlog.get_logger()
|
|
@@ -39,7 +39,6 @@ class ContainerPool:
|
|
|
39
39
|
def __init__(self):
|
|
40
40
|
"""Initialize container pool."""
|
|
41
41
|
self.config = get_config()
|
|
42
|
-
self.claude_dispatcher = ClaudeDispatcher()
|
|
43
42
|
|
|
44
43
|
# Track running containers for idle cleanup
|
|
45
44
|
self.running_containers: Dict[str, Dict[str, Any]] = {}
|
|
@@ -140,21 +139,6 @@ class ContainerPool:
|
|
|
140
139
|
Returns:
|
|
141
140
|
DevsOptions if user-specific config exists, None otherwise
|
|
142
141
|
"""
|
|
143
|
-
def _load_devs_yml(file_path: Path) -> dict:
|
|
144
|
-
"""Load and parse a DEVS.yml file, returning empty dict if not found."""
|
|
145
|
-
if not file_path.exists():
|
|
146
|
-
return {}
|
|
147
|
-
|
|
148
|
-
try:
|
|
149
|
-
with open(file_path, 'r') as f:
|
|
150
|
-
data = yaml.safe_load(f)
|
|
151
|
-
return data if data else {}
|
|
152
|
-
except Exception as e:
|
|
153
|
-
logger.warning("Failed to parse user DEVS.yml",
|
|
154
|
-
file_path=str(file_path),
|
|
155
|
-
error=str(e))
|
|
156
|
-
return {}
|
|
157
|
-
|
|
158
142
|
# Check for user-specific configuration files
|
|
159
143
|
user_envs_dir = Path.home() / ".devs" / "envs"
|
|
160
144
|
default_devs_yml = user_envs_dir / "default" / "DEVS.yml"
|
|
@@ -165,69 +149,26 @@ class ContainerPool:
|
|
|
165
149
|
if not default_devs_yml.exists() and not project_devs_yml.exists():
|
|
166
150
|
return None
|
|
167
151
|
|
|
168
|
-
# Load user
|
|
169
|
-
|
|
152
|
+
# Load configuration without repository files (user configs only)
|
|
153
|
+
# Create a fake path that doesn't exist to skip repository DEVS.yml
|
|
154
|
+
fake_repo_path = Path("/dev/null/fake_repo_path")
|
|
155
|
+
devs_options = DevsConfigLoader.load(project_name=project_name, repo_path=fake_repo_path)
|
|
170
156
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
all_data.update(default_data)
|
|
180
|
-
all_data.update(project_data)
|
|
181
|
-
|
|
182
|
-
# Update devs_options with merged values
|
|
183
|
-
if all_data:
|
|
184
|
-
if 'default_branch' in all_data:
|
|
185
|
-
devs_options.default_branch = all_data['default_branch']
|
|
186
|
-
if 'prompt_extra' in all_data:
|
|
187
|
-
devs_options.prompt_extra = all_data['prompt_extra']
|
|
188
|
-
if 'prompt_override' in all_data:
|
|
189
|
-
devs_options.prompt_override = all_data['prompt_override']
|
|
190
|
-
if 'direct_commit' in all_data:
|
|
191
|
-
devs_options.direct_commit = all_data['direct_commit']
|
|
192
|
-
if 'single_queue' in all_data:
|
|
193
|
-
devs_options.single_queue = all_data['single_queue']
|
|
194
|
-
if 'ci_enabled' in all_data:
|
|
195
|
-
devs_options.ci_enabled = all_data['ci_enabled']
|
|
196
|
-
if 'ci_test_command' in all_data:
|
|
197
|
-
devs_options.ci_test_command = all_data['ci_test_command']
|
|
198
|
-
if 'ci_branches' in all_data:
|
|
199
|
-
devs_options.ci_branches = all_data['ci_branches']
|
|
200
|
-
|
|
201
|
-
# Merge env_vars from both sources
|
|
202
|
-
merged_env_vars = {}
|
|
203
|
-
for source_data in [default_data, project_data]:
|
|
204
|
-
if 'env_vars' in source_data and source_data['env_vars']:
|
|
205
|
-
for container_name, env_dict in source_data['env_vars'].items():
|
|
206
|
-
if container_name not in merged_env_vars:
|
|
207
|
-
merged_env_vars[container_name] = {}
|
|
208
|
-
merged_env_vars[container_name].update(env_dict)
|
|
209
|
-
|
|
210
|
-
if merged_env_vars:
|
|
211
|
-
devs_options.env_vars = merged_env_vars
|
|
212
|
-
|
|
213
|
-
logger.info("Loaded user-specific DEVS.yml configuration",
|
|
214
|
-
repo=repo_name,
|
|
215
|
-
default_file_exists=default_devs_yml.exists(),
|
|
216
|
-
project_file_exists=project_devs_yml.exists(),
|
|
217
|
-
default_branch=devs_options.default_branch,
|
|
218
|
-
single_queue=devs_options.single_queue,
|
|
219
|
-
ci_enabled=devs_options.ci_enabled,
|
|
220
|
-
env_vars_containers=list(devs_options.env_vars.keys()) if devs_options.env_vars else [])
|
|
221
|
-
|
|
222
|
-
return devs_options
|
|
157
|
+
logger.info("Loaded user-specific DEVS.yml configuration",
|
|
158
|
+
repo=repo_name,
|
|
159
|
+
default_file_exists=default_devs_yml.exists(),
|
|
160
|
+
project_file_exists=project_devs_yml.exists(),
|
|
161
|
+
default_branch=devs_options.default_branch,
|
|
162
|
+
single_queue=devs_options.single_queue,
|
|
163
|
+
ci_enabled=devs_options.ci_enabled,
|
|
164
|
+
env_vars_containers=list(devs_options.env_vars.keys()) if devs_options.env_vars else [])
|
|
223
165
|
|
|
224
|
-
|
|
225
|
-
return None
|
|
166
|
+
return devs_options
|
|
226
167
|
|
|
227
168
|
def _read_devs_options(self, repo_path: Path, repo_name: str) -> DevsOptions:
|
|
228
169
|
"""Read and parse DEVS.yml options from multiple sources.
|
|
229
170
|
|
|
230
|
-
|
|
171
|
+
Uses the shared DevsConfigLoader to load from:
|
|
231
172
|
1. ~/.devs/envs/{org-repo}/DEVS.yml (user-specific overrides)
|
|
232
173
|
2. ~/.devs/envs/default/DEVS.yml (user defaults)
|
|
233
174
|
3. {repo_path}/DEVS.yml (repository configuration)
|
|
@@ -239,87 +180,29 @@ class ContainerPool:
|
|
|
239
180
|
Returns:
|
|
240
181
|
DevsOptions with values from DEVS.yml files or defaults
|
|
241
182
|
"""
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
def _load_devs_yml(file_path: Path) -> dict:
|
|
245
|
-
"""Load and parse a DEVS.yml file, returning empty dict if not found."""
|
|
246
|
-
if not file_path.exists():
|
|
247
|
-
return {}
|
|
248
|
-
|
|
249
|
-
try:
|
|
250
|
-
with open(file_path, 'r') as f:
|
|
251
|
-
data = yaml.safe_load(f)
|
|
252
|
-
return data if data else {}
|
|
253
|
-
except Exception as e:
|
|
254
|
-
logger.warning("Failed to parse DEVS.yml",
|
|
255
|
-
file_path=str(file_path),
|
|
256
|
-
error=str(e))
|
|
257
|
-
return {}
|
|
258
|
-
|
|
259
|
-
# 1. Load repository DEVS.yml (lowest priority)
|
|
260
|
-
repo_devs_yml = repo_path / "DEVS.yml"
|
|
261
|
-
repo_data = _load_devs_yml(repo_devs_yml)
|
|
183
|
+
project_name = repo_name.replace('/', '-') # Convert org/repo to org-repo
|
|
184
|
+
devs_options = DevsConfigLoader.load(project_name=project_name, repo_path=repo_path)
|
|
262
185
|
|
|
263
|
-
#
|
|
186
|
+
# Check which files exist for logging
|
|
264
187
|
user_envs_dir = Path.home() / ".devs" / "envs"
|
|
188
|
+
repo_devs_yml = repo_path / "DEVS.yml"
|
|
265
189
|
default_devs_yml = user_envs_dir / "default" / "DEVS.yml"
|
|
266
|
-
default_data = _load_devs_yml(default_devs_yml)
|
|
267
|
-
|
|
268
|
-
# 3. Load user project-specific DEVS.yml (highest priority)
|
|
269
|
-
project_name = repo_name.replace('/', '-') # Convert org/repo to org-repo
|
|
270
190
|
project_devs_yml = user_envs_dir / project_name / "DEVS.yml"
|
|
271
|
-
project_data = _load_devs_yml(project_devs_yml)
|
|
272
|
-
|
|
273
|
-
# Merge data in priority order (later updates override earlier ones)
|
|
274
|
-
all_data = {}
|
|
275
|
-
for source_data in [repo_data, default_data, project_data]:
|
|
276
|
-
all_data.update(source_data)
|
|
277
191
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if 'ci_test_command' in all_data:
|
|
293
|
-
devs_options.ci_test_command = all_data['ci_test_command']
|
|
294
|
-
if 'ci_branches' in all_data:
|
|
295
|
-
devs_options.ci_branches = all_data['ci_branches']
|
|
296
|
-
|
|
297
|
-
# Merge env_vars from all sources (repository < default < project)
|
|
298
|
-
merged_env_vars = {}
|
|
299
|
-
for source_data in [repo_data, default_data, project_data]:
|
|
300
|
-
if 'env_vars' in source_data and source_data['env_vars']:
|
|
301
|
-
for container_name, env_dict in source_data['env_vars'].items():
|
|
302
|
-
if container_name not in merged_env_vars:
|
|
303
|
-
merged_env_vars[container_name] = {}
|
|
304
|
-
merged_env_vars[container_name].update(env_dict)
|
|
305
|
-
|
|
306
|
-
if merged_env_vars:
|
|
307
|
-
devs_options.env_vars = merged_env_vars
|
|
308
|
-
|
|
309
|
-
logger.info("Loaded DEVS.yml configuration from multiple sources",
|
|
310
|
-
repo=repo_name,
|
|
311
|
-
repo_file_exists=repo_devs_yml.exists(),
|
|
312
|
-
default_file_exists=default_devs_yml.exists(),
|
|
313
|
-
project_file_exists=project_devs_yml.exists(),
|
|
314
|
-
default_branch=devs_options.default_branch,
|
|
315
|
-
has_prompt_extra=bool(devs_options.prompt_extra),
|
|
316
|
-
has_prompt_override=bool(devs_options.prompt_override),
|
|
317
|
-
direct_commit=devs_options.direct_commit,
|
|
318
|
-
single_queue=devs_options.single_queue,
|
|
319
|
-
ci_enabled=devs_options.ci_enabled,
|
|
320
|
-
ci_test_command=devs_options.ci_test_command,
|
|
321
|
-
ci_branches=devs_options.ci_branches,
|
|
322
|
-
env_vars_containers=list(devs_options.env_vars.keys()) if devs_options.env_vars else [])
|
|
192
|
+
logger.info("Loaded DEVS.yml configuration from multiple sources",
|
|
193
|
+
repo=repo_name,
|
|
194
|
+
repo_file_exists=repo_devs_yml.exists(),
|
|
195
|
+
default_file_exists=default_devs_yml.exists(),
|
|
196
|
+
project_file_exists=project_devs_yml.exists(),
|
|
197
|
+
default_branch=devs_options.default_branch,
|
|
198
|
+
has_prompt_extra=bool(devs_options.prompt_extra),
|
|
199
|
+
has_prompt_override=bool(devs_options.prompt_override),
|
|
200
|
+
direct_commit=devs_options.direct_commit,
|
|
201
|
+
single_queue=devs_options.single_queue,
|
|
202
|
+
ci_enabled=devs_options.ci_enabled,
|
|
203
|
+
ci_test_command=devs_options.ci_test_command,
|
|
204
|
+
ci_branches=devs_options.ci_branches,
|
|
205
|
+
env_vars_containers=list(devs_options.env_vars.keys()) if devs_options.env_vars else [])
|
|
323
206
|
|
|
324
207
|
return devs_options
|
|
325
208
|
|