IssueCreator Implementation: - Add tddai/issue_creator.py with full POST API functionality for issue creation - Support multiple creation methods: basic, enhancement, bug, template-based - Include structured issue formatting with acceptance criteria and dependencies - Template system with variable substitution for reusable issue creation Authentication Fix: - Fix critical authentication bug: use GITEA_API_TOKEN instead of GITEA_TOKEN - Update both IssueCreator and IssueWriter for consistency - Update all tests and documentation to reflect correct environment variable Comprehensive Test Suite: - Add 15 unit tests for IssueCreator (tests/test_issue_creator.py) - Add 5 integration tests for full API lifecycle (tests/test_issue_integration.py) - Create test_environment_variable_detection to prevent future auth issues - Total 33 tests covering complete issue handling workflow CLI Integration: - Enhance tddai_cli.py with 3 new commands: create-issue, create-enhancement, create-from-template - Add comprehensive argument parsing with optional fields and priority support - Include user-friendly output with next step guidance - Update package exports to include IssueCreator CLI Roadmap Execution: - Successfully create 8 CLI implementation issues (#12-#19) in Gitea - Resolve mismatch between NEXT.md roadmap and actual Gitea issues - Issues prioritized for core USPs: Database Query CLI and AST Query CLI - Remove local MISSING_ISSUES.md file after successful creation Framework Maturity: - Complete CRUD operations for issue management (Create, Read, Update, Delete) - Robust error handling and API integration patterns - Full authentication and environment variable management - Ready for production CLI implementation workflow 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
233 lines
7.3 KiB
Python
233 lines
7.3 KiB
Python
"""
|
|
Issue creation for Gitea API.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
from subprocess import PIPE
|
|
from typing import Dict, Any, Optional, List
|
|
|
|
from .config import get_config
|
|
from .exceptions import IssueError
|
|
|
|
|
|
class IssueCreator:
|
|
"""Creates new issues via Gitea API."""
|
|
|
|
def __init__(self, config=None, auth_token=None):
|
|
self.config = config or get_config()
|
|
self.auth_token = auth_token or os.getenv('GITEA_API_TOKEN')
|
|
|
|
def create_issue(self, title: str, body: str, **kwargs) -> Dict[str, Any]:
|
|
"""Create a new issue via POST operation.
|
|
|
|
Args:
|
|
title: Issue title (required)
|
|
body: Issue description/body (required)
|
|
**kwargs: Optional fields (assignees, milestone, labels, etc.)
|
|
|
|
Returns:
|
|
Dict containing created issue data including issue number
|
|
|
|
Raises:
|
|
IssueError: If creation fails
|
|
"""
|
|
if not self.auth_token:
|
|
raise IssueError("Authentication token required for issue creation")
|
|
|
|
if not title.strip():
|
|
raise IssueError("Issue title cannot be empty")
|
|
|
|
# Prepare issue data
|
|
issue_data = {
|
|
'title': title.strip(),
|
|
'body': body.strip() if body else ''
|
|
}
|
|
|
|
# Add optional fields
|
|
if 'assignees' in kwargs and kwargs['assignees']:
|
|
issue_data['assignees'] = kwargs['assignees']
|
|
|
|
if 'milestone' in kwargs and kwargs['milestone']:
|
|
issue_data['milestone'] = kwargs['milestone']
|
|
|
|
if 'labels' in kwargs and kwargs['labels']:
|
|
issue_data['labels'] = kwargs['labels']
|
|
|
|
url = self.config.issues_api_url
|
|
|
|
try:
|
|
# Prepare curl command with authentication
|
|
curl_cmd = [
|
|
'curl', '-s', '-X', 'POST',
|
|
'-H', 'Content-Type: application/json',
|
|
'-H', f'Authorization: token {self.auth_token}',
|
|
'-d', json.dumps(issue_data),
|
|
url
|
|
]
|
|
|
|
result = subprocess.run(
|
|
curl_cmd,
|
|
stdout=PIPE,
|
|
stderr=PIPE,
|
|
universal_newlines=True,
|
|
check=True
|
|
)
|
|
|
|
if result.returncode != 0:
|
|
raise IssueError(f"Failed to create issue: {result.stderr}")
|
|
|
|
response_data = json.loads(result.stdout)
|
|
|
|
# Check for API error responses
|
|
if 'message' in response_data and 'number' not in response_data:
|
|
raise IssueError(f"Failed to create issue: {response_data['message']}")
|
|
|
|
return response_data
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
raise IssueError(f"Failed to create issue: {e}")
|
|
except json.JSONDecodeError as e:
|
|
raise IssueError(f"Failed to parse response data: {e}")
|
|
|
|
def create_enhancement_issue(self, title: str, use_case: str,
|
|
technical_requirements: str = "",
|
|
acceptance_criteria: List[str] = None,
|
|
dependencies: List[str] = None,
|
|
priority: str = "Medium") -> Dict[str, Any]:
|
|
"""Create an enhancement issue with structured format.
|
|
|
|
Args:
|
|
title: Issue title
|
|
use_case: UseCase description
|
|
technical_requirements: Technical implementation details
|
|
acceptance_criteria: List of acceptance criteria
|
|
dependencies: List of dependency descriptions
|
|
priority: Priority level (High, Medium, Low)
|
|
|
|
Returns:
|
|
Dict containing created issue data
|
|
"""
|
|
# Build structured body
|
|
body_parts = [f"UseCase: {use_case}"]
|
|
|
|
if technical_requirements:
|
|
body_parts.extend([
|
|
"",
|
|
"Technical Requirements:",
|
|
technical_requirements
|
|
])
|
|
|
|
if acceptance_criteria:
|
|
body_parts.extend([
|
|
"",
|
|
"Acceptance Criteria:"
|
|
])
|
|
for criterion in acceptance_criteria:
|
|
body_parts.append(f"- [ ] {criterion}")
|
|
|
|
if dependencies:
|
|
body_parts.extend([
|
|
"",
|
|
"Dependencies:"
|
|
])
|
|
for dep in dependencies:
|
|
body_parts.append(f"- {dep}")
|
|
|
|
body = "\n".join(body_parts)
|
|
|
|
# Create with enhancement label
|
|
return self.create_issue(
|
|
title=title,
|
|
body=body,
|
|
labels=[priority.lower(), "enhancement"]
|
|
)
|
|
|
|
def create_bug_issue(self, title: str, description: str,
|
|
steps_to_reproduce: List[str] = None,
|
|
expected_behavior: str = "",
|
|
actual_behavior: str = "",
|
|
environment: str = "") -> Dict[str, Any]:
|
|
"""Create a bug issue with structured format.
|
|
|
|
Args:
|
|
title: Bug title
|
|
description: Bug description
|
|
steps_to_reproduce: List of reproduction steps
|
|
expected_behavior: What should happen
|
|
actual_behavior: What actually happens
|
|
environment: Environment details
|
|
|
|
Returns:
|
|
Dict containing created issue data
|
|
"""
|
|
body_parts = [description]
|
|
|
|
if steps_to_reproduce:
|
|
body_parts.extend([
|
|
"",
|
|
"Steps to Reproduce:"
|
|
])
|
|
for i, step in enumerate(steps_to_reproduce, 1):
|
|
body_parts.append(f"{i}. {step}")
|
|
|
|
if expected_behavior:
|
|
body_parts.extend([
|
|
"",
|
|
f"Expected Behavior: {expected_behavior}"
|
|
])
|
|
|
|
if actual_behavior:
|
|
body_parts.extend([
|
|
"",
|
|
f"Actual Behavior: {actual_behavior}"
|
|
])
|
|
|
|
if environment:
|
|
body_parts.extend([
|
|
"",
|
|
f"Environment: {environment}"
|
|
])
|
|
|
|
body = "\n".join(body_parts)
|
|
|
|
# Create with bug label
|
|
return self.create_issue(
|
|
title=title,
|
|
body=body,
|
|
labels=["bug"]
|
|
)
|
|
|
|
def create_from_template(self, template_file: str, **template_vars) -> Dict[str, Any]:
|
|
"""Create issue from a template file.
|
|
|
|
Args:
|
|
template_file: Path to template file
|
|
**template_vars: Variables to substitute in template
|
|
|
|
Returns:
|
|
Dict containing created issue data
|
|
"""
|
|
try:
|
|
with open(template_file, 'r') as f:
|
|
template_content = f.read()
|
|
|
|
# Simple template variable substitution
|
|
for key, value in template_vars.items():
|
|
template_content = template_content.replace(f"{{{key}}}", str(value))
|
|
|
|
# Extract title (first line) and body (rest)
|
|
lines = template_content.strip().split('\n')
|
|
if not lines or (len(lines) == 1 and not lines[0].strip()):
|
|
raise IssueError("Template file is empty")
|
|
|
|
title = lines[0].replace('Title: ', '').strip()
|
|
body = '\n'.join(lines[1:]).strip()
|
|
|
|
return self.create_issue(title=title, body=body)
|
|
|
|
except FileNotFoundError:
|
|
raise IssueError(f"Template file not found: {template_file}")
|
|
except Exception as e:
|
|
raise IssueError(f"Failed to process template: {e}") |