-
Notifications
You must be signed in to change notification settings - Fork 0
feat: プロポーザルにPR関連フィールドを追加し、PR作成機能を実装 #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,6 +12,7 @@ | |||||||||||||||||
| from app.repository.proposal_repository import ProposalRepository | ||||||||||||||||||
| from app.service.artifact_service import ArtifactService | ||||||||||||||||||
| from app.workflow.analyzer_graph import build_analyzer_graph | ||||||||||||||||||
| from app.workflow.create_pr_graph import build_create_pr_graph | ||||||||||||||||||
| from app.workflow.implementation_graph import build_implementation_graph | ||||||||||||||||||
|
|
||||||||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||||||||
|
|
@@ -281,3 +282,118 @@ def _check_job_completion(db: Session, job_id: UUID) -> None: | |||||||||||||||||
| new_status = JobStatus.COMPLETED if any_succeeded else JobStatus.FAILED | ||||||||||||||||||
| job_repo.update_status(job_id, new_status) | ||||||||||||||||||
| logger.info("Job %s completed with status: %s", job_id, new_status) | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| class CreatePRUseCase: | ||||||||||||||||||
| """Trigger PR creation for a completed proposal.""" | ||||||||||||||||||
|
|
||||||||||||||||||
| def __init__(self, db: Session) -> None: | ||||||||||||||||||
| self.db = db | ||||||||||||||||||
| self.job_repo = JobRepository(db) | ||||||||||||||||||
| self.proposal_repo = ProposalRepository(db) | ||||||||||||||||||
|
|
||||||||||||||||||
| async def execute(self, job_id: UUID, proposal_index: int) -> Proposal: | ||||||||||||||||||
| job = self.job_repo.get_by_id(job_id) | ||||||||||||||||||
| if not job: | ||||||||||||||||||
| raise ValueError("Job not found") | ||||||||||||||||||
|
|
||||||||||||||||||
| proposal = self.proposal_repo.get_by_job_and_index(job_id, proposal_index) | ||||||||||||||||||
| if not proposal: | ||||||||||||||||||
| raise ValueError(f"Proposal {proposal_index} not found") | ||||||||||||||||||
| if proposal.status != ProposalStatus.COMPLETED: | ||||||||||||||||||
| raise ValueError(f"Proposal is not completed: {proposal.status}") | ||||||||||||||||||
| if proposal.pr_status == "created": | ||||||||||||||||||
| raise ValueError("PR already created") | ||||||||||||||||||
| if proposal.pr_status == "creating": | ||||||||||||||||||
| raise ValueError("PR creation already in progress") | ||||||||||||||||||
|
|
||||||||||||||||||
| proposal_id = UUID(str(proposal.id)) | ||||||||||||||||||
| self.proposal_repo.update_status(proposal_id, proposal.status, pr_status="creating") | ||||||||||||||||||
|
||||||||||||||||||
| self.proposal_repo.update_status(proposal_id, proposal.status, pr_status="creating") | |
| self.proposal_repo.update_status( | |
| proposal_id, | |
| proposal.status, | |
| pr_status="creating", | |
| error_message=None, | |
| pr_url=None, | |
| ) |
Copilot
AI
Feb 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
成功時に pr_url と pr_status は更新していますが、過去の失敗で error_message が入っている場合にクリアされません。成功時は error_message=None に更新して、UI/APIで古い失敗理由が残らないようにしてください。
| pr_status="created", | |
| pr_status="created", | |
| error_message=None, |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,81 @@ | ||||||||||||||||||||
| import logging | ||||||||||||||||||||
| from typing import Any | ||||||||||||||||||||
|
|
||||||||||||||||||||
| from langgraph.graph import END, START, StateGraph | ||||||||||||||||||||
|
|
||||||||||||||||||||
| from app.service.artifact_service import ArtifactService | ||||||||||||||||||||
| from app.service.k8s_service import K8sService | ||||||||||||||||||||
| from app.workflow.state import CreatePRState | ||||||||||||||||||||
|
|
||||||||||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| async def create_k8s_job(state: CreatePRState) -> dict: | ||||||||||||||||||||
| """Create the K8s Job for PR creation.""" | ||||||||||||||||||||
| k8s = K8sService() | ||||||||||||||||||||
| job_name = k8s.create_pr_job( | ||||||||||||||||||||
| job_id=state["job_id"], | ||||||||||||||||||||
| repo_url=state["repo_url"], | ||||||||||||||||||||
| branch=state["branch"], | ||||||||||||||||||||
| proposal_index=state["proposal_index"], | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| return {"k8s_job_name": job_name, "status": "running"} | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| async def wait_for_job(state: CreatePRState) -> dict: | ||||||||||||||||||||
| """Poll K8s Job until completion.""" | ||||||||||||||||||||
| k8s = K8sService() | ||||||||||||||||||||
| k8s_job_name = state["k8s_job_name"] | ||||||||||||||||||||
| assert k8s_job_name is not None | ||||||||||||||||||||
| result = await k8s.wait_for_job(k8s_job_name) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if result == "succeeded": | ||||||||||||||||||||
| return {"status": "succeeded"} | ||||||||||||||||||||
|
|
||||||||||||||||||||
| logs = k8s.get_job_logs(k8s_job_name) | ||||||||||||||||||||
| error_msg = f"PR creation job {result}." | ||||||||||||||||||||
| if logs: | ||||||||||||||||||||
| error_msg += f" Logs: {logs[:500]}" | ||||||||||||||||||||
|
||||||||||||||||||||
| error_msg += f" Logs: {logs[:500]}" | |
| # Do not include raw logs in the error field to avoid leaking sensitive information. | |
| # Log truncated logs for operational debugging instead. | |
| logger.error( | |
| "PR creation job %s for k8s_job_name=%s failed. Logs (first 500 chars): %s", | |
| result, | |
| k8s_job_name, | |
| logs[:500], | |
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| """add pr fields to proposals | ||
|
|
||
| Revision ID: a1b2c3d4e5f6 | ||
| Revises: 199f015d6e7d | ||
| Create Date: 2026-02-24 00:00:00.000000 | ||
|
|
||
| """ | ||
| from typing import Sequence, Union | ||
|
|
||
| from alembic import op | ||
| import sqlalchemy as sa | ||
|
|
||
|
|
||
| # revision identifiers, used by Alembic. | ||
| revision: str = 'a1b2c3d4e5f6' | ||
| down_revision: Union[str, None] = '199f015d6e7d' | ||
| branch_labels: Union[str, Sequence[str], None] = None | ||
| depends_on: Union[str, Sequence[str], None] = None | ||
|
|
||
|
|
||
| def upgrade() -> None: | ||
| """Add pr_url and pr_status columns to proposals table.""" | ||
| op.add_column('proposals', sa.Column('pr_url', sa.String(length=500), nullable=True)) | ||
| op.add_column('proposals', sa.Column('pr_status', sa.String(length=20), nullable=True)) | ||
|
|
||
|
|
||
| def downgrade() -> None: | ||
| """Remove pr_url and pr_status columns from proposals table.""" | ||
| op.drop_column('proposals', 'pr_status') | ||
| op.drop_column('proposals', 'pr_url') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
新規エンドポイント
POST /api/jobs/{job_id}/proposals/{proposal_index}/create-prが追加されていますが、backend/tests にはこのAPIの成功/失敗(未完了提案・存在しない提案・二重実行など)を検証するテストがありません。既存のTestClientベースのAPIテストに同様の形で追加して、バリデーションとステータス遷移が壊れないようにしてください。