From b919d0966964ce27e5974a3a43ea93ed020db46d Mon Sep 17 00:00:00 2001 From: SangwanYu Date: Sun, 9 Aug 2026 23:14:04 +0900 Subject: [PATCH 1/4] =?UTF-8?q?ci(root):=20AI=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EC=BB=A4=EB=B0=8B=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=ED=95=9C=20=EC=A4=84=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/scripts/review_publishing.py | 10 ++-------- .github/scripts/test_review_publishing.py | 15 ++++++--------- .github/workflows/ci.yml | 2 +- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/.github/scripts/review_publishing.py b/.github/scripts/review_publishing.py index 65ffe4a..70a56a4 100644 --- a/.github/scripts/review_publishing.py +++ b/.github/scripts/review_publishing.py @@ -986,14 +986,8 @@ 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" + f"docs(docs): PR-{pr_number} 리뷰 판단 근거 기록 " + "[Generated-By: whylog-ai-review]" ) diff --git a/.github/scripts/test_review_publishing.py b/.github/scripts/test_review_publishing.py index 7e837fa..8dfbf2d 100644 --- a/.github/scripts/test_review_publishing.py +++ b/.github/scripts/test_review_publishing.py @@ -557,14 +557,10 @@ def test_sync_creates_commit_from_reviewed_head(self): commit_call = github.calls[-2] self.assertEqual(commit_call[0], "/repos/repo/git/commits") self.assertEqual(commit_call[2]["parents"], ["headsha"]) - self.assertIn("Generated-By: whylog-ai-review", commit_call[2]["message"]) - self.assertTrue( - commit_call[2]["message"].startswith( - "docs(docs): PR-7 리뷰 판단 근거를 저장소에 남김" - ) - ) - self.assertIn( - "Rejected: Direct push to protected base", commit_call[2]["message"] + self.assertEqual( + commit_call[2]["message"], + "docs(docs): PR-7 리뷰 판단 근거 기록 " + "[Generated-By: whylog-ai-review]", ) def test_sync_stale_head_is_artifact_only(self): @@ -602,7 +598,8 @@ def test_detects_generated_doc_only_head_commit(self): responses=[ { "commit": { - "message": "docs(docs): PR-7 리뷰 판단 근거를 저장소에 남김\n\nGenerated-By: whylog-ai-review\n" + "message": "docs(docs): PR-7 리뷰 판단 근거 기록 " + "[Generated-By: whylog-ai-review]" }, "files": [{"filename": "docs/pr-reviews/PR-7.md"}], "parents": [{"sha": "parent-sha"}], diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14d40a2..da49bbc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -218,7 +218,7 @@ jobs: env: GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} - AI_REVIEW_PUSH_TOKEN: ${{ secrets.AI_REVIEW_PUSH_TOKEN }} + AI_REVIEW_PUSH_TOKEN: "" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPOSITORY_IS_PRIVATE: ${{ github.event.repository.private }} PYTHONUNBUFFERED: "1" From eef8e25cbf2dedb95aa6cc59aa59284208500a53 Mon Sep 17 00:00:00 2001 From: SangwanYu Date: Sun, 9 Aug 2026 23:17:04 +0900 Subject: [PATCH 2/4] =?UTF-8?q?ci(root):=20AI=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EC=BB=A4=EB=B0=8B=20=EC=8B=9D=EB=B3=84=20?= =?UTF-8?q?=EA=B7=9C=EC=B9=99=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/scripts/review_publishing.py | 10 ++++---- .github/scripts/test_review_publishing.py | 29 +++++++++++++++++++---- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/.github/scripts/review_publishing.py b/.github/scripts/review_publishing.py index 70a56a4..c2af224 100644 --- a/.github/scripts/review_publishing.py +++ b/.github/scripts/review_publishing.py @@ -985,10 +985,7 @@ def sync_pr_review_document( def _doc_commit_message(pr_number: int) -> str: - return ( - f"docs(docs): PR-{pr_number} 리뷰 판단 근거 기록 " - "[Generated-By: whylog-ai-review]" - ) + return f"docs(docs): PR-{pr_number} 리뷰 판단 근거 기록" def generated_doc_only_parent_sha( @@ -1016,8 +1013,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 @@ -1032,6 +1027,9 @@ def generated_doc_only_parent_sha( or not isinstance(parents[0], Mapping) ): return None + document_pr_number = int(Path(paths[0]).stem.removeprefix("PR-")) + if message != _doc_commit_message(document_pr_number): + return None return _string(parents[0].get("sha")) or None diff --git a/.github/scripts/test_review_publishing.py b/.github/scripts/test_review_publishing.py index 8dfbf2d..593f67b 100644 --- a/.github/scripts/test_review_publishing.py +++ b/.github/scripts/test_review_publishing.py @@ -559,8 +559,7 @@ def test_sync_creates_commit_from_reviewed_head(self): self.assertEqual(commit_call[2]["parents"], ["headsha"]) self.assertEqual( commit_call[2]["message"], - "docs(docs): PR-7 리뷰 판단 근거 기록 " - "[Generated-By: whylog-ai-review]", + "docs(docs): PR-7 리뷰 판단 근거 기록", ) def test_sync_stale_head_is_artifact_only(self): @@ -598,8 +597,7 @@ def test_detects_generated_doc_only_head_commit(self): responses=[ { "commit": { - "message": "docs(docs): PR-7 리뷰 판단 근거 기록 " - "[Generated-By: whylog-ai-review]" + "message": "docs(docs): PR-7 리뷰 판단 근거 기록" }, "files": [{"filename": "docs/pr-reviews/PR-7.md"}], "parents": [{"sha": "parent-sha"}], @@ -614,7 +612,7 @@ def test_detects_generated_doc_only_head_commit(self): "parent-sha", ) - def test_rejects_non_review_doc_or_missing_trailer(self): + def test_rejects_non_review_doc_or_wrong_message(self): github = FakeGitHub( responses=[ { @@ -630,6 +628,27 @@ def test_rejects_non_review_doc_or_missing_trailer(self): "api", "token", "repo", {"head": {"sha": "abc"}}, github ) ) + + def test_rejects_mismatched_pr_number_in_generated_message(self): + github = FakeGitHub( + responses=[ + { + "commit": { + "message": "docs(docs): PR-8 리뷰 판단 근거 기록" + }, + "files": [{"filename": "docs/pr-reviews/PR-7.md"}], + "parents": [{"sha": "parent-sha"}], + } + ] + ) + + self.assertIsNone( + rp.generated_doc_only_parent_sha( + "api", "token", "repo", {"head": {"sha": "abc"}}, github + ) + ) + + def test_recognizes_pr_review_document_path(self): self.assertTrue(rp.is_pr_review_doc_path("docs/pr-reviews/PR-1.md")) self.assertFalse(rp.is_pr_review_doc_path("docs/pr-reviews/README.md")) From 484f8ec912bf76a0fefc5c222c7ccdc0435b0752 Mon Sep 17 00:00:00 2001 From: SangwanYu Date: Sun, 9 Aug 2026 23:26:06 +0900 Subject: [PATCH 3/4] =?UTF-8?q?ci(root):=20=EC=9D=B8=EB=9D=BC=EC=9D=B8=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=A4=91=EB=B3=B5=20=EC=9A=94=EC=95=BD=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/scripts/review_publishing.py | 82 +++++++++++------------ .github/scripts/test_ai_review.py | 4 +- .github/scripts/test_review_publishing.py | 52 +++++++++++--- 3 files changed, 83 insertions(+), 55 deletions(-) diff --git a/.github/scripts/review_publishing.py b/.github/scripts/review_publishing.py index c2af224..dc44979 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 @@ -429,53 +429,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, ) 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 593f67b..978af13 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("