cicaddy-gitlab 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.
- cicaddy_gitlab/__init__.py +1 -0
- cicaddy_gitlab/agent/__init__.py +1 -0
- cicaddy_gitlab/agent/base.py +29 -0
- cicaddy_gitlab/agent/base_review_agent.py +135 -0
- cicaddy_gitlab/agent/branch_agent.py +100 -0
- cicaddy_gitlab/agent/factory.py +57 -0
- cicaddy_gitlab/agent/mr_agent.py +392 -0
- cicaddy_gitlab/config/__init__.py +1 -0
- cicaddy_gitlab/config/settings.py +362 -0
- cicaddy_gitlab/gitlab_integration/__init__.py +5 -0
- cicaddy_gitlab/gitlab_integration/analyzer.py +455 -0
- cicaddy_gitlab/plugin.py +107 -0
- cicaddy_gitlab-0.1.0.dist-info/METADATA +234 -0
- cicaddy_gitlab-0.1.0.dist-info/RECORD +18 -0
- cicaddy_gitlab-0.1.0.dist-info/WHEEL +5 -0
- cicaddy_gitlab-0.1.0.dist-info/entry_points.txt +21 -0
- cicaddy_gitlab-0.1.0.dist-info/licenses/LICENSE +190 -0
- cicaddy_gitlab-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""GitLab platform plugin for cicaddy AI agent."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Agent modules for cicaddy-gitlab plugin."""
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""BaseAIAgent with GitLab platform integration."""
|
|
2
|
+
|
|
3
|
+
from cicaddy.agent.base import BaseAIAgent as _CicaddyBaseAIAgent
|
|
4
|
+
from cicaddy.utils.logger import get_logger
|
|
5
|
+
|
|
6
|
+
logger = get_logger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseAIAgent(_CicaddyBaseAIAgent):
|
|
10
|
+
"""BaseAIAgent with GitLab _setup_platform_integration override."""
|
|
11
|
+
|
|
12
|
+
async def _setup_platform_integration(self):
|
|
13
|
+
"""Setup GitLab platform integration if project_id is available."""
|
|
14
|
+
project_id = getattr(self.settings, "project_id", None)
|
|
15
|
+
if project_id:
|
|
16
|
+
from cicaddy_gitlab.gitlab_integration.analyzer import GitLabAnalyzer
|
|
17
|
+
|
|
18
|
+
gitlab_api_url = getattr(self.settings, "gitlab_api_url", "")
|
|
19
|
+
gitlab_token = getattr(self.settings, "gitlab_token", "")
|
|
20
|
+
logger.info(f"Initializing GitLab analyzer with URL: {gitlab_api_url}")
|
|
21
|
+
logger.info(f"Project ID: {project_id}")
|
|
22
|
+
self.platform_analyzer = GitLabAnalyzer(
|
|
23
|
+
token=gitlab_token,
|
|
24
|
+
api_url=gitlab_api_url,
|
|
25
|
+
project_id=project_id,
|
|
26
|
+
ssl_verify=self.settings.ssl_verify,
|
|
27
|
+
)
|
|
28
|
+
else:
|
|
29
|
+
logger.warning("No project ID available - platform analyzer disabled")
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""BaseReviewAgent with GitLab platform integration.
|
|
2
|
+
|
|
3
|
+
This class mirrors cicaddy's BaseReviewAgent interface but inherits from
|
|
4
|
+
cicaddy_gitlab's BaseAIAgent instead of cicaddy's BaseAIAgent. This is
|
|
5
|
+
required because cicaddy's BaseAIAgent has a no-op _setup_platform_integration(),
|
|
6
|
+
while cicaddy_gitlab's BaseAIAgent overrides it to initialize the GitLabAnalyzer
|
|
7
|
+
for posting MR comments and accessing the GitLab API.
|
|
8
|
+
|
|
9
|
+
MRO: MergeRequestAgent -> BaseReviewAgent (this) -> BaseAIAgent (cicaddy_gitlab)
|
|
10
|
+
-> BaseAIAgent (cicaddy) -> ABC
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Any, Dict, Optional
|
|
14
|
+
|
|
15
|
+
from cicaddy.config.settings import Settings
|
|
16
|
+
from cicaddy.git.diff_analyzer import DiffAnalyzer
|
|
17
|
+
from cicaddy.utils.logger import get_logger
|
|
18
|
+
|
|
19
|
+
from cicaddy_gitlab.agent.base import BaseAIAgent
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BaseReviewAgent(BaseAIAgent):
|
|
25
|
+
"""Base class for code review agents with GitLab platform integration.
|
|
26
|
+
|
|
27
|
+
Inherits from cicaddy_gitlab.agent.base.BaseAIAgent to get GitLabAnalyzer
|
|
28
|
+
initialization via _setup_platform_integration(). MergeRequestAgent extends
|
|
29
|
+
this class to gain both diff analysis and GitLab API capabilities.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, settings: Optional[Settings] = None):
|
|
33
|
+
super().__init__(settings)
|
|
34
|
+
self.diff_analyzer: Optional[DiffAnalyzer] = None
|
|
35
|
+
|
|
36
|
+
async def initialize(self):
|
|
37
|
+
"""Initialize the review agent with diff analyzer."""
|
|
38
|
+
await super().initialize()
|
|
39
|
+
|
|
40
|
+
# Initialize diff analyzer with git working directory
|
|
41
|
+
working_dir = getattr(self.settings, "git_working_directory", None)
|
|
42
|
+
self.diff_analyzer = DiffAnalyzer(working_directory=working_dir)
|
|
43
|
+
|
|
44
|
+
logger.info(
|
|
45
|
+
"Review agent initialized",
|
|
46
|
+
diff_analyzer_available=self.diff_analyzer is not None,
|
|
47
|
+
git_working_directory=working_dir,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
async def get_analysis_context(self) -> Dict[str, Any]:
|
|
51
|
+
"""Gather analysis context including diff and review-specific information."""
|
|
52
|
+
if not self.diff_analyzer:
|
|
53
|
+
raise ValueError("Diff analyzer not initialized - call initialize() first")
|
|
54
|
+
|
|
55
|
+
logger.info("Gathering analysis context for review")
|
|
56
|
+
|
|
57
|
+
# Get diff content
|
|
58
|
+
try:
|
|
59
|
+
diff_content = await self.get_diff_content()
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.error(f"Failed to get diff content: {e}")
|
|
62
|
+
diff_content = f"Error retrieving diff: {str(e)}"
|
|
63
|
+
|
|
64
|
+
# Get review-specific context
|
|
65
|
+
try:
|
|
66
|
+
review_context = await self.get_review_context()
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error(f"Failed to get review context: {e}")
|
|
69
|
+
review_context = {"error": f"Failed to get review context: {str(e)}"}
|
|
70
|
+
|
|
71
|
+
# Get project information if GitLab analyzer is available
|
|
72
|
+
project_info = {}
|
|
73
|
+
if self.platform_analyzer:
|
|
74
|
+
try:
|
|
75
|
+
project_info = await self.platform_analyzer.get_project_info()
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.warning(f"Could not get project info: {e}")
|
|
78
|
+
project_info = {"name": "Unknown Project", "error": str(e)}
|
|
79
|
+
|
|
80
|
+
# Combine all context
|
|
81
|
+
context = {
|
|
82
|
+
"project": project_info,
|
|
83
|
+
"diff": diff_content,
|
|
84
|
+
"platform_available": self.platform_analyzer is not None,
|
|
85
|
+
"timestamp": self.start_time.isoformat(),
|
|
86
|
+
"diff_lines": len(diff_content.splitlines()) if diff_content else 0,
|
|
87
|
+
**review_context,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
logger.info(
|
|
91
|
+
"Analysis context gathered",
|
|
92
|
+
diff_lines=context["diff_lines"],
|
|
93
|
+
analysis_type=context.get("analysis_type", "unknown"),
|
|
94
|
+
platform_available=context["platform_available"],
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return context
|
|
98
|
+
|
|
99
|
+
async def get_diff_summary(self) -> Dict[str, Any]:
|
|
100
|
+
"""Generate a summary of the diff changes."""
|
|
101
|
+
try:
|
|
102
|
+
diff_content = await self.get_diff_content()
|
|
103
|
+
|
|
104
|
+
lines = diff_content.splitlines()
|
|
105
|
+
added_lines = len([line for line in lines if line.startswith("+")])
|
|
106
|
+
removed_lines = len([line for line in lines if line.startswith("-")])
|
|
107
|
+
modified_files = len(
|
|
108
|
+
[line for line in lines if line.startswith("diff --git")]
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
"total_lines": len(lines),
|
|
113
|
+
"added_lines": added_lines,
|
|
114
|
+
"removed_lines": removed_lines,
|
|
115
|
+
"modified_files": modified_files,
|
|
116
|
+
"has_changes": len(lines) > 0,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.error(f"Failed to generate diff summary: {e}")
|
|
121
|
+
return {
|
|
122
|
+
"total_lines": 0,
|
|
123
|
+
"added_lines": 0,
|
|
124
|
+
"removed_lines": 0,
|
|
125
|
+
"modified_files": 0,
|
|
126
|
+
"has_changes": False,
|
|
127
|
+
"error": str(e),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
def _validate_initialized(self):
|
|
131
|
+
"""Validate that the agent is properly initialized."""
|
|
132
|
+
if not self.diff_analyzer:
|
|
133
|
+
raise ValueError(
|
|
134
|
+
"Review agent not properly initialized - diff analyzer missing"
|
|
135
|
+
)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""GitLab-specific BranchReviewAgent with commit comment posting.
|
|
2
|
+
|
|
3
|
+
The core cicaddy BranchReviewAgent handles branch review logic and Slack
|
|
4
|
+
notifications. This subclass re-parents it under cicaddy_gitlab's
|
|
5
|
+
BaseReviewAgent (which initializes GitLabAnalyzer) and adds GitLab
|
|
6
|
+
commit comment posting on top of the core notification behavior.
|
|
7
|
+
|
|
8
|
+
MRO: BranchReviewAgent (this) -> BaseReviewAgent (cicaddy_gitlab)
|
|
9
|
+
-> BaseAIAgent (cicaddy_gitlab) -> BranchReviewAgent (cicaddy)
|
|
10
|
+
-> BaseReviewAgent (cicaddy) -> BaseAIAgent (cicaddy) -> ABC
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from typing import Any, Dict
|
|
15
|
+
|
|
16
|
+
from cicaddy.agent.branch_agent import BranchReviewAgent as CoreBranchReviewAgent
|
|
17
|
+
from cicaddy.utils.logger import get_logger
|
|
18
|
+
|
|
19
|
+
from cicaddy_gitlab.agent.base_review_agent import BaseReviewAgent
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
BOT_NOTE_MARKER = "<!-- cicaddy-gitlab:branch-review -->"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BranchReviewAgent(BaseReviewAgent, CoreBranchReviewAgent):
|
|
27
|
+
"""BranchReviewAgent with GitLab platform integration.
|
|
28
|
+
|
|
29
|
+
Combines cicaddy core's branch review logic with GitLab's
|
|
30
|
+
BaseReviewAgent for platform integration. Adds GitLab commit
|
|
31
|
+
comment posting on top of core notification behavior.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
async def send_notifications(
|
|
35
|
+
self, report: Dict[str, Any], analysis_result: Dict[str, Any]
|
|
36
|
+
):
|
|
37
|
+
"""Send notifications via Slack and post GitLab commit comment."""
|
|
38
|
+
logger.info(
|
|
39
|
+
f"Sending notifications for branch review: "
|
|
40
|
+
f"{self.source_branch} -> {self.target_branch}"
|
|
41
|
+
)
|
|
42
|
+
await super().send_notifications(report, analysis_result)
|
|
43
|
+
await self._post_gitlab_commit_comment(report, analysis_result)
|
|
44
|
+
|
|
45
|
+
async def _post_gitlab_commit_comment(
|
|
46
|
+
self, report: Dict[str, Any], analysis_result: Dict[str, Any]
|
|
47
|
+
):
|
|
48
|
+
"""Post analysis results as a comment to the current commit."""
|
|
49
|
+
if not self.platform_analyzer:
|
|
50
|
+
logger.debug("No platform analyzer available, skipping commit comment")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
commit_sha = os.getenv("CI_COMMIT_SHA")
|
|
54
|
+
if not commit_sha:
|
|
55
|
+
logger.debug("No CI_COMMIT_SHA available, skipping commit comment")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
comment_content = self._format_gitlab_comment(report, analysis_result)
|
|
60
|
+
result = await self.platform_analyzer.post_commit_note(
|
|
61
|
+
commit_sha, comment_content, note_marker=BOT_NOTE_MARKER
|
|
62
|
+
)
|
|
63
|
+
logger.info(
|
|
64
|
+
f"Posted analysis comment to commit {commit_sha[:8]}, "
|
|
65
|
+
f"note ID: {result.get('id')}"
|
|
66
|
+
)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error(f"Failed to post GitLab commit comment: {e}")
|
|
69
|
+
|
|
70
|
+
def _format_gitlab_comment(
|
|
71
|
+
self, report: Dict[str, Any], analysis_result: Dict[str, Any]
|
|
72
|
+
) -> str:
|
|
73
|
+
"""Format analysis results for GitLab commit comment.
|
|
74
|
+
|
|
75
|
+
The hidden marker is prepended so the bot can find and update its
|
|
76
|
+
own note later.
|
|
77
|
+
"""
|
|
78
|
+
ai_analysis = analysis_result.get("ai_analysis", "No analysis available")
|
|
79
|
+
status = analysis_result.get("status", "unknown")
|
|
80
|
+
execution_time = analysis_result.get("execution_time", 0)
|
|
81
|
+
project_name = report.get("project", "Unknown Project")
|
|
82
|
+
status_icon = "pass" if status == "success" else "fail"
|
|
83
|
+
|
|
84
|
+
return f"""{BOT_NOTE_MARKER}
|
|
85
|
+
## AI Branch Analysis Results ({status_icon})
|
|
86
|
+
|
|
87
|
+
**Project:** {project_name}
|
|
88
|
+
**Branch:** {self.source_branch} -> {self.target_branch}
|
|
89
|
+
**Status:** {status.title()}
|
|
90
|
+
**Execution Time:** {execution_time:.1f}s
|
|
91
|
+
**Report ID:** `{report.get("report_id", "unknown")}`
|
|
92
|
+
|
|
93
|
+
### Analysis Summary
|
|
94
|
+
|
|
95
|
+
{ai_analysis}
|
|
96
|
+
|
|
97
|
+
<!-- cicaddy-footer -->
|
|
98
|
+
---
|
|
99
|
+
*Analysis by cicaddy-gitlab AI Agent | Report: `{report.get("report_id", "N/A")}`*
|
|
100
|
+
"""
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""GitLab-specific agent type detection for cicaddy plugin."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from cicaddy.utils.logger import get_logger
|
|
7
|
+
|
|
8
|
+
logger = get_logger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _detect_gitlab_agent_type(settings) -> Optional[str]:
|
|
12
|
+
"""Detect GitLab-specific agent types from CI environment variables.
|
|
13
|
+
|
|
14
|
+
Handles merge_request, branch_review, and task detection from GitLab CI
|
|
15
|
+
variables. Runs at priority 40 (before cicaddy core's detector at 50).
|
|
16
|
+
"""
|
|
17
|
+
# Merge request context
|
|
18
|
+
ci_mr_iid = os.getenv("CI_MERGE_REQUEST_IID")
|
|
19
|
+
if ci_mr_iid:
|
|
20
|
+
logger.info(f"Detected merge request context: CI_MERGE_REQUEST_IID={ci_mr_iid}")
|
|
21
|
+
return "merge_request"
|
|
22
|
+
|
|
23
|
+
mr_iid = getattr(settings, "merge_request_iid", None)
|
|
24
|
+
if mr_iid:
|
|
25
|
+
logger.info(f"Found merge request IID in settings: {mr_iid}")
|
|
26
|
+
return "merge_request"
|
|
27
|
+
|
|
28
|
+
# Task/scheduled context
|
|
29
|
+
task_type = os.getenv("TASK_TYPE") or os.getenv("CRON_TASK_TYPE")
|
|
30
|
+
if task_type:
|
|
31
|
+
logger.info(f"Detected task context: TASK_TYPE={task_type}")
|
|
32
|
+
return "task"
|
|
33
|
+
|
|
34
|
+
# GitLab CI pipeline source detection
|
|
35
|
+
pipeline_source = os.getenv("CI_PIPELINE_SOURCE")
|
|
36
|
+
if pipeline_source == "merge_request_event":
|
|
37
|
+
logger.info(
|
|
38
|
+
f"Detected merge request context: CI_PIPELINE_SOURCE={pipeline_source}"
|
|
39
|
+
)
|
|
40
|
+
return "merge_request"
|
|
41
|
+
elif pipeline_source == "schedule":
|
|
42
|
+
logger.info(
|
|
43
|
+
f"Detected task context: CI_PIPELINE_SOURCE={pipeline_source}, "
|
|
44
|
+
f"TASK_TYPE={task_type}"
|
|
45
|
+
)
|
|
46
|
+
return "task"
|
|
47
|
+
elif pipeline_source == "push":
|
|
48
|
+
ci_commit_branch = os.getenv("CI_COMMIT_BRANCH")
|
|
49
|
+
ci_default_branch = os.getenv("CI_DEFAULT_BRANCH", "main")
|
|
50
|
+
if ci_commit_branch and ci_commit_branch != ci_default_branch:
|
|
51
|
+
logger.info(
|
|
52
|
+
f"Detected branch push context: CI_PIPELINE_SOURCE={pipeline_source}, "
|
|
53
|
+
f"branch={ci_commit_branch}"
|
|
54
|
+
)
|
|
55
|
+
return "branch_review"
|
|
56
|
+
|
|
57
|
+
return None
|