diff --git a/.github/scripts/review_publishing.py b/.github/scripts/review_publishing.py index 65ffe4a..1eb42f3 100644 --- a/.github/scripts/review_publishing.py +++ b/.github/scripts/review_publishing.py @@ -60,7 +60,7 @@ class InlineReviewPlan: @dataclass(frozen=True) class InlinePublishResult: - created_review: bool + created_comments: bool posted: int updated: int resolved: int @@ -142,8 +142,13 @@ def pr_review_doc_path(pr_number: int) -> str: return f"{DOC_ROOT}/PR-{pr_number}.md" +def _pr_number_from_review_doc_path(path: str) -> int | None: + match = re.fullmatch(r"docs/pr-reviews/PR-([1-9][0-9]*)\.md", path.strip()) + return int(match.group(1)) if match else None + + def is_pr_review_doc_path(path: str) -> bool: - return bool(re.fullmatch(r"docs/pr-reviews/PR-[1-9][0-9]*\.md", path.strip())) + return _pr_number_from_review_doc_path(path) is not None def parse_right_side_lines(files: Sequence[Mapping[str, Any]]) -> dict[str, set[int]]: @@ -429,53 +434,47 @@ def publish_inline_review_comments( if not new_comments: return InlinePublishResult(False, 0, updated, resolved, plan.fallback_findings) - payload = { - "commit_id": commit_id, - "body": "WhyLog AI 자동 줄 단위 리뷰입니다.", - "event": "COMMENT", - "comments": [ - { - "path": comment.path, - "line": comment.line, - "side": "RIGHT", - "body": comment.body, - } - for comment in new_comments - ], - } - try: - github_request( - api_url, - token, - f"/repos/{repository}/pulls/{pr_number}/reviews", - method="POST", - payload=payload, - ) - except Exception as error: - if _is_status_error(error, 422): - fallback = list(plan.fallback_findings) - fallback.extend( - { - "kind": comment.kind, - "file": comment.path, + posted = 0 + fallback = list(plan.fallback_findings) + first_error: str | None = None + for comment in new_comments: + try: + github_request( + api_url, + token, + f"/repos/{repository}/pulls/{pr_number}/comments", + method="POST", + payload={ + "commit_id": commit_id, + "path": comment.path, "line": comment.line, - "fingerprint": comment.fingerprint, - "fallback_reason": "github_inline_review_422", - } - for comment in new_comments - ) - return InlinePublishResult( - created_review=False, - posted=0, - updated=updated, - resolved=resolved, - fallback_findings=tuple(fallback), - post_failed_fallback=str(error)[:500], + "side": "RIGHT", + "body": comment.body, + }, ) - raise + except Exception as error: + if _is_status_error(error, 422): + fallback.append( + { + "kind": comment.kind, + "file": comment.path, + "line": comment.line, + "fingerprint": comment.fingerprint, + "fallback_reason": "github_inline_comment_422", + } + ) + first_error = first_error or str(error)[:500] + continue + raise + posted += 1 return InlinePublishResult( - True, len(new_comments), updated, resolved, plan.fallback_findings + posted > 0, + posted, + updated, + resolved, + tuple(fallback), + first_error, ) @@ -985,16 +984,7 @@ def sync_pr_review_document( def _doc_commit_message(pr_number: int) -> str: - return ( - f"docs(docs): PR-{pr_number} 리뷰 판단 근거를 저장소에 남김\n\n" - "Constraint: CI AI review result must remain readable from repository docs\n" - "Rejected: Direct push to protected base with the default GITHUB_TOKEN | branch protection and auditability require branch-scoped document updates only\n" - "Confidence: medium\n" - "Scope-risk: narrow\n" - "Directive: Do not edit generated state markers by hand\n" - "Tested: AI review publishing workflow generated this document\n" - "Generated-By: whylog-ai-review\n" - ) + return f"docs(docs): PR-{pr_number} 리뷰 판단 근거 기록" def generated_doc_only_parent_sha( @@ -1022,8 +1012,6 @@ def generated_doc_only_parent_sha( raw_files = commit.get("files") if isinstance(raw_files, list): files = raw_files - if "Generated-By: whylog-ai-review" not in message: - return None paths = [ item.get("filename") for item in files @@ -1038,6 +1026,11 @@ def generated_doc_only_parent_sha( or not isinstance(parents[0], Mapping) ): return None + document_pr_number = _pr_number_from_review_doc_path(paths[0]) + if document_pr_number is None or message != _doc_commit_message( + document_pr_number + ): + return None return _string(parents[0].get("sha")) or None diff --git a/.github/scripts/test_ai_review.py b/.github/scripts/test_ai_review.py index 9053edc..c34de1e 100644 --- a/.github/scripts/test_ai_review.py +++ b/.github/scripts/test_ai_review.py @@ -262,7 +262,7 @@ def test_renders_inline_and_document_publish_status(self) -> None: ai_review.Review("요약", (), ()), ) inline = ai_review.review_publishing.InlinePublishResult( - created_review=True, + created_comments=True, posted=2, updated=1, resolved=1, @@ -391,7 +391,7 @@ def test_run_reviews_internal_public_pull_request( } inline_result = ai_review.review_publishing.InlinePublishResult( - created_review=False, + created_comments=False, posted=0, updated=0, resolved=0, diff --git a/.github/scripts/test_review_publishing.py b/.github/scripts/test_review_publishing.py index 7e837fa..a619e5a 100644 --- a/.github/scripts/test_review_publishing.py +++ b/.github/scripts/test_review_publishing.py @@ -168,7 +168,7 @@ def test_updates_duplicate_and_marks_stale_resolved(self): self.assertIn("/pulls/comments/10", github.calls[1][0]) self.assertIn("재검출되지 않음", github.calls[2][2]["body"]) - def test_posts_new_comments_as_one_review(self): + def test_posts_new_comments_individually_without_review_wrapper(self): github = FakeGitHub(responses=[[], {"id": 1}]) published = rp.publish_inline_review_comments( @@ -182,16 +182,18 @@ def test_posts_new_comments_as_one_review(self): github, ) - self.assertTrue(published.created_review) + self.assertTrue(published.created_comments) self.assertEqual(published.posted, 1) self.assertEqual( - github.calls[-1][0], "/repos/WhyLog-App/WhyLog/pulls/7/reviews" + github.calls[-1][0], "/repos/WhyLog-App/WhyLog/pulls/7/comments" ) - self.assertEqual( - github.calls[-1][2]["body"], "WhyLog AI 자동 줄 단위 리뷰입니다." + self.assertTrue( + github.calls[-1][2]["body"].startswith("