From 816415dd24053b2a67ab29ccc1be901bde65b3a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A8=B8=EB=8B=88=ED=8E=98=EB=8B=88?= Date: Fri, 20 Mar 2026 17:44:45 +0900 Subject: [PATCH] feat: complete PR creation via GiteaClient in commit_and_open_pr and open_pr --- agent/middleware/open_pr.py | 18 ++++- agent/tools/commit_and_open_pr.py | 39 ++++++++- tests/test_commit_and_open_pr.py | 129 ++++++++++++++++++++++++++++++ 3 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 tests/test_commit_and_open_pr.py diff --git a/agent/middleware/open_pr.py b/agent/middleware/open_pr.py index a6a48ea..211452d 100644 --- a/agent/middleware/open_pr.py +++ b/agent/middleware/open_pr.py @@ -16,6 +16,7 @@ from langchain.agents.middleware import AgentState, after_agent from langgraph.config import get_config from langgraph.runtime import Runtime +from ..utils.gitea_client import get_gitea_client from ..utils.git_utils import ( git_add_all, git_checkout_branch, @@ -135,8 +136,21 @@ async def open_pr_if_needed( git_push, sandbox_backend, repo_dir, target_branch, gitea_token ) - # TODO: Phase 2 - use GiteaClient to create PR via Gitea API - logger.info("Pushed to branch %s, PR creation pending Gitea integration", target_branch) + # --- PR 생성 (GiteaClient) --- + default_branch = os.environ.get("DEFAULT_BRANCH", "main") + client = get_gitea_client() + try: + pr_result = await client.create_pull_request( + owner=repo_owner, + repo=repo_name, + title=pr_title, + head=target_branch, + base=default_branch, + body=pr_body, + ) + logger.info("Safety net PR created: %s", pr_result.get("html_url")) + except Exception: + logger.exception("Safety net PR creation failed (changes were pushed)") logger.info("After-agent middleware completed successfully") diff --git a/agent/tools/commit_and_open_pr.py b/agent/tools/commit_and_open_pr.py index 5f5b66f..b892ae5 100644 --- a/agent/tools/commit_and_open_pr.py +++ b/agent/tools/commit_and_open_pr.py @@ -1,9 +1,12 @@ import asyncio import logging +import os from typing import Any from langgraph.config import get_config +from agent.utils.gitea_client import get_gitea_client + from ..utils.git_utils import ( git_add_all, git_checkout_branch, @@ -95,7 +98,6 @@ def commit_and_open_pr( "pr_url": None, } - import os gitea_token = os.environ.get("GITEA_TOKEN", "") if not gitea_token: logger.error("commit_and_open_pr missing Gitea token for thread %s", thread_id) @@ -113,8 +115,39 @@ def commit_and_open_pr( "pr_url": None, } - # TODO: Phase 2 - use GiteaClient to create PR - return {"success": True, "pr_url": "pending-gitea-implementation"} + # --- PR 생성 (GiteaClient) --- + gitea_external_url = os.environ.get("GITEA_EXTERNAL_URL", "") + gitea_internal_url = os.environ.get("GITEA_URL", "http://gitea:3000") + default_branch = os.environ.get("DEFAULT_BRANCH", "main") + client = get_gitea_client() + + try: + pr_result = asyncio.run( + client.create_pull_request( + owner=repo_owner, + repo=repo_name, + title=title, + head=target_branch, + base=default_branch, + body=body, + ) + ) + pr_url = pr_result.get("html_url", "") + if gitea_external_url and pr_url: + pr_url = pr_url.replace(gitea_internal_url, gitea_external_url) + + return { + "success": True, + "pr_url": pr_url, + "pr_number": pr_result.get("number"), + } + except Exception as e: + logger.exception("Failed to create PR (push succeeded)") + return { + "success": True, + "pr_url": "", + "error": f"Push succeeded but PR creation failed: {e}", + } except Exception as e: logger.exception("commit_and_open_pr failed") diff --git a/tests/test_commit_and_open_pr.py b/tests/test_commit_and_open_pr.py new file mode 100644 index 0000000..5185df9 --- /dev/null +++ b/tests/test_commit_and_open_pr.py @@ -0,0 +1,129 @@ +import pytest +from unittest.mock import AsyncMock, MagicMock, patch + + +def test_pr_creation_after_push(): + """push 성공 후 GiteaClient로 PR을 생성한다.""" + mock_gitea = MagicMock() + mock_gitea.create_pull_request = AsyncMock( + return_value={ + "number": 1, + "html_url": "http://gitea:3000/quant/galaxis-po/pulls/1", + } + ) + mock_sandbox = MagicMock() + mock_result = MagicMock(exit_code=0, output="") + + with patch( + "agent.tools.commit_and_open_pr.get_gitea_client", return_value=mock_gitea + ), patch( + "agent.tools.commit_and_open_pr.get_sandbox_backend_sync", + return_value=mock_sandbox, + ), patch( + "agent.tools.commit_and_open_pr.get_config", + return_value={ + "configurable": { + "thread_id": "test-thread", + "repo": {"owner": "quant", "name": "galaxis-po"}, + } + }, + ), patch( + "agent.tools.commit_and_open_pr.resolve_repo_dir", + return_value="/workspace/galaxis-po", + ), patch( + "agent.tools.commit_and_open_pr.git_has_uncommitted_changes", + return_value=True, + ), patch( + "agent.tools.commit_and_open_pr.git_fetch_origin", + ), patch( + "agent.tools.commit_and_open_pr.git_has_unpushed_commits", + return_value=False, + ), patch( + "agent.tools.commit_and_open_pr.git_current_branch", + return_value="galaxis-agent/test-thread", + ), patch( + "agent.tools.commit_and_open_pr.git_checkout_branch", + ), patch( + "agent.tools.commit_and_open_pr.git_config_user", + ), patch( + "agent.tools.commit_and_open_pr.git_add_all", + ), patch( + "agent.tools.commit_and_open_pr.git_commit", + return_value=mock_result, + ), patch( + "agent.tools.commit_and_open_pr.git_push", + return_value=mock_result, + ), patch.dict( + "os.environ", {"GITEA_TOKEN": "test-token"}, + ): + from agent.tools.commit_and_open_pr import commit_and_open_pr + result = commit_and_open_pr(title="feat: add feature", body="PR description") + assert result["success"] is True + assert "pulls/1" in result["pr_url"] + mock_gitea.create_pull_request.assert_called_once() + + +def test_pr_creation_converts_internal_to_external_url(): + """PR URL이 내부 URL에서 외부 URL로 변환된다.""" + mock_gitea = MagicMock() + mock_gitea.create_pull_request = AsyncMock( + return_value={ + "number": 5, + "html_url": "http://gitea:3000/quant/galaxis-po/pulls/5", + } + ) + mock_sandbox = MagicMock() + mock_result = MagicMock(exit_code=0, output="") + + with patch( + "agent.tools.commit_and_open_pr.get_gitea_client", return_value=mock_gitea + ), patch( + "agent.tools.commit_and_open_pr.get_sandbox_backend_sync", + return_value=mock_sandbox, + ), patch( + "agent.tools.commit_and_open_pr.get_config", + return_value={ + "configurable": { + "thread_id": "test-thread", + "repo": {"owner": "quant", "name": "galaxis-po"}, + } + }, + ), patch( + "agent.tools.commit_and_open_pr.resolve_repo_dir", + return_value="/workspace/galaxis-po", + ), patch( + "agent.tools.commit_and_open_pr.git_has_uncommitted_changes", + return_value=True, + ), patch( + "agent.tools.commit_and_open_pr.git_fetch_origin", + ), patch( + "agent.tools.commit_and_open_pr.git_has_unpushed_commits", + return_value=False, + ), patch( + "agent.tools.commit_and_open_pr.git_current_branch", + return_value="galaxis-agent/test-thread", + ), patch( + "agent.tools.commit_and_open_pr.git_checkout_branch", + ), patch( + "agent.tools.commit_and_open_pr.git_config_user", + ), patch( + "agent.tools.commit_and_open_pr.git_add_all", + ), patch( + "agent.tools.commit_and_open_pr.git_commit", + return_value=mock_result, + ), patch( + "agent.tools.commit_and_open_pr.git_push", + return_value=mock_result, + ), patch.dict( + "os.environ", + { + "GITEA_TOKEN": "test-token", + "GITEA_EXTERNAL_URL": "https://ayuriel.duckdns.org", + "GITEA_URL": "http://gitea:3000", + }, + ): + from agent.tools.commit_and_open_pr import commit_and_open_pr + result = commit_and_open_pr(title="feat: test", body="body") + assert result["success"] is True + assert "ayuriel.duckdns.org" in result["pr_url"] + assert "gitea:3000" not in result["pr_url"]