claude-mpm 4.3.20__py3-none-any.whl → 4.4.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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/agent_loader.py +2 -2
- claude_mpm/agents/agent_loader_integration.py +2 -2
- claude_mpm/agents/async_agent_loader.py +2 -2
- claude_mpm/agents/base_agent_loader.py +2 -2
- claude_mpm/agents/frontmatter_validator.py +2 -2
- claude_mpm/agents/system_agent_config.py +2 -2
- claude_mpm/agents/templates/data_engineer.json +1 -2
- claude_mpm/cli/commands/doctor.py +2 -2
- claude_mpm/cli/commands/mpm_init.py +560 -47
- claude_mpm/cli/commands/mpm_init_handler.py +6 -0
- claude_mpm/cli/parsers/mpm_init_parser.py +39 -1
- claude_mpm/cli/startup_logging.py +11 -9
- claude_mpm/commands/mpm-init.md +76 -12
- claude_mpm/config/agent_config.py +2 -2
- claude_mpm/config/paths.py +2 -2
- claude_mpm/core/agent_name_normalizer.py +2 -2
- claude_mpm/core/config.py +2 -1
- claude_mpm/core/config_aliases.py +2 -2
- claude_mpm/core/file_utils.py +1 -0
- claude_mpm/core/log_manager.py +2 -2
- claude_mpm/core/tool_access_control.py +2 -2
- claude_mpm/core/unified_agent_registry.py +2 -2
- claude_mpm/core/unified_paths.py +2 -2
- claude_mpm/experimental/cli_enhancements.py +3 -2
- claude_mpm/hooks/base_hook.py +2 -2
- claude_mpm/hooks/instruction_reinforcement.py +2 -2
- claude_mpm/hooks/memory_integration_hook.py +1 -1
- claude_mpm/hooks/validation_hooks.py +2 -2
- claude_mpm/scripts/mpm_doctor.py +2 -2
- claude_mpm/services/agents/loading/agent_profile_loader.py +2 -2
- claude_mpm/services/agents/loading/base_agent_manager.py +2 -2
- claude_mpm/services/agents/loading/framework_agent_loader.py +2 -2
- claude_mpm/services/agents/management/agent_capabilities_generator.py +2 -2
- claude_mpm/services/agents/management/agent_management_service.py +2 -2
- claude_mpm/services/agents/memory/content_manager.py +5 -2
- claude_mpm/services/agents/memory/memory_categorization_service.py +5 -2
- claude_mpm/services/agents/memory/memory_file_service.py +28 -6
- claude_mpm/services/agents/memory/memory_format_service.py +5 -2
- claude_mpm/services/agents/memory/memory_limits_service.py +4 -2
- claude_mpm/services/agents/registry/deployed_agent_discovery.py +2 -2
- claude_mpm/services/agents/registry/modification_tracker.py +4 -4
- claude_mpm/services/async_session_logger.py +2 -1
- claude_mpm/services/claude_session_logger.py +2 -2
- claude_mpm/services/core/path_resolver.py +3 -2
- claude_mpm/services/diagnostics/diagnostic_runner.py +4 -3
- claude_mpm/services/event_bus/direct_relay.py +2 -1
- claude_mpm/services/event_bus/event_bus.py +2 -1
- claude_mpm/services/event_bus/relay.py +2 -2
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +2 -2
- claude_mpm/services/infrastructure/daemon_manager.py +2 -2
- claude_mpm/services/memory/cache/simple_cache.py +2 -2
- claude_mpm/services/project/archive_manager.py +981 -0
- claude_mpm/services/project/documentation_manager.py +536 -0
- claude_mpm/services/project/enhanced_analyzer.py +491 -0
- claude_mpm/services/project/project_organizer.py +904 -0
- claude_mpm/services/response_tracker.py +2 -2
- claude_mpm/services/socketio/handlers/connection.py +14 -33
- claude_mpm/services/socketio/server/eventbus_integration.py +2 -2
- claude_mpm/services/unified/__init__.py +65 -0
- claude_mpm/services/unified/analyzer_strategies/__init__.py +44 -0
- claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +473 -0
- claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +643 -0
- claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +804 -0
- claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +661 -0
- claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +696 -0
- claude_mpm/services/unified/deployment_strategies/__init__.py +97 -0
- claude_mpm/services/unified/deployment_strategies/base.py +557 -0
- claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +486 -0
- claude_mpm/services/unified/deployment_strategies/local.py +594 -0
- claude_mpm/services/unified/deployment_strategies/utils.py +672 -0
- claude_mpm/services/unified/deployment_strategies/vercel.py +471 -0
- claude_mpm/services/unified/interfaces.py +499 -0
- claude_mpm/services/unified/migration.py +532 -0
- claude_mpm/services/unified/strategies.py +551 -0
- claude_mpm/services/unified/unified_analyzer.py +534 -0
- claude_mpm/services/unified/unified_config.py +688 -0
- claude_mpm/services/unified/unified_deployment.py +470 -0
- claude_mpm/services/version_control/version_parser.py +5 -4
- claude_mpm/storage/state_storage.py +2 -2
- claude_mpm/utils/agent_dependency_loader.py +49 -0
- claude_mpm/utils/common.py +542 -0
- claude_mpm/utils/database_connector.py +298 -0
- claude_mpm/utils/error_handler.py +2 -1
- claude_mpm/utils/log_cleanup.py +2 -2
- claude_mpm/utils/path_operations.py +2 -2
- claude_mpm/utils/robust_installer.py +56 -0
- claude_mpm/utils/session_logging.py +2 -2
- claude_mpm/utils/subprocess_utils.py +2 -2
- claude_mpm/validation/agent_validator.py +2 -2
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.4.0.dist-info}/METADATA +1 -1
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.4.0.dist-info}/RECORD +96 -71
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.4.0.dist-info}/WHEEL +0 -0
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.4.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.4.0.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.3.20.dist-info → claude_mpm-4.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,486 @@
|
|
1
|
+
"""
|
2
|
+
Cloud Deployment Strategies
|
3
|
+
===========================
|
4
|
+
|
5
|
+
Consolidated cloud deployment strategies for Railway, AWS, Docker, and Git.
|
6
|
+
Reduces duplication by sharing common cloud deployment patterns.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import json
|
10
|
+
import subprocess
|
11
|
+
from datetime import datetime
|
12
|
+
from pathlib import Path
|
13
|
+
from typing import Any, Dict, List, Optional
|
14
|
+
|
15
|
+
from claude_mpm.core.logging_utils import get_logger
|
16
|
+
from claude_mpm.services.unified.strategies import StrategyMetadata, StrategyPriority
|
17
|
+
|
18
|
+
from .base import DeploymentContext, DeploymentResult, DeploymentStrategy
|
19
|
+
from .utils import (
|
20
|
+
check_docker_container,
|
21
|
+
prepare_deployment_artifact,
|
22
|
+
rollback_docker_deployment,
|
23
|
+
verify_deployment_health,
|
24
|
+
)
|
25
|
+
|
26
|
+
|
27
|
+
class RailwayDeploymentStrategy(DeploymentStrategy):
|
28
|
+
"""Deploy to Railway platform."""
|
29
|
+
|
30
|
+
def __init__(self):
|
31
|
+
"""Initialize Railway strategy."""
|
32
|
+
super().__init__(StrategyMetadata(
|
33
|
+
name="RailwayDeploymentStrategy",
|
34
|
+
description="Deploy to Railway cloud platform",
|
35
|
+
supported_types=["application", "service", "*"],
|
36
|
+
supported_operations=["deploy", "rollback", "verify"],
|
37
|
+
priority=StrategyPriority.NORMAL,
|
38
|
+
tags={"railway", "cloud", "paas"},
|
39
|
+
))
|
40
|
+
self._logger = get_logger(f"{__name__}.RailwayDeploymentStrategy")
|
41
|
+
|
42
|
+
def validate(self, context: DeploymentContext) -> List[str]:
|
43
|
+
"""Validate Railway deployment."""
|
44
|
+
errors = []
|
45
|
+
|
46
|
+
# Check Railway CLI
|
47
|
+
try:
|
48
|
+
subprocess.run(["railway", "--version"], capture_output=True, check=True)
|
49
|
+
except:
|
50
|
+
errors.append("Railway CLI not installed. Install with: npm i -g @railway/cli")
|
51
|
+
|
52
|
+
# Check authentication
|
53
|
+
try:
|
54
|
+
subprocess.run(["railway", "whoami"], capture_output=True, check=True)
|
55
|
+
except:
|
56
|
+
errors.append("Not authenticated with Railway. Run: railway login")
|
57
|
+
|
58
|
+
return errors
|
59
|
+
|
60
|
+
def prepare(self, context: DeploymentContext) -> List[Path]:
|
61
|
+
"""Prepare Railway artifacts."""
|
62
|
+
artifact_path, metadata = prepare_deployment_artifact(
|
63
|
+
context.source, "directory", context.config
|
64
|
+
)
|
65
|
+
return [artifact_path]
|
66
|
+
|
67
|
+
def execute(self, context: DeploymentContext, artifacts: List[Path]) -> Dict[str, Any]:
|
68
|
+
"""Execute Railway deployment."""
|
69
|
+
deploy_dir = artifacts[0] if artifacts else Path(context.source)
|
70
|
+
|
71
|
+
cmd = ["railway", "up"]
|
72
|
+
if context.config.get("service"):
|
73
|
+
cmd.extend(["--service", context.config["service"]])
|
74
|
+
if context.config.get("environment"):
|
75
|
+
cmd.extend(["--environment", context.config["environment"]])
|
76
|
+
|
77
|
+
try:
|
78
|
+
result = subprocess.run(cmd, cwd=deploy_dir, capture_output=True, text=True, check=True)
|
79
|
+
|
80
|
+
# Parse deployment URL from output
|
81
|
+
deployment_url = None
|
82
|
+
for line in result.stdout.split("\n"):
|
83
|
+
if "https://" in line:
|
84
|
+
import re
|
85
|
+
match = re.search(r"https://[^\s]+", line)
|
86
|
+
if match:
|
87
|
+
deployment_url = match.group(0)
|
88
|
+
break
|
89
|
+
|
90
|
+
return {
|
91
|
+
"deployment_id": f"railway_{datetime.now().timestamp()}",
|
92
|
+
"deployment_url": deployment_url,
|
93
|
+
"deployed_path": deploy_dir,
|
94
|
+
"stdout": result.stdout,
|
95
|
+
}
|
96
|
+
except subprocess.CalledProcessError as e:
|
97
|
+
raise Exception(f"Railway deployment failed: {e.stderr}")
|
98
|
+
|
99
|
+
def verify(self, context: DeploymentContext, deployment_info: Dict[str, Any]) -> bool:
|
100
|
+
"""Verify Railway deployment."""
|
101
|
+
return verify_deployment_health(
|
102
|
+
"railway", deployment_info, ["accessibility"]
|
103
|
+
)["status"] == "healthy"
|
104
|
+
|
105
|
+
def rollback(self, context: DeploymentContext, result: DeploymentResult) -> bool:
|
106
|
+
"""Railway doesn't support CLI rollback."""
|
107
|
+
self._logger.warning("Railway rollback must be done via dashboard")
|
108
|
+
return False
|
109
|
+
|
110
|
+
def get_health_status(self, deployment_info: Dict[str, Any]) -> Dict[str, Any]:
|
111
|
+
"""Get Railway deployment health."""
|
112
|
+
return verify_deployment_health("railway", deployment_info)
|
113
|
+
|
114
|
+
|
115
|
+
class AWSDeploymentStrategy(DeploymentStrategy):
|
116
|
+
"""Deploy to AWS (Lambda, EC2, ECS)."""
|
117
|
+
|
118
|
+
def __init__(self):
|
119
|
+
"""Initialize AWS strategy."""
|
120
|
+
super().__init__(StrategyMetadata(
|
121
|
+
name="AWSDeploymentStrategy",
|
122
|
+
description="Deploy to AWS services",
|
123
|
+
supported_types=["lambda", "ec2", "ecs", "application", "*"],
|
124
|
+
supported_operations=["deploy", "rollback", "verify"],
|
125
|
+
priority=StrategyPriority.NORMAL,
|
126
|
+
tags={"aws", "cloud", "serverless"},
|
127
|
+
))
|
128
|
+
self._logger = get_logger(f"{__name__}.AWSDeploymentStrategy")
|
129
|
+
|
130
|
+
def validate(self, context: DeploymentContext) -> List[str]:
|
131
|
+
"""Validate AWS deployment."""
|
132
|
+
errors = []
|
133
|
+
|
134
|
+
# Check AWS CLI
|
135
|
+
try:
|
136
|
+
subprocess.run(["aws", "--version"], capture_output=True, check=True)
|
137
|
+
except:
|
138
|
+
errors.append("AWS CLI not installed")
|
139
|
+
|
140
|
+
# Check credentials
|
141
|
+
try:
|
142
|
+
subprocess.run(["aws", "sts", "get-caller-identity"], capture_output=True, check=True)
|
143
|
+
except:
|
144
|
+
errors.append("AWS credentials not configured")
|
145
|
+
|
146
|
+
# Validate service type
|
147
|
+
service = context.config.get("service", "lambda")
|
148
|
+
if service not in ["lambda", "ec2", "ecs", "s3"]:
|
149
|
+
errors.append(f"Unsupported AWS service: {service}")
|
150
|
+
|
151
|
+
return errors
|
152
|
+
|
153
|
+
def prepare(self, context: DeploymentContext) -> List[Path]:
|
154
|
+
"""Prepare AWS deployment artifacts."""
|
155
|
+
service = context.config.get("service", "lambda")
|
156
|
+
|
157
|
+
if service == "lambda":
|
158
|
+
# Create ZIP for Lambda
|
159
|
+
artifact_path, _ = prepare_deployment_artifact(
|
160
|
+
context.source, "zip", context.config
|
161
|
+
)
|
162
|
+
return [artifact_path]
|
163
|
+
else:
|
164
|
+
artifact_path, _ = prepare_deployment_artifact(
|
165
|
+
context.source, "directory", context.config
|
166
|
+
)
|
167
|
+
return [artifact_path]
|
168
|
+
|
169
|
+
def execute(self, context: DeploymentContext, artifacts: List[Path]) -> Dict[str, Any]:
|
170
|
+
"""Execute AWS deployment."""
|
171
|
+
service = context.config.get("service", "lambda")
|
172
|
+
|
173
|
+
if service == "lambda":
|
174
|
+
return self._deploy_lambda(context, artifacts[0])
|
175
|
+
elif service == "s3":
|
176
|
+
return self._deploy_s3(context, artifacts[0])
|
177
|
+
else:
|
178
|
+
raise NotImplementedError(f"AWS {service} deployment not implemented")
|
179
|
+
|
180
|
+
def _deploy_lambda(self, context: DeploymentContext, artifact: Path) -> Dict[str, Any]:
|
181
|
+
"""Deploy AWS Lambda function."""
|
182
|
+
function_name = context.config.get("function_name", artifact.stem)
|
183
|
+
|
184
|
+
# Check if function exists
|
185
|
+
try:
|
186
|
+
subprocess.run(
|
187
|
+
["aws", "lambda", "get-function", "--function-name", function_name],
|
188
|
+
capture_output=True, check=True
|
189
|
+
)
|
190
|
+
# Update existing function
|
191
|
+
cmd = [
|
192
|
+
"aws", "lambda", "update-function-code",
|
193
|
+
"--function-name", function_name,
|
194
|
+
"--zip-file", f"fileb://{artifact}"
|
195
|
+
]
|
196
|
+
except:
|
197
|
+
# Create new function
|
198
|
+
cmd = [
|
199
|
+
"aws", "lambda", "create-function",
|
200
|
+
"--function-name", function_name,
|
201
|
+
"--runtime", context.config.get("runtime", "python3.9"),
|
202
|
+
"--role", context.config.get("role"),
|
203
|
+
"--handler", context.config.get("handler", "index.handler"),
|
204
|
+
"--zip-file", f"fileb://{artifact}"
|
205
|
+
]
|
206
|
+
|
207
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
208
|
+
response = json.loads(result.stdout)
|
209
|
+
|
210
|
+
return {
|
211
|
+
"deployment_id": response.get("FunctionArn"),
|
212
|
+
"deployment_url": response.get("FunctionUrl"),
|
213
|
+
"deployed_path": artifact,
|
214
|
+
"function_arn": response.get("FunctionArn"),
|
215
|
+
}
|
216
|
+
|
217
|
+
def _deploy_s3(self, context: DeploymentContext, artifact: Path) -> Dict[str, Any]:
|
218
|
+
"""Deploy to S3 bucket."""
|
219
|
+
bucket = context.config.get("bucket")
|
220
|
+
prefix = context.config.get("prefix", "")
|
221
|
+
|
222
|
+
cmd = ["aws", "s3", "sync", str(artifact), f"s3://{bucket}/{prefix}"]
|
223
|
+
subprocess.run(cmd, check=True)
|
224
|
+
|
225
|
+
return {
|
226
|
+
"deployment_id": f"s3://{bucket}/{prefix}",
|
227
|
+
"deployment_url": f"https://{bucket}.s3.amazonaws.com/{prefix}",
|
228
|
+
"deployed_path": artifact,
|
229
|
+
}
|
230
|
+
|
231
|
+
def verify(self, context: DeploymentContext, deployment_info: Dict[str, Any]) -> bool:
|
232
|
+
"""Verify AWS deployment."""
|
233
|
+
service = context.config.get("service", "lambda")
|
234
|
+
|
235
|
+
if service == "lambda" and "function_arn" in deployment_info:
|
236
|
+
try:
|
237
|
+
cmd = ["aws", "lambda", "get-function", "--function-name",
|
238
|
+
deployment_info["function_arn"]]
|
239
|
+
subprocess.run(cmd, capture_output=True, check=True)
|
240
|
+
return True
|
241
|
+
except:
|
242
|
+
return False
|
243
|
+
|
244
|
+
return True
|
245
|
+
|
246
|
+
def rollback(self, context: DeploymentContext, result: DeploymentResult) -> bool:
|
247
|
+
"""Rollback AWS deployment."""
|
248
|
+
# AWS Lambda supports versioning for rollback
|
249
|
+
if context.config.get("service") == "lambda" and result.previous_version:
|
250
|
+
function_name = context.config.get("function_name")
|
251
|
+
cmd = [
|
252
|
+
"aws", "lambda", "update-alias",
|
253
|
+
"--function-name", function_name,
|
254
|
+
"--name", "PROD",
|
255
|
+
"--function-version", result.previous_version
|
256
|
+
]
|
257
|
+
try:
|
258
|
+
subprocess.run(cmd, check=True)
|
259
|
+
return True
|
260
|
+
except:
|
261
|
+
pass
|
262
|
+
return False
|
263
|
+
|
264
|
+
def get_health_status(self, deployment_info: Dict[str, Any]) -> Dict[str, Any]:
|
265
|
+
"""Get AWS deployment health."""
|
266
|
+
return verify_deployment_health("aws", deployment_info)
|
267
|
+
|
268
|
+
|
269
|
+
class DockerDeploymentStrategy(DeploymentStrategy):
|
270
|
+
"""Deploy using Docker containers."""
|
271
|
+
|
272
|
+
def __init__(self):
|
273
|
+
"""Initialize Docker strategy."""
|
274
|
+
super().__init__(StrategyMetadata(
|
275
|
+
name="DockerDeploymentStrategy",
|
276
|
+
description="Deploy using Docker containers",
|
277
|
+
supported_types=["container", "application", "service", "*"],
|
278
|
+
supported_operations=["deploy", "rollback", "verify", "stop"],
|
279
|
+
priority=StrategyPriority.HIGH,
|
280
|
+
tags={"docker", "container", "microservice"},
|
281
|
+
))
|
282
|
+
self._logger = get_logger(f"{__name__}.DockerDeploymentStrategy")
|
283
|
+
|
284
|
+
def validate(self, context: DeploymentContext) -> List[str]:
|
285
|
+
"""Validate Docker deployment."""
|
286
|
+
errors = []
|
287
|
+
|
288
|
+
# Check Docker
|
289
|
+
try:
|
290
|
+
subprocess.run(["docker", "--version"], capture_output=True, check=True)
|
291
|
+
except:
|
292
|
+
errors.append("Docker not installed or not running")
|
293
|
+
|
294
|
+
# Check Dockerfile exists
|
295
|
+
source_path = Path(context.source)
|
296
|
+
if source_path.is_dir():
|
297
|
+
dockerfile = source_path / "Dockerfile"
|
298
|
+
if not dockerfile.exists():
|
299
|
+
errors.append(f"No Dockerfile found in {source_path}")
|
300
|
+
|
301
|
+
return errors
|
302
|
+
|
303
|
+
def prepare(self, context: DeploymentContext) -> List[Path]:
|
304
|
+
"""Prepare Docker artifacts."""
|
305
|
+
return [Path(context.source)]
|
306
|
+
|
307
|
+
def execute(self, context: DeploymentContext, artifacts: List[Path]) -> Dict[str, Any]:
|
308
|
+
"""Execute Docker deployment."""
|
309
|
+
source_dir = artifacts[0] if artifacts[0].is_dir() else artifacts[0].parent
|
310
|
+
image_name = context.config.get("image_name", f"app_{datetime.now().timestamp()}")
|
311
|
+
container_name = context.config.get("container_name", image_name)
|
312
|
+
|
313
|
+
# Build image
|
314
|
+
build_cmd = ["docker", "build", "-t", image_name, str(source_dir)]
|
315
|
+
subprocess.run(build_cmd, check=True)
|
316
|
+
|
317
|
+
# Stop existing container if exists
|
318
|
+
subprocess.run(["docker", "stop", container_name], capture_output=True, check=False)
|
319
|
+
subprocess.run(["docker", "rm", container_name], capture_output=True, check=False)
|
320
|
+
|
321
|
+
# Run container
|
322
|
+
run_cmd = ["docker", "run", "-d", "--name", container_name]
|
323
|
+
|
324
|
+
# Add port mapping
|
325
|
+
if "ports" in context.config:
|
326
|
+
for port_map in context.config["ports"]:
|
327
|
+
run_cmd.extend(["-p", port_map])
|
328
|
+
|
329
|
+
# Add environment variables
|
330
|
+
if "env" in context.config:
|
331
|
+
for key, value in context.config["env"].items():
|
332
|
+
run_cmd.extend(["-e", f"{key}={value}"])
|
333
|
+
|
334
|
+
run_cmd.append(image_name)
|
335
|
+
|
336
|
+
result = subprocess.run(run_cmd, capture_output=True, text=True, check=True)
|
337
|
+
container_id = result.stdout.strip()
|
338
|
+
|
339
|
+
return {
|
340
|
+
"deployment_id": container_id[:12],
|
341
|
+
"container_id": container_id,
|
342
|
+
"image_name": image_name,
|
343
|
+
"container_name": container_name,
|
344
|
+
"deployed_path": source_dir,
|
345
|
+
}
|
346
|
+
|
347
|
+
def verify(self, context: DeploymentContext, deployment_info: Dict[str, Any]) -> bool:
|
348
|
+
"""Verify Docker deployment."""
|
349
|
+
return check_docker_container(deployment_info.get("container_id"))
|
350
|
+
|
351
|
+
def rollback(self, context: DeploymentContext, result: DeploymentResult) -> bool:
|
352
|
+
"""Rollback Docker deployment."""
|
353
|
+
return rollback_docker_deployment(result.to_dict())
|
354
|
+
|
355
|
+
def get_health_status(self, deployment_info: Dict[str, Any]) -> Dict[str, Any]:
|
356
|
+
"""Get Docker container health."""
|
357
|
+
container_id = deployment_info.get("container_id")
|
358
|
+
health = {"status": "unknown", "container_id": container_id}
|
359
|
+
|
360
|
+
if container_id:
|
361
|
+
health["running"] = check_docker_container(container_id)
|
362
|
+
health["status"] = "healthy" if health["running"] else "unhealthy"
|
363
|
+
|
364
|
+
return health
|
365
|
+
|
366
|
+
|
367
|
+
class GitDeploymentStrategy(DeploymentStrategy):
|
368
|
+
"""Deploy using Git (GitHub, GitLab)."""
|
369
|
+
|
370
|
+
def __init__(self):
|
371
|
+
"""Initialize Git strategy."""
|
372
|
+
super().__init__(StrategyMetadata(
|
373
|
+
name="GitDeploymentStrategy",
|
374
|
+
description="Deploy using Git repositories",
|
375
|
+
supported_types=["repository", "code", "*"],
|
376
|
+
supported_operations=["deploy", "rollback", "verify"],
|
377
|
+
priority=StrategyPriority.NORMAL,
|
378
|
+
tags={"git", "github", "gitlab", "version-control"},
|
379
|
+
))
|
380
|
+
self._logger = get_logger(f"{__name__}.GitDeploymentStrategy")
|
381
|
+
|
382
|
+
def validate(self, context: DeploymentContext) -> List[str]:
|
383
|
+
"""Validate Git deployment."""
|
384
|
+
errors = []
|
385
|
+
|
386
|
+
# Check Git
|
387
|
+
try:
|
388
|
+
subprocess.run(["git", "--version"], capture_output=True, check=True)
|
389
|
+
except:
|
390
|
+
errors.append("Git not installed")
|
391
|
+
|
392
|
+
# Check remote URL
|
393
|
+
if not context.config.get("remote_url"):
|
394
|
+
errors.append("Git remote URL required")
|
395
|
+
|
396
|
+
return errors
|
397
|
+
|
398
|
+
def prepare(self, context: DeploymentContext) -> List[Path]:
|
399
|
+
"""Prepare Git artifacts."""
|
400
|
+
return [Path(context.source)]
|
401
|
+
|
402
|
+
def execute(self, context: DeploymentContext, artifacts: List[Path]) -> Dict[str, Any]:
|
403
|
+
"""Execute Git deployment."""
|
404
|
+
source_dir = artifacts[0] if artifacts[0].is_dir() else artifacts[0].parent
|
405
|
+
remote_url = context.config.get("remote_url")
|
406
|
+
branch = context.config.get("branch", "main")
|
407
|
+
|
408
|
+
# Initialize git if needed
|
409
|
+
if not (source_dir / ".git").exists():
|
410
|
+
subprocess.run(["git", "init"], cwd=source_dir, check=True)
|
411
|
+
|
412
|
+
# Add remote
|
413
|
+
subprocess.run(
|
414
|
+
["git", "remote", "add", "deploy", remote_url],
|
415
|
+
cwd=source_dir, capture_output=True, check=False
|
416
|
+
)
|
417
|
+
|
418
|
+
# Add all files
|
419
|
+
subprocess.run(["git", "add", "."], cwd=source_dir, check=True)
|
420
|
+
|
421
|
+
# Commit
|
422
|
+
commit_msg = context.config.get("commit_message", "Deploy via Claude MPM")
|
423
|
+
subprocess.run(
|
424
|
+
["git", "commit", "-m", commit_msg],
|
425
|
+
cwd=source_dir, capture_output=True, check=False
|
426
|
+
)
|
427
|
+
|
428
|
+
# Push
|
429
|
+
subprocess.run(
|
430
|
+
["git", "push", "-u", "deploy", branch],
|
431
|
+
cwd=source_dir, check=True
|
432
|
+
)
|
433
|
+
|
434
|
+
# Get commit hash
|
435
|
+
result = subprocess.run(
|
436
|
+
["git", "rev-parse", "HEAD"],
|
437
|
+
cwd=source_dir, capture_output=True, text=True, check=True
|
438
|
+
)
|
439
|
+
commit_hash = result.stdout.strip()
|
440
|
+
|
441
|
+
return {
|
442
|
+
"deployment_id": commit_hash[:8],
|
443
|
+
"commit_hash": commit_hash,
|
444
|
+
"remote_url": remote_url,
|
445
|
+
"branch": branch,
|
446
|
+
"deployed_path": source_dir,
|
447
|
+
}
|
448
|
+
|
449
|
+
def verify(self, context: DeploymentContext, deployment_info: Dict[str, Any]) -> bool:
|
450
|
+
"""Verify Git deployment."""
|
451
|
+
# Check if commit exists on remote
|
452
|
+
try:
|
453
|
+
subprocess.run(
|
454
|
+
["git", "ls-remote", deployment_info.get("remote_url"),
|
455
|
+
deployment_info.get("commit_hash")],
|
456
|
+
capture_output=True, check=True
|
457
|
+
)
|
458
|
+
return True
|
459
|
+
except:
|
460
|
+
return False
|
461
|
+
|
462
|
+
def rollback(self, context: DeploymentContext, result: DeploymentResult) -> bool:
|
463
|
+
"""Rollback Git deployment."""
|
464
|
+
if result.previous_version:
|
465
|
+
try:
|
466
|
+
subprocess.run(
|
467
|
+
["git", "checkout", result.previous_version],
|
468
|
+
cwd=result.deployed_path, check=True
|
469
|
+
)
|
470
|
+
subprocess.run(
|
471
|
+
["git", "push", "--force", "deploy",
|
472
|
+
f"{result.previous_version}:{context.config.get('branch', 'main')}"],
|
473
|
+
cwd=result.deployed_path, check=True
|
474
|
+
)
|
475
|
+
return True
|
476
|
+
except:
|
477
|
+
pass
|
478
|
+
return False
|
479
|
+
|
480
|
+
def get_health_status(self, deployment_info: Dict[str, Any]) -> Dict[str, Any]:
|
481
|
+
"""Get Git deployment health."""
|
482
|
+
return {
|
483
|
+
"status": "healthy" if deployment_info.get("commit_hash") else "unhealthy",
|
484
|
+
"commit": deployment_info.get("commit_hash", "unknown"),
|
485
|
+
"branch": deployment_info.get("branch", "unknown"),
|
486
|
+
}
|