cicaddy-github 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_github/__init__.py +3 -0
- cicaddy_github/config/__init__.py +0 -0
- cicaddy_github/config/settings.py +193 -0
- cicaddy_github/github_integration/__init__.py +0 -0
- cicaddy_github/github_integration/agents.py +268 -0
- cicaddy_github/github_integration/analyzer.py +281 -0
- cicaddy_github/github_integration/detector.py +23 -0
- cicaddy_github/github_integration/tools.py +136 -0
- cicaddy_github/plugin.py +107 -0
- cicaddy_github/security/__init__.py +0 -0
- cicaddy_github/security/leak_detector.py +104 -0
- cicaddy_github/validation.py +18 -0
- cicaddy_github-0.1.0.dist-info/METADATA +220 -0
- cicaddy_github-0.1.0.dist-info/RECORD +17 -0
- cicaddy_github-0.1.0.dist-info/WHEEL +4 -0
- cicaddy_github-0.1.0.dist-info/entry_points.txt +14 -0
- cicaddy_github-0.1.0.dist-info/licenses/LICENSE +190 -0
|
File without changes
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Configuration for cicaddy-github (GitHub-specific extension of cicaddy)."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from cicaddy.config.settings import CoreSettings
|
|
8
|
+
from pydantic import AliasChoices, Field
|
|
9
|
+
from pydantic_settings import SettingsConfigDict
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Settings(CoreSettings):
|
|
15
|
+
"""Application settings with GitHub Actions platform-specific fields.
|
|
16
|
+
|
|
17
|
+
Extends CoreSettings with GitHub-specific configuration such as
|
|
18
|
+
tokens, repository info, event names, and other CI variables.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
model_config = SettingsConfigDict(extra="ignore")
|
|
22
|
+
|
|
23
|
+
# GitHub configuration (uses built-in Actions variables)
|
|
24
|
+
github_token: str = Field(
|
|
25
|
+
default="",
|
|
26
|
+
validation_alias=AliasChoices("GITHUB_TOKEN"),
|
|
27
|
+
description="GitHub API token for repository access",
|
|
28
|
+
)
|
|
29
|
+
github_repository: str = Field(
|
|
30
|
+
default="",
|
|
31
|
+
validation_alias=AliasChoices("GITHUB_REPOSITORY"),
|
|
32
|
+
description="Repository in owner/name format",
|
|
33
|
+
)
|
|
34
|
+
github_ref: str = Field(
|
|
35
|
+
default="",
|
|
36
|
+
validation_alias=AliasChoices("GITHUB_REF"),
|
|
37
|
+
description="Git ref that triggered the workflow",
|
|
38
|
+
)
|
|
39
|
+
github_event_name: str = Field(
|
|
40
|
+
default="",
|
|
41
|
+
validation_alias=AliasChoices("GITHUB_EVENT_NAME"),
|
|
42
|
+
description="Name of the event that triggered the workflow",
|
|
43
|
+
)
|
|
44
|
+
github_sha: str = Field(
|
|
45
|
+
default="",
|
|
46
|
+
validation_alias=AliasChoices("GITHUB_SHA"),
|
|
47
|
+
description="Commit SHA that triggered the workflow",
|
|
48
|
+
)
|
|
49
|
+
github_run_id: str | None = Field(
|
|
50
|
+
default=None,
|
|
51
|
+
validation_alias=AliasChoices("GITHUB_RUN_ID"),
|
|
52
|
+
description="Unique ID for the workflow run",
|
|
53
|
+
)
|
|
54
|
+
github_pr_number: str | None = Field(
|
|
55
|
+
default=None,
|
|
56
|
+
validation_alias=AliasChoices("GITHUB_PR_NUMBER"),
|
|
57
|
+
description="Pull request number",
|
|
58
|
+
)
|
|
59
|
+
post_pr_comment: bool = Field(
|
|
60
|
+
default=False,
|
|
61
|
+
validation_alias=AliasChoices("POST_PR_COMMENT"),
|
|
62
|
+
description="Whether to post analysis results as PR comment",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def load_settings() -> Settings:
|
|
67
|
+
"""Load settings from environment variables with GitHub Actions defaults."""
|
|
68
|
+
|
|
69
|
+
# Handle MCP_SERVERS_CONFIG - default to empty array if missing
|
|
70
|
+
current_mcp_config = os.getenv("MCP_SERVERS_CONFIG")
|
|
71
|
+
if not current_mcp_config:
|
|
72
|
+
os.environ["MCP_SERVERS_CONFIG"] = "[]"
|
|
73
|
+
|
|
74
|
+
# Explicitly pass environment variables to work around Pydantic env reading issues
|
|
75
|
+
env_data: dict[str, Any] = {}
|
|
76
|
+
|
|
77
|
+
# GitHub configuration
|
|
78
|
+
if os.getenv("GITHUB_TOKEN"):
|
|
79
|
+
env_data["github_token"] = os.getenv("GITHUB_TOKEN")
|
|
80
|
+
if os.getenv("GITHUB_REPOSITORY"):
|
|
81
|
+
env_data["github_repository"] = os.getenv("GITHUB_REPOSITORY")
|
|
82
|
+
if os.getenv("GITHUB_REF"):
|
|
83
|
+
env_data["github_ref"] = os.getenv("GITHUB_REF")
|
|
84
|
+
if os.getenv("GITHUB_EVENT_NAME"):
|
|
85
|
+
env_data["github_event_name"] = os.getenv("GITHUB_EVENT_NAME")
|
|
86
|
+
if os.getenv("GITHUB_SHA"):
|
|
87
|
+
env_data["github_sha"] = os.getenv("GITHUB_SHA")
|
|
88
|
+
if os.getenv("GITHUB_RUN_ID"):
|
|
89
|
+
env_data["github_run_id"] = os.getenv("GITHUB_RUN_ID")
|
|
90
|
+
if os.getenv("GITHUB_PR_NUMBER"):
|
|
91
|
+
env_data["github_pr_number"] = os.getenv("GITHUB_PR_NUMBER")
|
|
92
|
+
|
|
93
|
+
# Post PR comment flag
|
|
94
|
+
post_pr = os.getenv("POST_PR_COMMENT", "").strip()
|
|
95
|
+
if post_pr:
|
|
96
|
+
env_data["post_pr_comment"] = post_pr.lower() in ("true", "1", "yes")
|
|
97
|
+
|
|
98
|
+
# AI provider configuration
|
|
99
|
+
if os.getenv("AI_PROVIDER"):
|
|
100
|
+
env_data["ai_provider"] = os.getenv("AI_PROVIDER")
|
|
101
|
+
if os.getenv("AI_MODEL"):
|
|
102
|
+
env_data["ai_model"] = os.getenv("AI_MODEL")
|
|
103
|
+
|
|
104
|
+
# AI API keys
|
|
105
|
+
if os.getenv("GEMINI_API_KEY"):
|
|
106
|
+
env_data["gemini_api_key"] = os.getenv("GEMINI_API_KEY")
|
|
107
|
+
if os.getenv("OPENAI_API_KEY"):
|
|
108
|
+
env_data["openai_api_key"] = os.getenv("OPENAI_API_KEY")
|
|
109
|
+
if os.getenv("ANTHROPIC_API_KEY"):
|
|
110
|
+
env_data["anthropic_api_key"] = os.getenv("ANTHROPIC_API_KEY")
|
|
111
|
+
|
|
112
|
+
# MCP server configuration
|
|
113
|
+
if os.getenv("MCP_SERVERS_CONFIG"):
|
|
114
|
+
env_data["mcp_servers_config"] = os.getenv("MCP_SERVERS_CONFIG")
|
|
115
|
+
|
|
116
|
+
# Slack configuration
|
|
117
|
+
if os.getenv("SLACK_WEBHOOK_URL"):
|
|
118
|
+
env_data["slack_webhook_url"] = os.getenv("SLACK_WEBHOOK_URL")
|
|
119
|
+
|
|
120
|
+
# Agent configuration
|
|
121
|
+
if os.getenv("AI_TASK_PROMPT"):
|
|
122
|
+
env_data["review_prompt"] = os.getenv("AI_TASK_PROMPT")
|
|
123
|
+
if os.getenv("AI_TASK_FILE"):
|
|
124
|
+
env_data["task_file"] = os.getenv("AI_TASK_FILE")
|
|
125
|
+
if os.getenv("ANALYSIS_FOCUS"):
|
|
126
|
+
env_data["analysis_focus"] = os.getenv("ANALYSIS_FOCUS")
|
|
127
|
+
|
|
128
|
+
# Git configuration
|
|
129
|
+
git_diff_context = os.getenv("GIT_DIFF_CONTEXT_LINES")
|
|
130
|
+
if git_diff_context:
|
|
131
|
+
env_data["git_diff_context_lines"] = int(git_diff_context)
|
|
132
|
+
|
|
133
|
+
# Local tools configuration
|
|
134
|
+
if os.getenv("ENABLE_LOCAL_TOOLS", "").strip():
|
|
135
|
+
env_data["enable_local_tools"] = os.getenv("ENABLE_LOCAL_TOOLS", "").lower().strip() in (
|
|
136
|
+
"true",
|
|
137
|
+
"1",
|
|
138
|
+
"yes",
|
|
139
|
+
)
|
|
140
|
+
if os.getenv("LOCAL_TOOLS_WORKING_DIR"):
|
|
141
|
+
env_data["local_tools_working_dir"] = os.getenv("LOCAL_TOOLS_WORKING_DIR")
|
|
142
|
+
|
|
143
|
+
# Execution configuration
|
|
144
|
+
max_exec_env = os.getenv("MAX_EXECUTION_TIME")
|
|
145
|
+
if max_exec_env == "":
|
|
146
|
+
os.environ.pop("MAX_EXECUTION_TIME", None)
|
|
147
|
+
elif max_exec_env:
|
|
148
|
+
try:
|
|
149
|
+
max_exec_time = int(max_exec_env)
|
|
150
|
+
if 60 <= max_exec_time <= 7200:
|
|
151
|
+
env_data["max_execution_time"] = max_exec_time
|
|
152
|
+
else:
|
|
153
|
+
logger.warning(
|
|
154
|
+
f"MAX_EXECUTION_TIME {max_exec_time} out of range [60, 7200], using default 600"
|
|
155
|
+
)
|
|
156
|
+
os.environ.pop("MAX_EXECUTION_TIME", None)
|
|
157
|
+
env_data["max_execution_time"] = 600
|
|
158
|
+
except ValueError:
|
|
159
|
+
logger.warning(f"Invalid MAX_EXECUTION_TIME value '{max_exec_env}', using default 600")
|
|
160
|
+
os.environ.pop("MAX_EXECUTION_TIME", None)
|
|
161
|
+
env_data["max_execution_time"] = 600
|
|
162
|
+
|
|
163
|
+
context_safety_env = os.getenv("CONTEXT_SAFETY_FACTOR")
|
|
164
|
+
if context_safety_env == "":
|
|
165
|
+
os.environ.pop("CONTEXT_SAFETY_FACTOR", None)
|
|
166
|
+
elif context_safety_env:
|
|
167
|
+
try:
|
|
168
|
+
safety_factor = float(context_safety_env)
|
|
169
|
+
if 0.5 <= safety_factor <= 0.97:
|
|
170
|
+
env_data["context_safety_factor"] = safety_factor
|
|
171
|
+
else:
|
|
172
|
+
logger.warning(
|
|
173
|
+
f"CONTEXT_SAFETY_FACTOR {safety_factor} out of range [0.5, 0.97], "
|
|
174
|
+
"using default 0.85"
|
|
175
|
+
)
|
|
176
|
+
os.environ.pop("CONTEXT_SAFETY_FACTOR", None)
|
|
177
|
+
env_data["context_safety_factor"] = 0.85
|
|
178
|
+
except ValueError:
|
|
179
|
+
logger.warning(
|
|
180
|
+
f"Invalid CONTEXT_SAFETY_FACTOR value '{context_safety_env}', using default 0.85"
|
|
181
|
+
)
|
|
182
|
+
os.environ.pop("CONTEXT_SAFETY_FACTOR", None)
|
|
183
|
+
env_data["context_safety_factor"] = 0.85
|
|
184
|
+
|
|
185
|
+
# Logging configuration
|
|
186
|
+
if os.getenv("LOG_LEVEL"):
|
|
187
|
+
env_data["log_level"] = os.getenv("LOG_LEVEL")
|
|
188
|
+
|
|
189
|
+
# Report configuration
|
|
190
|
+
if os.getenv("REPORT_TEMPLATE"):
|
|
191
|
+
env_data["report_template"] = os.getenv("REPORT_TEMPLATE")
|
|
192
|
+
|
|
193
|
+
return Settings(**env_data)
|
|
File without changes
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""GitHub AI Agents for PR review and task execution."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from cicaddy.agent.base import BaseAIAgent
|
|
7
|
+
from cicaddy.tools import ToolRegistry
|
|
8
|
+
from cicaddy.utils.logger import get_logger
|
|
9
|
+
|
|
10
|
+
from cicaddy_github.config.settings import Settings
|
|
11
|
+
from cicaddy_github.github_integration.analyzer import GitHubAnalyzer
|
|
12
|
+
from cicaddy_github.github_integration.tools import get_all_tools
|
|
13
|
+
from cicaddy_github.security.leak_detector import LeakDetector
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
BOT_COMMENT_MARKER_PR_REVIEW = "<!-- cicaddy-action:pr-review -->"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GitHubTaskAgent(BaseAIAgent):
|
|
21
|
+
"""AI Agent for scheduled tasks and changelog generation."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, settings: Settings | None = None):
|
|
24
|
+
super().__init__(settings)
|
|
25
|
+
self.leak_detector = LeakDetector()
|
|
26
|
+
|
|
27
|
+
async def _setup_local_tools(self):
|
|
28
|
+
"""Setup local tools including git operations for changelog."""
|
|
29
|
+
await super()._setup_local_tools()
|
|
30
|
+
if self.local_tool_registry is None:
|
|
31
|
+
self.local_tool_registry = ToolRegistry(server_name="local")
|
|
32
|
+
# Register git tools as local tools
|
|
33
|
+
for t in get_all_tools():
|
|
34
|
+
self.local_tool_registry.register(t)
|
|
35
|
+
logger.info(f"Registered git tools: {self.local_tool_registry.list_tool_names()}")
|
|
36
|
+
|
|
37
|
+
async def _setup_platform_integration(self):
|
|
38
|
+
"""Setup GitHub analyzer for API access."""
|
|
39
|
+
token = os.getenv("GITHUB_TOKEN", "")
|
|
40
|
+
repository = os.getenv("GITHUB_REPOSITORY", "")
|
|
41
|
+
working_dir = getattr(self.settings, "local_tools_working_dir", None) or "."
|
|
42
|
+
|
|
43
|
+
if token and repository:
|
|
44
|
+
try:
|
|
45
|
+
self.platform_analyzer = GitHubAnalyzer(
|
|
46
|
+
token=token, repository=repository, working_dir=working_dir
|
|
47
|
+
)
|
|
48
|
+
logger.info(f"GitHub analyzer initialized for {repository}")
|
|
49
|
+
except Exception as e:
|
|
50
|
+
logger.warning(f"Failed to initialize GitHub analyzer: {e}")
|
|
51
|
+
else:
|
|
52
|
+
logger.debug("GitHub analyzer not initialized (missing token or repository)")
|
|
53
|
+
|
|
54
|
+
async def get_analysis_context(self) -> dict[str, Any]:
|
|
55
|
+
"""Get task-specific context."""
|
|
56
|
+
return {
|
|
57
|
+
"analysis_type": "task",
|
|
58
|
+
"repository": os.getenv("GITHUB_REPOSITORY", ""),
|
|
59
|
+
"ref": os.getenv("GITHUB_REF", ""),
|
|
60
|
+
"sha": os.getenv("GITHUB_SHA", ""),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
def build_analysis_prompt(self, context: dict[str, Any]) -> str:
|
|
64
|
+
"""Build analysis prompt for task execution.
|
|
65
|
+
|
|
66
|
+
Supports DSPy YAML task definitions via AI_TASK_FILE.
|
|
67
|
+
Falls back to inline prompt or default.
|
|
68
|
+
"""
|
|
69
|
+
task_file = os.getenv("AI_TASK_FILE")
|
|
70
|
+
if task_file:
|
|
71
|
+
dspy_prompt = self.build_dspy_prompt(task_file, context)
|
|
72
|
+
if dspy_prompt:
|
|
73
|
+
return dspy_prompt
|
|
74
|
+
|
|
75
|
+
task_prompt = os.getenv("AI_TASK_PROMPT", "")
|
|
76
|
+
if task_prompt:
|
|
77
|
+
return task_prompt
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
"Analyze the repository and provide a summary of recent changes. "
|
|
81
|
+
"Use the available git tools to gather information."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
async def send_notifications(self, report: dict[str, Any], analysis_result: dict[str, Any]):
|
|
85
|
+
"""Send notifications via Slack (sanitized)."""
|
|
86
|
+
# Sanitize outputs before sending
|
|
87
|
+
if "ai_analysis" in analysis_result:
|
|
88
|
+
analysis_result["ai_analysis"] = self.leak_detector.sanitize_text(
|
|
89
|
+
analysis_result["ai_analysis"]
|
|
90
|
+
)
|
|
91
|
+
await super().send_notifications(report, analysis_result)
|
|
92
|
+
|
|
93
|
+
def get_session_id(self) -> str:
|
|
94
|
+
"""Get unique session ID for this task."""
|
|
95
|
+
run_id = os.getenv("GITHUB_RUN_ID", "unknown")
|
|
96
|
+
return f"task_{run_id}"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class GitHubPRAgent(BaseAIAgent):
|
|
100
|
+
"""AI Agent specialized for pull request analysis and code review."""
|
|
101
|
+
|
|
102
|
+
def __init__(self, settings: Settings | None = None):
|
|
103
|
+
super().__init__(settings)
|
|
104
|
+
self.pr_number = settings.github_pr_number if settings else os.getenv("GITHUB_PR_NUMBER")
|
|
105
|
+
self.leak_detector = LeakDetector()
|
|
106
|
+
|
|
107
|
+
async def _setup_local_tools(self):
|
|
108
|
+
"""Setup local tools for PR review."""
|
|
109
|
+
await super()._setup_local_tools()
|
|
110
|
+
if self.local_tool_registry is None:
|
|
111
|
+
self.local_tool_registry = ToolRegistry(server_name="local")
|
|
112
|
+
for t in get_all_tools():
|
|
113
|
+
self.local_tool_registry.register(t)
|
|
114
|
+
|
|
115
|
+
async def _setup_platform_integration(self):
|
|
116
|
+
"""Setup GitHub analyzer for PR access."""
|
|
117
|
+
token = os.getenv("GITHUB_TOKEN", "")
|
|
118
|
+
repository = os.getenv("GITHUB_REPOSITORY", "")
|
|
119
|
+
working_dir = getattr(self.settings, "local_tools_working_dir", None) or "."
|
|
120
|
+
|
|
121
|
+
if token and repository:
|
|
122
|
+
try:
|
|
123
|
+
self.platform_analyzer = GitHubAnalyzer(
|
|
124
|
+
token=token, repository=repository, working_dir=working_dir
|
|
125
|
+
)
|
|
126
|
+
logger.info(f"GitHub analyzer initialized for {repository}")
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.warning(f"Failed to initialize GitHub analyzer: {e}")
|
|
129
|
+
|
|
130
|
+
async def get_diff_content(self) -> str:
|
|
131
|
+
"""Get pull request diff content."""
|
|
132
|
+
if not self.pr_number:
|
|
133
|
+
raise ValueError("No PR number provided")
|
|
134
|
+
if not self.platform_analyzer:
|
|
135
|
+
raise ValueError("GitHub analyzer not initialized")
|
|
136
|
+
|
|
137
|
+
return await self.platform_analyzer.get_pull_request_diff(
|
|
138
|
+
int(self.pr_number),
|
|
139
|
+
context_lines=getattr(self.settings, "git_diff_context_lines", 3),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
async def get_review_context(self) -> dict[str, Any]:
|
|
143
|
+
"""Get pull request specific context."""
|
|
144
|
+
if not self.pr_number:
|
|
145
|
+
raise ValueError("No PR number provided")
|
|
146
|
+
if not self.platform_analyzer:
|
|
147
|
+
raise ValueError("GitHub analyzer not initialized")
|
|
148
|
+
|
|
149
|
+
pr_data = await self.platform_analyzer.get_pull_request_data(int(self.pr_number))
|
|
150
|
+
return {
|
|
151
|
+
"pull_request": pr_data,
|
|
152
|
+
"analysis_type": "pull_request",
|
|
153
|
+
"pr_number": self.pr_number,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async def get_analysis_context(self) -> dict[str, Any]:
|
|
157
|
+
"""Get analysis context including PR data and diff."""
|
|
158
|
+
context = await self.get_review_context()
|
|
159
|
+
|
|
160
|
+
# Get diff content
|
|
161
|
+
diff_content = await self.get_diff_content()
|
|
162
|
+
context["diff"] = diff_content
|
|
163
|
+
|
|
164
|
+
# Add repository info
|
|
165
|
+
context["repository"] = os.getenv("GITHUB_REPOSITORY", "")
|
|
166
|
+
|
|
167
|
+
return context
|
|
168
|
+
|
|
169
|
+
def build_analysis_prompt(self, context: dict[str, Any]) -> str:
|
|
170
|
+
"""Build analysis prompt for PR code review.
|
|
171
|
+
|
|
172
|
+
Supports DSPy YAML task definitions via AI_TASK_FILE.
|
|
173
|
+
Falls back to built-in prompt.
|
|
174
|
+
"""
|
|
175
|
+
task_file = os.getenv("AI_TASK_FILE")
|
|
176
|
+
if task_file:
|
|
177
|
+
pr_context = self._prepare_dspy_context(context)
|
|
178
|
+
dspy_prompt = self.build_dspy_prompt(task_file, pr_context)
|
|
179
|
+
if dspy_prompt:
|
|
180
|
+
return dspy_prompt
|
|
181
|
+
|
|
182
|
+
pr_data = context["pull_request"]
|
|
183
|
+
diff_content = context["diff"]
|
|
184
|
+
|
|
185
|
+
return f"""You are an AI agent performing pull request code review.
|
|
186
|
+
|
|
187
|
+
Repository: {context.get("repository", "Unknown")}
|
|
188
|
+
Pull Request: {pr_data["title"]}
|
|
189
|
+
Description: {pr_data.get("description", "No description")}
|
|
190
|
+
Author: {pr_data.get("author", {}).get("name", "Unknown")}
|
|
191
|
+
Target Branch: {pr_data.get("target_branch", "Unknown")}
|
|
192
|
+
Source Branch: {pr_data.get("source_branch", "Unknown")}
|
|
193
|
+
|
|
194
|
+
Code Changes:
|
|
195
|
+
```diff
|
|
196
|
+
{diff_content}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Instructions:
|
|
200
|
+
1. Analyze the code changes thoroughly
|
|
201
|
+
2. Identify bugs, security issues, and potential problems
|
|
202
|
+
3. Suggest improvements for code quality and maintainability
|
|
203
|
+
4. Provide actionable, specific feedback
|
|
204
|
+
|
|
205
|
+
Please provide your comprehensive analysis in markdown format.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
def _prepare_dspy_context(self, context: dict[str, Any]) -> dict[str, Any]:
|
|
209
|
+
"""Prepare context with PR-specific data for DSPy prompt building."""
|
|
210
|
+
pr_context = context.copy()
|
|
211
|
+
pr_data = context.get("pull_request", {})
|
|
212
|
+
pr_context["pr_title"] = pr_data.get("title", "Unknown")
|
|
213
|
+
pr_context["pr_description"] = pr_data.get("description", "")
|
|
214
|
+
pr_context["pr_author"] = pr_data.get("author", {}).get("name", "Unknown")
|
|
215
|
+
pr_context["target_branch"] = pr_data.get("target_branch", "Unknown")
|
|
216
|
+
pr_context["source_branch"] = pr_data.get("source_branch", "Unknown")
|
|
217
|
+
pr_context["pr_number"] = self.pr_number
|
|
218
|
+
pr_context["diff_content"] = context.get("diff", "")
|
|
219
|
+
return pr_context
|
|
220
|
+
|
|
221
|
+
async def send_notifications(self, report: dict[str, Any], analysis_result: dict[str, Any]):
|
|
222
|
+
"""Send notifications via PR comment and Slack."""
|
|
223
|
+
# Sanitize outputs
|
|
224
|
+
if "ai_analysis" in analysis_result:
|
|
225
|
+
analysis_result["ai_analysis"] = self.leak_detector.sanitize_text(
|
|
226
|
+
analysis_result["ai_analysis"]
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Post PR comment if enabled (updates existing bot comment in-place)
|
|
230
|
+
post_comment = getattr(self.settings, "post_pr_comment", False)
|
|
231
|
+
if post_comment and self.platform_analyzer and self.pr_number:
|
|
232
|
+
try:
|
|
233
|
+
comment = self._format_pr_comment(analysis_result)
|
|
234
|
+
await self.platform_analyzer.post_pr_comment(
|
|
235
|
+
int(self.pr_number), comment, comment_marker=BOT_COMMENT_MARKER_PR_REVIEW
|
|
236
|
+
)
|
|
237
|
+
logger.info(f"Posted analysis to PR #{self.pr_number}")
|
|
238
|
+
except Exception as e:
|
|
239
|
+
logger.error(
|
|
240
|
+
f"Failed to post PR comment: {self.leak_detector.sanitize_text(str(e))}"
|
|
241
|
+
)
|
|
242
|
+
logger.debug("PR comment post traceback:", exc_info=True)
|
|
243
|
+
|
|
244
|
+
# Send Slack notification using parent class
|
|
245
|
+
await super().send_notifications(report, analysis_result)
|
|
246
|
+
|
|
247
|
+
def _format_pr_comment(self, analysis_result: dict[str, Any]) -> str:
|
|
248
|
+
"""Format analysis results as a PR comment.
|
|
249
|
+
|
|
250
|
+
The hidden marker is prepended so the bot can find and update its own
|
|
251
|
+
comment later. No heading is injected — the AI analysis output already
|
|
252
|
+
contains its own structure.
|
|
253
|
+
"""
|
|
254
|
+
comment = f"{BOT_COMMENT_MARKER_PR_REVIEW}\n"
|
|
255
|
+
|
|
256
|
+
if "ai_analysis" in analysis_result:
|
|
257
|
+
comment += analysis_result["ai_analysis"] + "\n"
|
|
258
|
+
|
|
259
|
+
comment += (
|
|
260
|
+
"\n<!-- cicaddy-footer -->\n---\n"
|
|
261
|
+
"*Generated with [cicaddy-action]"
|
|
262
|
+
"(https://github.com/redhat-community-ai-tools/cicaddy-action)*"
|
|
263
|
+
)
|
|
264
|
+
return comment
|
|
265
|
+
|
|
266
|
+
def get_session_id(self) -> str:
|
|
267
|
+
"""Get unique session ID for this PR analysis."""
|
|
268
|
+
return f"pr_{self.pr_number or 'unknown'}"
|