Files
markitect-main/tddai/issue_writer.py
tegwick 64286b138d fix: Resolve label assignment issue using dedicated Gitea API endpoint
- Update ProjectManager.set_issue_state() to use /issues/{id}/labels endpoint with PUT method
- Update ProjectManager.set_issue_priority() to use dedicated labels endpoint
- Update IssueWriter.update_labels() to use dedicated labels endpoint for reliability
- Fix API format incompatibility where issue PATCH endpoint was ignoring label updates
- Label assignment now works correctly with proper state and priority management
- Issues will now properly appear in correct Kanban columns based on status labels

Root cause: Gitea API issue PATCH endpoint silently ignores label updates, but the
dedicated labels endpoint (/issues/{id}/labels) with PUT method works correctly.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-25 00:31:37 +02:00

180 lines
7.1 KiB
Python

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