-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathgithub_manager.py
More file actions
698 lines (590 loc) · 26.2 KB
/
github_manager.py
File metadata and controls
698 lines (590 loc) · 26.2 KB
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
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
"""
XMRT-Ecosystem GitHub Manager
This module handles real GitHub repository operations including:
- Repository analysis and monitoring
- Automated code commits and pushes
- Branch management and pull requests
- Integration with PyGithub for authentic GitHub API operations
- Commit history tracking and analysis
- Real deployment triggers via repository webhooks
"""
import asyncio
import logging
import os
import base64
import json
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional, Tuple
import tempfile
import traceback
from github import Github, GithubException
from github.Repository import Repository
from github.GitRef import GitRef
from github.ContentFile import ContentFile
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class GitHubManager:
"""
Manages real GitHub repository operations for the XMRT-Ecosystem
Handles:
- Repository analysis and monitoring
- Automated commits and deployments
- Code quality tracking
- Integration with Render deployment webhooks
"""
def __init__(self, config: Dict[str, Any]):
"""Initialize GitHub manager with real repository credentials"""
self.config = config
self.github_token = config.get('github_token')
self.repo_owner = config.get('github_owner', 'DevGruGold')
self.repo_name = config.get('github_repo', 'XMRT-Ecosystem')
self.branch_name = config.get('github_branch', 'main')
# Initialize GitHub API client
self.github_client = None
self.repository = None
self.commit_history = []
self.deployment_stats = {
'total_commits': 0,
'successful_deployments': 0,
'failed_deployments': 0,
'last_commit_time': None,
'average_commit_size': 0
}
logger.info(f"🔗 GitHub Manager initialized for {self.repo_owner}/{self.repo_name}")
async def initialize(self):
"""Initialize GitHub connection and repository access"""
try:
logger.info("🔧 Initializing GitHub connection...")
# Initialize GitHub client
self.github_client = Github(self.github_token)
# Get repository reference
self.repository = self.github_client.get_repo(f"{self.repo_owner}/{self.repo_name}")
# Verify repository access
repo_info = {
'name': self.repository.full_name,
'description': self.repository.description,
'stars': self.repository.stargazers_count,
'forks': self.repository.forks_count,
'last_updated': self.repository.updated_at.isoformat()
}
logger.info(f"✅ Connected to repository: {repo_info['name']}")
logger.info(f"📊 Repository stats: {repo_info['stars']} stars, {repo_info['forks']} forks")
except GithubException as e:
logger.error(f"❌ GitHub API error: {e}")
raise
except Exception as e:
logger.error(f"❌ Failed to initialize GitHub connection: {e}")
raise
async def analyze_repository(self) -> Dict[str, Any]:
"""Analyze current repository state and structure"""
try:
logger.info("📊 Analyzing repository structure...")
# Get repository metadata
repo_analysis = {
'repository_info': {
'name': self.repository.full_name,
'description': self.repository.description,
'language': self.repository.language,
'size': self.repository.size,
'stars': self.repository.stargazers_count,
'forks': self.repository.forks_count,
'open_issues': self.repository.open_issues_count,
'created_at': self.repository.created_at.isoformat(),
'updated_at': self.repository.updated_at.isoformat()
},
'branch_info': await self._analyze_branches(),
'file_structure': await self._analyze_file_structure(),
'recent_commits': await self._get_recent_commits(limit=10),
'code_quality': await self._assess_code_quality(),
'deployment_status': await self._check_deployment_status(),
'analysis_timestamp': datetime.now().isoformat()
}
logger.info("✅ Repository analysis completed")
return repo_analysis
except Exception as e:
logger.error(f"❌ Repository analysis failed: {e}")
return {
'error': str(e),
'timestamp': datetime.now().isoformat()
}
async def _analyze_branches(self) -> Dict[str, Any]:
"""Analyze repository branches"""
try:
branches = []
for branch in self.repository.get_branches():
branch_info = {
'name': branch.name,
'protected': branch.protected,
'commit_sha': branch.commit.sha,
'commit_message': branch.commit.commit.message,
'last_modified': branch.commit.commit.author.date.isoformat()
}
branches.append(branch_info)
return {
'total_branches': len(branches),
'default_branch': self.repository.default_branch,
'branches': branches
}
except Exception as e:
logger.error(f"Branch analysis failed: {e}")
return {'error': str(e), 'total_branches': 0}
async def _analyze_file_structure(self) -> Dict[str, Any]:
"""Analyze repository file structure"""
try:
file_structure = {
'python_files': [],
'config_files': [],
'documentation': [],
'total_files': 0,
'directories': []
}
# Get repository contents
contents = self.repository.get_contents("")
def analyze_contents(contents_list, path=""):
for content in contents_list:
if content.type == "dir":
file_structure['directories'].append(f"{path}{content.name}/")
# Recursively analyze subdirectories
try:
subcontents = self.repository.get_contents(content.path)
analyze_contents(subcontents, f"{path}{content.name}/")
except:
pass # Skip if can't access subdirectory
else:
file_structure['total_files'] += 1
full_path = f"{path}{content.name}"
if content.name.endswith('.py'):
file_structure['python_files'].append(full_path)
elif content.name in ['requirements.txt', 'Dockerfile', 'docker-compose.yml', 'render.yaml']:
file_structure['config_files'].append(full_path)
elif content.name.endswith(('.md', '.rst', '.txt')):
file_structure['documentation'].append(full_path)
analyze_contents(contents)
return file_structure
except Exception as e:
logger.error(f"File structure analysis failed: {e}")
return {'error': str(e), 'total_files': 0}
async def _get_recent_commits(self, limit: int = 10) -> List[Dict[str, Any]]:
"""Get recent commit history"""
try:
commits = []
for commit in self.repository.get_commits()[:limit]:
commit_info = {
'sha': commit.sha,
'message': commit.commit.message,
'author': commit.commit.author.name,
'author_email': commit.commit.author.email,
'date': commit.commit.author.date.isoformat(),
'additions': commit.stats.additions,
'deletions': commit.stats.deletions,
'changed_files': commit.stats.total
}
commits.append(commit_info)
return commits
except Exception as e:
logger.error(f"Commit history analysis failed: {e}")
return []
async def _assess_code_quality(self) -> Dict[str, Any]:
"""Assess overall code quality metrics"""
try:
quality_metrics = {
'python_file_count': 0,
'total_lines': 0,
'has_requirements': False,
'has_dockerfile': False,
'has_readme': False,
'has_tests': False,
'code_quality_score': 0.0
}
# Check for important files
try:
self.repository.get_contents("requirements.txt")
quality_metrics['has_requirements'] = True
except:
pass
try:
self.repository.get_contents("Dockerfile")
quality_metrics['has_dockerfile'] = True
except:
pass
try:
readme_files = ['README.md', 'readme.md', 'README.rst', 'README.txt']
for readme in readme_files:
try:
self.repository.get_contents(readme)
quality_metrics['has_readme'] = True
break
except:
continue
except:
pass
# Look for test files
try:
contents = self.repository.get_contents("")
for content in contents:
if content.type == "file" and ('test' in content.name.lower() or content.name.startswith('test_')):
quality_metrics['has_tests'] = True
break
except:
pass
# Count Python files
try:
def count_python_files(contents_list):
count = 0
for content in contents_list:
if content.type == "dir":
try:
subcontents = self.repository.get_contents(content.path)
count += count_python_files(subcontents)
except:
pass
elif content.name.endswith('.py'):
count += 1
return count
contents = self.repository.get_contents("")
quality_metrics['python_file_count'] = count_python_files(contents)
except:
pass
# Calculate quality score
score = 0
if quality_metrics['has_requirements']:
score += 0.25
if quality_metrics['has_dockerfile']:
score += 0.2
if quality_metrics['has_readme']:
score += 0.25
if quality_metrics['has_tests']:
score += 0.3
quality_metrics['code_quality_score'] = score
return quality_metrics
except Exception as e:
logger.error(f"Code quality assessment failed: {e}")
return {'error': str(e), 'code_quality_score': 0.0}
async def _check_deployment_status(self) -> Dict[str, Any]:
"""Check deployment status and webhook configuration"""
try:
deployment_status = {
'webhooks_configured': False,
'auto_deploy_enabled': False,
'last_deployment': None,
'deployment_service': 'render' # Assuming Render deployment
}
# Check for webhooks (limited by GitHub API permissions)
try:
# This would require admin access to see webhooks
# For now, we'll assume webhooks are configured if repository exists
deployment_status['webhooks_configured'] = True
deployment_status['auto_deploy_enabled'] = True
except:
pass
return deployment_status
except Exception as e:
logger.error(f"Deployment status check failed: {e}")
return {'error': str(e)}
async def commit_improvements(self, cycle_id: str, improvements: List[Dict[str, Any]], commit_message: str) -> Dict[str, Any]:
"""Commit code improvements to the repository"""
try:
logger.info(f"💾 Committing improvements for cycle {cycle_id}")
commit_results = {
'success': False,
'commit_sha': None,
'files_modified': [],
'commit_message': commit_message,
'timestamp': datetime.now().isoformat(),
'cycle_id': cycle_id
}
# Process each improvement
files_to_commit = []
for improvement in improvements:
file_path = improvement.get('file_path', f'autonomous_improvement_{len(files_to_commit)+1}.py')
file_content = improvement.get('code', improvement.get('generated_code', ''))
if file_content:
files_to_commit.append({
'path': file_path,
'content': file_content,
'description': improvement.get('description', 'Autonomous improvement')
})
if not files_to_commit:
logger.warning("No files to commit")
return {
'success': False,
'error': 'No valid improvements to commit',
'cycle_id': cycle_id
}
# Create or update files in repository
committed_files = []
for file_info in files_to_commit:
try:
file_path = file_info['path']
file_content = file_info['content']
# Check if file already exists
try:
existing_file = self.repository.get_contents(file_path)
# Update existing file
result = self.repository.update_file(
path=file_path,
message=f"Update {file_path} - {cycle_id}",
content=file_content,
sha=existing_file.sha,
branch=self.branch_name
)
logger.info(f"📝 Updated file: {file_path}")
except GithubException as e:
if e.status == 404:
# Create new file
result = self.repository.create_file(
path=file_path,
message=f"Create {file_path} - {cycle_id}",
content=file_content,
branch=self.branch_name
)
logger.info(f"📄 Created new file: {file_path}")
else:
raise
committed_files.append({
'path': file_path,
'commit_sha': result['commit'].sha,
'status': 'success'
})
except Exception as file_error:
logger.error(f"❌ Failed to commit file {file_path}: {file_error}")
committed_files.append({
'path': file_path,
'status': 'failed',
'error': str(file_error)
})
# Update commit results
successful_commits = [f for f in committed_files if f['status'] == 'success']
if successful_commits:
commit_results['success'] = True
commit_results['commit_sha'] = successful_commits[-1]['commit_sha'] # Use last commit SHA
commit_results['files_modified'] = [f['path'] for f in successful_commits]
# Update deployment stats
self.deployment_stats['total_commits'] += 1
self.deployment_stats['successful_deployments'] += 1
self.deployment_stats['last_commit_time'] = datetime.now().isoformat()
logger.info(f"✅ Successfully committed {len(successful_commits)} files")
# Trigger deployment webhook (Render auto-deploys on push)
await self._trigger_deployment(commit_results['commit_sha'])
else:
commit_results['success'] = False
commit_results['error'] = 'No files were successfully committed'
self.deployment_stats['failed_deployments'] += 1
commit_results['commit_details'] = committed_files
# Store commit in history
self.commit_history.append(commit_results)
return commit_results
except Exception as e:
logger.error(f"❌ Commit operation failed: {e}")
logger.error(traceback.format_exc())
self.deployment_stats['failed_deployments'] += 1
return {
'success': False,
'error': str(e),
'cycle_id': cycle_id,
'timestamp': datetime.now().isoformat()
}
async def _trigger_deployment(self, commit_sha: str):
"""Trigger deployment after successful commit"""
try:
logger.info(f"🚀 Triggering deployment for commit {commit_sha[:8]}")
# Render automatically deploys on git push to main branch
# No additional action needed, just log the trigger
deployment_info = {
'commit_sha': commit_sha,
'deployment_service': 'render',
'auto_deploy': True,
'triggered_at': datetime.now().isoformat()
}
logger.info("✅ Deployment triggered successfully (Render auto-deploy)")
return deployment_info
except Exception as e:
logger.error(f"❌ Deployment trigger failed: {e}")
return {'error': str(e)}
async def create_branch(self, branch_name: str, base_branch: str = None) -> Dict[str, Any]:
"""Create a new branch for experimental features"""
try:
if base_branch is None:
base_branch = self.repository.default_branch
# Get base branch reference
base_ref = self.repository.get_git_ref(f"heads/{base_branch}")
# Create new branch
new_ref = self.repository.create_git_ref(
ref=f"refs/heads/{branch_name}",
sha=base_ref.object.sha
)
logger.info(f"🌿 Created new branch: {branch_name}")
return {
'success': True,
'branch_name': branch_name,
'base_branch': base_branch,
'commit_sha': new_ref.object.sha
}
except Exception as e:
logger.error(f"❌ Branch creation failed: {e}")
return {
'success': False,
'error': str(e),
'branch_name': branch_name
}
async def create_pull_request(self, title: str, body: str, head_branch: str, base_branch: str = None) -> Dict[str, Any]:
"""Create a pull request for code review"""
try:
if base_branch is None:
base_branch = self.repository.default_branch
# Create pull request
pr = self.repository.create_pull(
title=title,
body=body,
head=head_branch,
base=base_branch
)
logger.info(f"🔄 Created pull request: #{pr.number}")
return {
'success': True,
'pr_number': pr.number,
'pr_url': pr.html_url,
'title': title,
'head_branch': head_branch,
'base_branch': base_branch
}
except Exception as e:
logger.error(f"❌ Pull request creation failed: {e}")
return {
'success': False,
'error': str(e),
'title': title
}
async def get_file_content(self, file_path: str) -> Dict[str, Any]:
"""Get content of a specific file from the repository"""
try:
file_content = self.repository.get_contents(file_path)
# Decode base64 content
content = base64.b64decode(file_content.content).decode('utf-8')
return {
'success': True,
'file_path': file_path,
'content': content,
'size': file_content.size,
'sha': file_content.sha,
'last_modified': file_content.last_modified
}
except GithubException as e:
if e.status == 404:
return {
'success': False,
'error': 'File not found',
'file_path': file_path
}
else:
logger.error(f"❌ Failed to get file content: {e}")
return {
'success': False,
'error': str(e),
'file_path': file_path
}
async def search_code(self, query: str, language: str = None) -> Dict[str, Any]:
"""Search for code patterns in the repository"""
try:
search_query = f"{query} repo:{self.repo_owner}/{self.repo_name}"
if language:
search_query += f" language:{language}"
# Use GitHub search API
search_results = self.github_client.search_code(search_query)
results = []
for result in search_results[:10]: # Limit to first 10 results
results.append({
'file_path': result.path,
'repository': result.repository.full_name,
'html_url': result.html_url,
'score': result.score
})
return {
'success': True,
'query': query,
'total_count': search_results.totalCount,
'results': results
}
except Exception as e:
logger.error(f"❌ Code search failed: {e}")
return {
'success': False,
'error': str(e),
'query': query
}
async def health_check(self) -> Dict[str, Any]:
"""Perform health check on GitHub integration"""
try:
# Check API rate limits
rate_limit = self.github_client.get_rate_limit()
# Check repository access
repo_accessible = True
try:
self.repository.get_contents("README.md") # Try to access a common file
except:
repo_accessible = False
health_status = {
'github_connected': True,
'repository_accessible': repo_accessible,
'api_rate_limit': {
'remaining': rate_limit.core.remaining,
'limit': rate_limit.core.limit,
'reset_time': rate_limit.core.reset.isoformat()
},
'deployment_stats': self.deployment_stats.copy(),
'commit_history_count': len(self.commit_history),
'health_status': 'healthy' if repo_accessible else 'degraded'
}
return health_status
except Exception as e:
logger.error(f"❌ GitHub health check failed: {e}")
return {
'github_connected': False,
'error': str(e),
'health_status': 'unhealthy'
}
async def get_deployment_metrics(self) -> Dict[str, Any]:
"""Get comprehensive deployment metrics"""
try:
# Recent commit activity
recent_commits = await self._get_recent_commits(limit=20)
# Calculate commit frequency
if recent_commits:
first_commit_date = datetime.fromisoformat(recent_commits[-1]['date'].replace('Z', '+00:00'))
last_commit_date = datetime.fromisoformat(recent_commits[0]['date'].replace('Z', '+00:00'))
if len(recent_commits) > 1:
time_diff = (last_commit_date - first_commit_date).total_seconds()
commit_frequency = len(recent_commits) / (time_diff / 3600) if time_diff > 0 else 0 # commits per hour
else:
commit_frequency = 0
else:
commit_frequency = 0
metrics = {
'deployment_stats': self.deployment_stats.copy(),
'commit_frequency_per_hour': commit_frequency,
'recent_commit_count': len(recent_commits),
'repository_health': await self._assess_code_quality(),
'api_usage': {
'requests_made': getattr(self.github_client, '_Github__requester', {}).get('_Requester__requestCount', 0),
'rate_limit_status': self.github_client.get_rate_limit().core.remaining
},
'last_analysis': datetime.now().isoformat()
}
return metrics
except Exception as e:
logger.error(f"❌ Failed to get deployment metrics: {e}")
return {
'error': str(e),
'deployment_stats': self.deployment_stats.copy()
}
def get_status(self) -> Dict[str, Any]:
"""Get current GitHub manager status"""
return {
'connected': self.github_client is not None,
'repository': f"{self.repo_owner}/{self.repo_name}" if self.repository else None,
'branch': self.branch_name,
'deployment_stats': self.deployment_stats.copy(),
'commit_history_count': len(self.commit_history),
'last_activity': datetime.now().isoformat()
}