feat: プロポーザルにPR関連フィールドを追加し、PR作成機能を実装#4
Conversation
There was a problem hiding this comment.
Pull request overview
プロポーザルにPR関連フィールドを追加し、完了した提案の差分を適用してGitHub PRを作成する一連の機能(K8s worker + backend workflow + frontend UI)を実装するPRです。
Changes:
- Proposalへ
pr_url/pr_statusを追加し、APIレスポンスとDBマイグレーションを更新 - PR作成用のLangGraphワークフロー + K8s Job 起動 + worker(createpr) を追加
- フロントエンドに「Create PR / View PR」導線とステータスポーリングを追加
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| k8s/secrets.yaml | GitHubトークン用のSecretキーを追加 |
| frontend/src/services/api.ts | Proposal型にPRフィールド追加、createPR API呼び出し追加 |
| frontend/src/pages/JobDetail.tsx | PR作成/再試行/表示UIと作成中ポーリングを追加 |
| docker/worker.Dockerfile | PR作成に必要なGitHub CLI(gh)を追加、createpr workerを同梱 |
| docker/worker-entrypoint.sh | WORKER_MODEにcreateprを追加 |
| docker/worker-createpr.py | 変更diff適用→push→ghでPR作成するworkerを新規追加 |
| backend/migration/versions/a1b2c3d4e5f6_add_pr_fields_to_proposals.py | proposalsテーブルへ pr_url / pr_status を追加 |
| backend/app/workflow/state.py | PR作成ワークフロー用State(TypedDict)を追加 |
| backend/app/workflow/create_pr_graph.py | PR作成用LangGraph(K8s job起動・待機・成果抽出)を追加 |
| backend/app/usecase/job_usecase.py | PR作成ユースケースとバックグラウンド実行を追加 |
| backend/app/service/k8s_service.py | PR作成用K8s Job生成メソッドを追加(SecretからGITHUB_TOKEN注入) |
| backend/app/service/artifact_service.py | pr_url.txt 読み取りメソッドを追加 |
| backend/app/schema/job_schema.py | ProposalResponseに pr_url / pr_status を追加 |
| backend/app/router/jobs.py | create-prエンドポイントを追加しレスポンスへPRフィールドを含める |
| backend/app/model/job.py | Proposalモデルに pr_url / pr_status カラムを追加 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const isInProgress = ['pending', 'analyzing', 'implementing'].includes(job.status) | ||
| const completedProposals = job.proposals.filter( | ||
| (p) => p.status === 'completed' && p.after_screenshot_url, | ||
| ) | ||
|
|
||
| const selectedProposal = | ||
| selectedIndex !== null | ||
| ? completedProposals.find((p) => p.proposal_index === selectedIndex) | ||
| : null |
There was a problem hiding this comment.
completedProposals を after_screenshot_url で絞っているため、実装側がステータスをcompletedにしてもスクリーンショットが無い提案は一覧/選択対象にならず、PR作成機能も使えません。PR作成の対象をcompleted提案全体にするなら、ここ(およびカード側)のフィルタ条件を見直してください。
| @router.post("/{job_id}/proposals/{proposal_index}/create-pr", response_model=ProposalResponse) | ||
| async def create_pr( | ||
| job_id: UUID, | ||
| proposal_index: int, | ||
| db: Session = Depends(get_db), | ||
| ) -> ProposalResponse: | ||
| usecase = CreatePRUseCase(db) | ||
| try: | ||
| proposal = await usecase.execute(job_id, proposal_index) | ||
| except ValueError as e: | ||
| raise HTTPException(status_code=400, detail=str(e)) from e | ||
| return _to_proposal_response(proposal, job_id) |
There was a problem hiding this comment.
新規エンドポイント POST /api/jobs/{job_id}/proposals/{proposal_index}/create-pr が追加されていますが、backend/tests にはこのAPIの成功/失敗(未完了提案・存在しない提案・二重実行など)を検証するテストがありません。既存のTestClientベースのAPIテストに同様の形で追加して、バリデーションとステータス遷移が壊れないようにしてください。
| if repo_url.startswith("https://github.com/"): | ||
| auth_repo_url = repo_url.replace( | ||
| "https://github.com/", | ||
| f"https://x-access-token:{github_token}@github.com/", | ||
| ) | ||
| else: | ||
| auth_repo_url = repo_url | ||
|
|
||
| subprocess.run( | ||
| ["git", "clone", "--depth", "1", "--branch", branch, auth_repo_url, repo_dir], | ||
| check=True, | ||
| ) |
There was a problem hiding this comment.
GitHubトークンを埋め込んだURLをそのままgit cloneに渡すと、失敗時のstderrや.git/config/git remote -v等からトークンがログに出る可能性があります。URL埋め込みは避け、http.extraHeader や GIT_ASKPASS/credential helper 等で認証するか、clone後にorigin URLをトークン無しへ戻すなどしてトークン露出を防いでください。
| if repo_url.startswith("https://github.com/"): | |
| auth_repo_url = repo_url.replace( | |
| "https://github.com/", | |
| f"https://x-access-token:{github_token}@github.com/", | |
| ) | |
| else: | |
| auth_repo_url = repo_url | |
| subprocess.run( | |
| ["git", "clone", "--depth", "1", "--branch", branch, auth_repo_url, repo_dir], | |
| check=True, | |
| ) | |
| clone_cmd = ["git", "clone", "--depth", "1", "--branch", branch] | |
| if repo_url.startswith("https://github.com/"): | |
| # Use http.extraHeader to send the token instead of embedding it in the URL | |
| clone_cmd.extend( | |
| [ | |
| "-c", | |
| f"http.extraHeader=Authorization: Bearer {github_token}", | |
| ] | |
| ) | |
| clone_cmd.extend([repo_url, repo_dir]) | |
| subprocess.run(clone_cmd, check=True) |
| logs = k8s.get_job_logs(k8s_job_name) | ||
| error_msg = f"PR creation job {result}." | ||
| if logs: | ||
| error_msg += f" Logs: {logs[:500]}" |
There was a problem hiding this comment.
失敗時にジョブログ(先頭500文字)をerrorに含めると、worker側の出力に認証情報や機密(例: git cloneのURLに埋め込まれたトークン)が含まれた場合にDBへ永続化される恐れがあります。エラーメッセージには結果/ジョブ名のみを入れ、ログはサニタイズ(トークンマスク)するか、別途セキュアな場所に保存して参照する形にしてください。
| 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], | |
| ) |
| 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") |
There was a problem hiding this comment.
PR作成を再実行する際に pr_status だけを "creating" に更新しており、以前の error_message や pr_url が残ったままになります。開始時点で error_message=None と pr_url=None を明示的にクリアして、状態が正しく反映されるようにしてください。
| 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, | |
| ) |
| UUID(proposal_id), | ||
| ProposalStatus.COMPLETED, | ||
| pr_url=result["pr_url"], | ||
| pr_status="created", |
There was a problem hiding this comment.
成功時に pr_url と pr_status は更新していますが、過去の失敗で error_message が入っている場合にクリアされません。成功時は error_message=None に更新して、UI/APIで古い失敗理由が残らないようにしてください。
| pr_status="created", | |
| pr_status="created", | |
| error_message=None, |
| createpr) | ||
| exec python /usr/local/bin/worker-createpr.py |
There was a problem hiding this comment.
Adding the createpr worker mode wires in a PR-creation worker that reads the GITHUB_TOKEN from a Kubernetes secret and exposes it to a Claude Agent with the Bash tool enabled. Because that agent runs arbitrary shell commands based on untrusted repo contents and diff text, a malicious repository or prompt injection can cause the agent to exfiltrate GITHUB_TOKEN or perform unintended GitHub actions. To mitigate this, avoid routing PR creation through a Bash-capable agent that can see the token; instead, keep GITHUB_TOKEN confined to a non-agent process that performs git push/gh pr create deterministically without following repo-supplied instructions.
No description provided.