Coverage for src / moai_adk / core / issue_creator.py: 0.00%
102 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
1"""
2GitHub Issue Creator for MoAI-ADK quick issue reporting.
4Enables users to quickly create GitHub Issues with standardized templates
5using `/alfred:9-feedback` interactive dialog.
7"""
9import subprocess
10from dataclasses import dataclass
11from enum import Enum
12from typing import Any, Dict, List, Optional
15class IssueType(Enum):
16 """Supported GitHub issue types."""
18 BUG = "bug"
19 FEATURE = "feature"
20 IMPROVEMENT = "improvement"
21 QUESTION = "question"
24class IssuePriority(Enum):
25 """Issue priority levels."""
27 CRITICAL = "critical"
28 HIGH = "high"
29 MEDIUM = "medium"
30 LOW = "low"
33@dataclass
34class IssueConfig:
35 """Configuration for issue creation."""
37 issue_type: IssueType
38 title: str
39 description: str
40 priority: IssuePriority = IssuePriority.MEDIUM
41 category: Optional[str] = None
42 assignees: Optional[List[str]] = None
43 custom_labels: Optional[List[str]] = None
46class GitHubIssueCreator:
47 """
48 Creates GitHub Issues using the `gh` CLI.
50 Supports:
51 - Multiple issue types (bug, feature, improvement, question)
52 - Priority levels and categories
53 - Standard templates for each type
54 - Label automation
55 - Priority emoji indicators
56 """
58 # Label mapping for issue types
59 LABEL_MAP = {
60 IssueType.BUG: ["bug", "reported"],
61 IssueType.FEATURE: ["feature-request", "enhancement"],
62 IssueType.IMPROVEMENT: ["improvement", "enhancement"],
63 IssueType.QUESTION: [
64 "question",
65 "help wanted",
66 ], # Fixed: "help-wanted" → "help wanted" (GitHub standard)
67 }
69 # Priority emoji
70 PRIORITY_EMOJI = {
71 IssuePriority.CRITICAL: "🔴",
72 IssuePriority.HIGH: "🟠",
73 IssuePriority.MEDIUM: "🟡",
74 IssuePriority.LOW: "🟢",
75 }
77 # Issue type emoji
78 TYPE_EMOJI = {
79 IssueType.BUG: "🐛",
80 IssueType.FEATURE: "✨",
81 IssueType.IMPROVEMENT: "⚡",
82 IssueType.QUESTION: "❓",
83 }
85 def __init__(self, github_token: Optional[str] = None):
86 """
87 Initialize the GitHub Issue Creator.
89 Args:
90 github_token: GitHub API token. If not provided, uses GITHUB_TOKEN env var.
91 """
92 self.github_token = github_token
93 self._check_gh_cli()
95 def _check_gh_cli(self) -> None:
96 """
97 Check if `gh` CLI is installed and accessible.
99 Raises:
100 RuntimeError: If `gh` CLI is not found or not authenticated.
101 """
102 try:
103 result = subprocess.run(
104 ["gh", "auth", "status"], capture_output=True, text=True, timeout=5
105 )
106 if result.returncode != 0:
107 raise RuntimeError(
108 "GitHub CLI (gh) is not authenticated. "
109 "Run `gh auth login` to authenticate."
110 )
111 except FileNotFoundError:
112 raise RuntimeError(
113 "GitHub CLI (gh) is not installed. "
114 "Please install it: https://cli.github.com"
115 )
117 def create_issue(self, config: IssueConfig) -> Dict[str, Any]:
118 """
119 Create a GitHub issue with the given configuration.
121 Args:
122 config: Issue configuration
124 Returns:
125 Dictionary containing issue creation result:
126 {
127 "success": bool,
128 "issue_number": int,
129 "issue_url": str,
130 "message": str
131 }
133 Raises:
134 RuntimeError: If issue creation fails
135 """
136 # Build title with emoji and priority
137 emoji = self.TYPE_EMOJI.get(config.issue_type, "📋")
138 priority_emoji = self.PRIORITY_EMOJI.get(config.priority, "")
139 full_title = f"{emoji} [{config.issue_type.value.upper()}] {config.title}"
140 if priority_emoji:
141 full_title = f"{priority_emoji} {full_title}"
143 # Build body with template
144 body = self._build_body(config)
146 # Collect labels
147 labels = self.LABEL_MAP.get(config.issue_type, []).copy()
148 if config.priority:
149 labels.append(
150 config.priority.value
151 ) # Fixed: removed "priority-" prefix (use direct label names)
152 if config.category:
153 labels.append(f"category-{config.category.lower().replace(' ', '-')}")
154 if config.custom_labels:
155 labels.extend(config.custom_labels)
157 # Build gh command
158 gh_command = [
159 "gh",
160 "issue",
161 "create",
162 "--title",
163 full_title,
164 "--body",
165 body,
166 ]
168 # Add labels
169 if labels:
170 gh_command.extend(["--label", ",".join(set(labels))])
172 # Add assignees if provided
173 if config.assignees:
174 gh_command.extend(["--assignee", ",".join(config.assignees)])
176 try:
177 result = subprocess.run(
178 gh_command, capture_output=True, text=True, timeout=30
179 )
181 if result.returncode != 0:
182 error_msg = result.stderr or result.stdout
183 raise RuntimeError(f"Failed to create GitHub issue: {error_msg}")
185 # Parse issue URL from output
186 issue_url = result.stdout.strip()
187 issue_number = self._extract_issue_number(issue_url)
189 return {
190 "success": True,
191 "issue_number": issue_number,
192 "issue_url": issue_url,
193 "message": f"✅ GitHub Issue #{issue_number} created successfully",
194 "title": full_title,
195 "labels": labels,
196 }
198 except subprocess.TimeoutExpired:
199 raise RuntimeError("GitHub issue creation timed out")
200 except Exception as e:
201 raise RuntimeError(f"Error creating GitHub issue: {e}")
203 def _build_body(self, config: IssueConfig) -> str:
204 """
205 Build the issue body based on issue type.
207 Args:
208 config: Issue configuration
210 Returns:
211 Formatted issue body
212 """
213 body = config.description
215 # Add metadata footer
216 footer = "\n\n---\n\n"
217 footer += f"**Type**: {config.issue_type.value} \n"
218 footer += f"**Priority**: {config.priority.value} \n"
219 if config.category:
220 footer += f"**Category**: {config.category} \n"
221 footer += "**Created via**: `/alfred:9-feedback`"
223 return body + footer
225 @staticmethod
226 def _extract_issue_number(url: str) -> int:
227 """
228 Extract issue number from GitHub URL.
230 Args:
231 url: GitHub issue URL
233 Returns:
234 Issue number
236 Raises:
237 ValueError: If unable to extract issue number
238 """
239 try:
240 # URL format: https://github.com/owner/repo/issues/123
241 return int(url.strip().split("/")[-1])
242 except (ValueError, IndexError):
243 raise ValueError(f"Unable to extract issue number from URL: {url}")
245 def format_result(self, result: Dict[str, Any]) -> str:
246 """
247 Format the issue creation result for display.
249 Args:
250 result: Issue creation result
252 Returns:
253 Formatted result string
254 """
255 if result["success"]:
256 output = f"{result['message']}\n"
257 output += f"📋 Title: {result['title']}\n"
258 output += f"🔗 URL: {result['issue_url']}\n"
259 if result.get("labels"):
260 output += f"🏷️ Labels: {', '.join(result['labels'])}\n"
261 return output
262 else:
263 return (
264 f"❌ Failed to create issue: {result.get('message', 'Unknown error')}"
265 )
268class IssueCreatorFactory:
269 """
270 Factory for creating issue creators with predefined configurations.
271 """
273 @staticmethod
274 def create_bug_issue(
275 title: str, description: str, priority: IssuePriority = IssuePriority.HIGH
276 ) -> IssueConfig:
277 """Create a bug report issue configuration."""
278 return IssueConfig(
279 issue_type=IssueType.BUG,
280 title=title,
281 description=description,
282 priority=priority,
283 category="Bug Report",
284 )
286 @staticmethod
287 def create_feature_issue(
288 title: str, description: str, priority: IssuePriority = IssuePriority.MEDIUM
289 ) -> IssueConfig:
290 """Create a feature request issue configuration."""
291 return IssueConfig(
292 issue_type=IssueType.FEATURE,
293 title=title,
294 description=description,
295 priority=priority,
296 category="Feature Request",
297 )
299 @staticmethod
300 def create_improvement_issue(
301 title: str, description: str, priority: IssuePriority = IssuePriority.MEDIUM
302 ) -> IssueConfig:
303 """Create an improvement issue configuration."""
304 return IssueConfig(
305 issue_type=IssueType.IMPROVEMENT,
306 title=title,
307 description=description,
308 priority=priority,
309 category="Improvement",
310 )
312 @staticmethod
313 def create_question_issue(
314 title: str, description: str, priority: IssuePriority = IssuePriority.LOW
315 ) -> IssueConfig:
316 """Create a question/discussion issue configuration."""
317 return IssueConfig(
318 issue_type=IssueType.QUESTION,
319 title=title,
320 description=description,
321 priority=priority,
322 category="Question",
323 )