markitect-main/tddai/issue_writer.py

180 lines
7.1 KiB
Python
Raw Normal View History

"""
Issue writing to Gitea API.
"""
import json
import os
import subprocess
from subprocess import PIPE
from typing import Dict, Any, Optional
from .config import get_config
from .exceptions import IssueError
class IssueWriter:
"""Writes issue updates to Gitea API."""
def __init__(self, config=None, auth_token=None):
self.config = config or get_config()
feat: Implement comprehensive IssueCreator system and create CLI roadmap issues 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>
2025-09-24 23:36:07 +02:00
self.auth_token = auth_token or os.getenv('GITEA_API_TOKEN')
def update_issue(self, issue_number: int, update_data: Dict[str, Any]) -> Dict[str, Any]:
"""Update an issue via PATCH operation."""
if not self.auth_token:
raise IssueError("Authentication token required for issue updates")
url = f"{self.config.issues_api_url}/{issue_number}"
try:
# Prepare curl command with authentication
curl_cmd = [
'curl', '-s', '-X', 'PATCH',
'-H', 'Content-Type: application/json',
'-H', f'Authorization: token {self.auth_token}',
'-d', json.dumps(update_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 update issue #{issue_number}: {result.stderr}")
response_data = json.loads(result.stdout)
if 'message' in response_data and 'number' not in response_data:
raise IssueError(f"Failed to update issue #{issue_number}: {response_data['message']}")
return response_data
except subprocess.CalledProcessError as e:
raise IssueError(f"Failed to update issue #{issue_number}: {e}")
except json.JSONDecodeError as e:
raise IssueError(f"Failed to parse response data: {e}")
def update_issue_title(self, issue_number: int, new_title: str) -> Dict[str, Any]:
"""Update only the title of an issue."""
return self.update_issue(issue_number, {'title': new_title})
def update_issue_body(self, issue_number: int, new_body: str) -> Dict[str, Any]:
"""Update only the body of an issue."""
return self.update_issue(issue_number, {'body': new_body})
def update_issue_state(self, issue_number: int, new_state: str) -> Dict[str, Any]:
"""Update only the state of an issue (open/closed)."""
if new_state not in ['open', 'closed']:
raise IssueError(f"Invalid state '{new_state}'. Must be 'open' or 'closed'")
return self.update_issue(issue_number, {'state': new_state})
def close_issue(self, issue_number: int) -> Dict[str, Any]:
"""Close an issue."""
return self.update_issue_state(issue_number, 'closed')
def reopen_issue(self, issue_number: int) -> Dict[str, Any]:
"""Reopen a closed issue."""
return self.update_issue_state(issue_number, 'open')
def assign_to_milestone(self, issue_number: int, milestone_id: int) -> Dict[str, Any]:
"""Assign issue to a milestone (project)."""
return self.update_issue(issue_number, {'milestone': milestone_id})
def remove_from_milestone(self, issue_number: int) -> Dict[str, Any]:
"""Remove issue from its current milestone."""
return self.update_issue(issue_number, {'milestone': None})
def update_labels(self, issue_number: int, labels: list) -> Dict[str, Any]:
"""Update issue labels completely using dedicated labels endpoint."""
if not self.auth_token:
raise IssueError("Authentication token required for label updates")
# Use the dedicated labels endpoint which works more reliably
url = f"{self.config.issues_api_url}/{issue_number}/labels"
try:
# Use PUT to replace all labels
curl_cmd = [
'curl', '-s', '-X', 'PUT',
'-H', 'Content-Type: application/json',
'-H', f'Authorization: token {self.auth_token}',
'-d', json.dumps({'labels': labels}),
url
]
result = subprocess.run(
curl_cmd,
stdout=PIPE,
stderr=PIPE,
universal_newlines=True,
check=True
)
if result.returncode != 0:
raise IssueError(f"Failed to update labels for issue #{issue_number}: {result.stderr}")
# Parse the response - labels endpoint returns array of labels
if result.stdout.strip():
response_data = json.loads(result.stdout)
# Convert labels response back to issue format for consistency
return {
'number': issue_number,
'labels': response_data if isinstance(response_data, list) else []
}
else:
return {'number': issue_number, 'labels': []}
except subprocess.CalledProcessError as e:
raise IssueError(f"Failed to update labels for issue #{issue_number}: {e}")
except json.JSONDecodeError as e:
raise IssueError(f"Failed to parse labels response: {e}")
def add_labels(self, issue_number: int, new_labels: list) -> Dict[str, Any]:
"""Add labels to issue (preserving existing labels)."""
# First get current labels
url = f"{self.config.issues_api_url}/{issue_number}"
curl_cmd = [
'curl', '-s', '-X', 'GET',
'-H', f'Authorization: token {self.auth_token}',
url
]
try:
result = subprocess.run(curl_cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True, check=True)
issue_data = json.loads(result.stdout)
current_labels = [label['name'] for label in issue_data.get('labels', [])]
# Add new labels (avoid duplicates)
updated_labels = list(set(current_labels + new_labels))
return self.update_labels(issue_number, updated_labels)
except (subprocess.CalledProcessError, json.JSONDecodeError) as e:
raise IssueError(f"Failed to add labels to issue #{issue_number}: {e}")
def remove_labels(self, issue_number: int, labels_to_remove: list) -> Dict[str, Any]:
"""Remove specific labels from issue."""
# First get current labels
url = f"{self.config.issues_api_url}/{issue_number}"
curl_cmd = [
'curl', '-s', '-X', 'GET',
'-H', f'Authorization: token {self.auth_token}',
url
]
try:
result = subprocess.run(curl_cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True, check=True)
issue_data = json.loads(result.stdout)
current_labels = [label['name'] for label in issue_data.get('labels', [])]
# Remove specified labels
updated_labels = [label for label in current_labels if label not in labels_to_remove]
return self.update_labels(issue_number, updated_labels)
except (subprocess.CalledProcessError, json.JSONDecodeError) as e:
raise IssueError(f"Failed to remove labels from issue #{issue_number}: {e}")