Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions backend/src/github_pm/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from datetime import datetime
import re
import time
from typing import Annotated, Any, AsyncGenerator, NoReturn
from typing import Annotated, Any, AsyncGenerator, Literal, NoReturn
from urllib.parse import quote_plus

from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query
Expand Down Expand Up @@ -475,33 +475,38 @@ async def update_issue_body(
return updated


class CloseWithComment(BaseModel):
"""Body for closing an issue with an optional comment.
class CloseIssueRequest(BaseModel):
"""Body for closing an issue with a reason and optional comment.

Generated-by: Cursor
"""

body: str = Field(title="Comment Body", min_length=1)
reason: Literal["done", "obsolete"] = Field(title="Close Reason", default="done")
body: str | None = Field(title="Comment Body", default=None)


@api_router.post("/issues/{issue_number}/close-with-comment")
async def close_issue_with_comment(
gitctx: Annotated[Connector, Depends(connection)],
issue_number: Annotated[int, Path(title="Issue")],
comment: Annotated[CloseWithComment, Body(title="Comment")],
comment: Annotated[CloseIssueRequest, Body(title="Comment")],
):
"""Add a comment to an issue and mark it closed.

Generated-by: Cursor
"""
resolved_body = (comment.body or "").strip() or (
"Obsolete" if comment.reason == "obsolete" else "Done"
)
state_reason = "not_planned" if comment.reason == "obsolete" else "completed"
created_comment = gitctx.post(
f"/repos/{context.github_repo}/issues/{issue_number}/comments",
data={"body": comment.body},
data={"body": resolved_body},
headers=_GITHUB_BODY_ACCEPT,
)
closed_issue = gitctx.patch(
f"/repos/{context.github_repo}/issues/{issue_number}",
data={"state": "closed"},
data={"state": "closed", "state_reason": state_reason},
headers=_GITHUB_BODY_ACCEPT,
)
logger.info("Closed issue #%s with comment", issue_number)
Expand Down
40 changes: 36 additions & 4 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
api_router,
clear_issue_parent,
close_issue_with_comment,
CloseWithComment,
CloseIssueRequest,
connection,
Connector,
create_comment,
Expand Down Expand Up @@ -1995,14 +1995,46 @@ async def test_close_with_comment_success(self):
with patch("github_pm.api.context") as mock_context:
mock_context.github_repo = "test/repo"
result = await close_issue_with_comment(
mock_gitctx, 42, CloseWithComment(body="Done")
mock_gitctx, 42, CloseIssueRequest(reason="done", body="Done")
)

assert result == {"comment": mock_comment, "issue": mock_issue}
mock_gitctx.post.assert_called_once()
mock_gitctx.post.assert_called_once_with(
"/repos/test/repo/issues/42/comments",
data={"body": "Done"},
headers={"Accept": "application/vnd.github.full+json"},
)
mock_gitctx.patch.assert_called_once_with(
"/repos/test/repo/issues/42",
data={"state": "closed", "state_reason": "completed"},
headers={"Accept": "application/vnd.github.full+json"},
)

@pytest.mark.asyncio
async def test_close_with_obsolete_default_comment(self):
mock_comment = {"id": 1, "body": "Obsolete"}
mock_issue = {"number": 42, "state": "closed"}
mock_gitctx = Mock(spec=Connector)
mock_gitctx.post = Mock(return_value=mock_comment)
mock_gitctx.patch = Mock(return_value=mock_issue)

with patch("github_pm.api.context") as mock_context:
mock_context.github_repo = "test/repo"
result = await close_issue_with_comment(
mock_gitctx,
42,
CloseIssueRequest(reason="obsolete", body=" "),
)

assert result == {"comment": mock_comment, "issue": mock_issue}
mock_gitctx.post.assert_called_once_with(
"/repos/test/repo/issues/42/comments",
data={"body": "Obsolete"},
headers={"Accept": "application/vnd.github.full+json"},
)
mock_gitctx.patch.assert_called_once_with(
"/repos/test/repo/issues/42",
data={"state": "closed"},
data={"state": "closed", "state_reason": "not_planned"},
headers={"Accept": "application/vnd.github.full+json"},
)

Expand Down
Loading
Loading