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

1""" 

2GitHub Issue Creator for MoAI-ADK quick issue reporting. 

3 

4Enables users to quickly create GitHub Issues with standardized templates 

5using `/alfred:9-feedback` interactive dialog. 

6 

7""" 

8 

9import subprocess 

10from dataclasses import dataclass 

11from enum import Enum 

12from typing import Any, Dict, List, Optional 

13 

14 

15class IssueType(Enum): 

16 """Supported GitHub issue types.""" 

17 

18 BUG = "bug" 

19 FEATURE = "feature" 

20 IMPROVEMENT = "improvement" 

21 QUESTION = "question" 

22 

23 

24class IssuePriority(Enum): 

25 """Issue priority levels.""" 

26 

27 CRITICAL = "critical" 

28 HIGH = "high" 

29 MEDIUM = "medium" 

30 LOW = "low" 

31 

32 

33@dataclass 

34class IssueConfig: 

35 """Configuration for issue creation.""" 

36 

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 

44 

45 

46class GitHubIssueCreator: 

47 """ 

48 Creates GitHub Issues using the `gh` CLI. 

49 

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 """ 

57 

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 } 

68 

69 # Priority emoji 

70 PRIORITY_EMOJI = { 

71 IssuePriority.CRITICAL: "🔴", 

72 IssuePriority.HIGH: "🟠", 

73 IssuePriority.MEDIUM: "🟡", 

74 IssuePriority.LOW: "🟢", 

75 } 

76 

77 # Issue type emoji 

78 TYPE_EMOJI = { 

79 IssueType.BUG: "🐛", 

80 IssueType.FEATURE: "✨", 

81 IssueType.IMPROVEMENT: "⚡", 

82 IssueType.QUESTION: "❓", 

83 } 

84 

85 def __init__(self, github_token: Optional[str] = None): 

86 """ 

87 Initialize the GitHub Issue Creator. 

88 

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() 

94 

95 def _check_gh_cli(self) -> None: 

96 """ 

97 Check if `gh` CLI is installed and accessible. 

98 

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 ) 

116 

117 def create_issue(self, config: IssueConfig) -> Dict[str, Any]: 

118 """ 

119 Create a GitHub issue with the given configuration. 

120 

121 Args: 

122 config: Issue configuration 

123 

124 Returns: 

125 Dictionary containing issue creation result: 

126 { 

127 "success": bool, 

128 "issue_number": int, 

129 "issue_url": str, 

130 "message": str 

131 } 

132 

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}" 

142 

143 # Build body with template 

144 body = self._build_body(config) 

145 

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) 

156 

157 # Build gh command 

158 gh_command = [ 

159 "gh", 

160 "issue", 

161 "create", 

162 "--title", 

163 full_title, 

164 "--body", 

165 body, 

166 ] 

167 

168 # Add labels 

169 if labels: 

170 gh_command.extend(["--label", ",".join(set(labels))]) 

171 

172 # Add assignees if provided 

173 if config.assignees: 

174 gh_command.extend(["--assignee", ",".join(config.assignees)]) 

175 

176 try: 

177 result = subprocess.run( 

178 gh_command, capture_output=True, text=True, timeout=30 

179 ) 

180 

181 if result.returncode != 0: 

182 error_msg = result.stderr or result.stdout 

183 raise RuntimeError(f"Failed to create GitHub issue: {error_msg}") 

184 

185 # Parse issue URL from output 

186 issue_url = result.stdout.strip() 

187 issue_number = self._extract_issue_number(issue_url) 

188 

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 } 

197 

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}") 

202 

203 def _build_body(self, config: IssueConfig) -> str: 

204 """ 

205 Build the issue body based on issue type. 

206 

207 Args: 

208 config: Issue configuration 

209 

210 Returns: 

211 Formatted issue body 

212 """ 

213 body = config.description 

214 

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`" 

222 

223 return body + footer 

224 

225 @staticmethod 

226 def _extract_issue_number(url: str) -> int: 

227 """ 

228 Extract issue number from GitHub URL. 

229 

230 Args: 

231 url: GitHub issue URL 

232 

233 Returns: 

234 Issue number 

235 

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}") 

244 

245 def format_result(self, result: Dict[str, Any]) -> str: 

246 """ 

247 Format the issue creation result for display. 

248 

249 Args: 

250 result: Issue creation result 

251 

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 ) 

266 

267 

268class IssueCreatorFactory: 

269 """ 

270 Factory for creating issue creators with predefined configurations. 

271 """ 

272 

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 ) 

285 

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 ) 

298 

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 ) 

311 

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 )