Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .codex/skills/gframework-pr-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Shortcut: `$gframework-pr-review`
- locate the PR for the current branch through the GitHub PR API
- fetch PR metadata, issue comments, reviews, and review comments through the GitHub API
- extract `Summary by CodeRabbit`、GitHub Actions bot comments such as `MegaLinter analysis: Success with warnings`、and CTRF test reports from issue comments
- parse the latest CodeRabbit review body itself, including folded sections such as `🧹 Nitpick comments (N)` and the overall AI-agent prompt
- fetch the latest head commit review threads from the GitHub PR API
- prefer unresolved review threads on the latest head commit over older summary-only signals
- extract failed checks, MegaLinter detailed issues, and test-report signals such as `Failed Tests` or `No failed tests in this run`
Expand All @@ -39,6 +40,7 @@ The script should produce:

- PR metadata: number, title, state, branch, URL
- CodeRabbit summary block from issue comments when available
- Folded latest-review sections such as `Nitpick comments (N)` when CodeRabbit puts them in the review body instead of issue comments
- Parsed latest head-review threads, with unresolved threads clearly separated
- Latest head commit review metadata and review threads
- Unresolved latest-commit review threads after reply-thread folding
Expand All @@ -54,6 +56,7 @@ The script should produce:
- Prefer GitHub API results over PR HTML. The PR HTML page is now a fallback/debugging source, not the primary source of truth.
- If the summary block and the latest head review threads disagree, trust the latest unresolved head-review threads and treat older summary findings as stale until re-verified locally.
- Treat GitHub Actions comments with `Success with warnings` as actionable review input when they include concrete linter diagnostics such as `MegaLinter` detailed issues; do not skip them just because the parent check is green.
- Do not assume all CodeRabbit findings live in issue comments. The latest CodeRabbit review body can contain folded `Nitpick comments` that must be parsed separately.

## Example Triggers

Expand Down
147 changes: 136 additions & 11 deletions .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,19 +210,39 @@ def parse_actionable_comments(actionable_block: str) -> dict[str, Any]:
comment_count_match = re.search(r"Actionable comments posted:\s*(\d+)", actionable_block)
count = int(comment_count_match.group(1)) if comment_count_match else 0

comments: list[dict[str, str]] = []
primary_block = actionable_block.split(
"<details>\n<summary>🤖 Prompt for all review comments with AI agents</summary>",
1,
)[0]
comments = parse_comment_cards(primary_block)

prompt_match = re.search(
r"<summary>🤖 Prompt for all review comments with AI agents</summary>\s*```(.*?)```",
actionable_block,
re.S,
)

return {
"count": count or len(comments),
"comments": comments,
"all_comments_prompt": prompt_match.group(1).strip() if prompt_match else "",
"raw": actionable_block.strip(),
}


def parse_comment_cards(comment_block: str) -> list[dict[str, str]]:
comments: list[dict[str, str]] = []
pattern = re.compile(
r"<summary>"
r"((?:[^<\n]+/)*[^<\n]+\.(?:cs|md|csproj|yaml|yml|json|txt|props|targets)|AGENTS\.md|CLAUDE\.md|README\.md|\.gitignore)"
# CodeRabbit can fold cards for source, docs, scripts, and repo config files.
# Keep the matcher path-like, but do not hardcode a tiny extension allow-list
# or we will silently drop valid findings such as .py skill files.
r"((?:[^<\n]+/)*[^<\n/]+(?:\.[A-Za-z0-9._-]+)+|AGENTS\.md|CLAUDE\.md|README\.md|\.gitignore)"
r" \((\d+)\)</summary><blockquote>\s*(.*?)\s*(?:(?:</blockquote></details>)|(?:</blockquote>))",
re.S,
)

for path, _, body in pattern.findall(primary_block):
for path, _, body in pattern.findall(comment_block):
finding_match = re.search(r"`([^`]+)`: \*\*(.*?)\*\*", body, re.S)
prompt_match = re.search(r"<summary>🤖 Prompt for AI Agents</summary>\s*```(.*?)```", body, re.S)
suggestion_match = re.search(r"<summary>✏️ 建议文案调整</summary>\s*```diff(.*?)```", body, re.S)
Expand All @@ -243,17 +263,67 @@ def parse_actionable_comments(actionable_block: str) -> dict[str, Any]:
}
)

return comments


def normalize_review_body_for_parsing(review_body: str) -> str:
# CodeRabbit sometimes wraps structured HTML sections in markdown blockquotes,
# such as the CAUTION block used for outside-diff comments. Remove the quote
# prefixes for parsing while leaving the original raw body unchanged for output.
return re.sub(r"(?m)^>\s?", "", review_body)


def find_section_block_end(review_body: str, block_start: int) -> int:
depth = 1
for tag_match in re.finditer(r"<details>|</details>", review_body[block_start:]):
tag = tag_match.group(0)
if tag == "<details>":
depth += 1
else:
depth -= 1
if depth == 0:
return block_start + tag_match.start()

return len(review_body)


def parse_review_comment_group(review_body: str, section_name: str) -> dict[str, Any]:
section_match = re.search(
rf"<summary>[^<]*{re.escape(section_name)} \((?P<count>\d+)\)</summary><blockquote>\s*",
review_body,
re.S,
)
if section_match is None:
return {"count": 0, "comments": [], "raw": ""}

block_end = find_section_block_end(review_body, section_match.end())
comment_block = review_body[section_match.end() : block_end].strip()
comment_block = re.sub(r"\s*</blockquote>\s*$", "", comment_block, flags=re.S)
return {
"count": int(section_match.group("count")),
"comments": parse_comment_cards(comment_block),
"raw": comment_block,
}


def parse_latest_review_body(review_body: str) -> dict[str, Any]:
normalized_review_body = normalize_review_body_for_parsing(review_body)
actionable_count_match = re.search(r"\*\*Actionable comments posted:\s*(\d+)\*\*", normalized_review_body)
prompt_match = re.search(
r"<summary>🤖 Prompt for all review comments with AI agents</summary>\s*```(.*?)```",
actionable_block,
normalized_review_body,
re.S,
)

outside_diff_group = parse_review_comment_group(normalized_review_body, "Outside diff range comments")
nitpick_group = parse_review_comment_group(normalized_review_body, "Nitpick comments")
return {
"count": count,
"comments": comments,
"actionable_count": int(actionable_count_match.group(1)) if actionable_count_match else 0,
"outside_diff_count": outside_diff_group["count"],
"outside_diff_comments": outside_diff_group["comments"],
"nitpick_count": nitpick_group["count"],
"nitpick_comments": nitpick_group["comments"],
"all_comments_prompt": prompt_match.group(1).strip() if prompt_match else "",
"raw": actionable_block.strip(),
"raw": review_body.strip(),
}


Expand Down Expand Up @@ -548,12 +618,39 @@ def build_result(pr_number: int, branch: str) -> dict[str, Any]:
warnings.append("MegaLinter report block was not found in issue comments.")

latest_commit_review: dict[str, Any] = {}
coderabbit_review: dict[str, Any] = {}
try:
latest_commit_review = fetch_latest_commit_review(pr_number)
latest_review = latest_commit_review.get("latest_review", {})
latest_review_body = str(latest_review.get("body") or "")
if latest_review.get("user") == CODERABBIT_LOGIN and latest_review_body:
coderabbit_review = parse_latest_review_body(latest_review_body)
outside_diff_count = int(coderabbit_review.get("outside_diff_count") or 0)
parsed_outside_diff_count = len(coderabbit_review.get("outside_diff_comments", []))
nitpick_count = int(coderabbit_review.get("nitpick_count") or 0)
parsed_nitpick_count = len(coderabbit_review.get("nitpick_comments", []))
if "Outside diff range comments" in latest_review_body and not parsed_outside_diff_count:
warnings.append("CodeRabbit outside-diff comments block could not be parsed from the latest review body.")
elif outside_diff_count and parsed_outside_diff_count != outside_diff_count:
warnings.append(
"CodeRabbit outside-diff comments were only partially parsed from the latest review body: "
f"declared={outside_diff_count}, parsed={parsed_outside_diff_count}."
)
if "Nitpick comments" in latest_review_body and not parsed_nitpick_count:
warnings.append("CodeRabbit nitpick comments block could not be parsed from the latest review body.")
elif nitpick_count and parsed_nitpick_count != nitpick_count:
warnings.append(
"CodeRabbit nitpick comments were only partially parsed from the latest review body: "
f"declared={nitpick_count}, parsed={parsed_nitpick_count}."
)
except Exception as error: # noqa: BLE001
warnings.append(f"Latest commit review comments could not be fetched: {error}")

if not actionable_block and not latest_commit_review.get("threads"):
if (
not actionable_block
and not latest_commit_review.get("threads")
and not coderabbit_review.get("nitpick_comments")
):
warnings.append("CodeRabbit actionable comments block was not found in issue comments.")

return {
Expand All @@ -571,6 +668,7 @@ def build_result(pr_number: int, branch: str) -> dict[str, Any]:
"raw": summary_block,
},
"coderabbit_comments": parse_actionable_comments(actionable_block) if actionable_block else {},
"coderabbit_review": coderabbit_review,
"latest_commit_review": latest_commit_review,
"megalinter_report": parse_megalinter_comment(megalinter_block) if megalinter_block else {},
"test_reports": [parse_test_report(block) for block in test_blocks],
Expand All @@ -594,15 +692,42 @@ def format_text(result: dict[str, Any]) -> str:
lines.append(f" Explanation: {check['explanation']}")
lines.append(f" Resolution: {check['resolution']}")

comments = result.get("coderabbit_comments", {}).get("comments", [])
coderabbit_comments = result.get("coderabbit_comments", {})
review_feedback = result.get("coderabbit_review", {})
comments = coderabbit_comments.get("comments", [])
actionable_count = review_feedback.get("actionable_count") or coderabbit_comments.get("count") or len(comments)
lines.append("")
lines.append(f"CodeRabbit actionable comments: {len(comments)}")
lines.append(f"CodeRabbit actionable comments: {actionable_count}")
for comment in comments:
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
if comment["title"]:
lines.append(f" Title: {comment['title']}")
if comment["description"]:
lines.append(f" Description: {comment['description']}")
if actionable_count and not comments:
lines.append(" Details: see latest-commit review threads below.")

outside_diff_comments = review_feedback.get("outside_diff_comments", [])
outside_diff_count = review_feedback.get("outside_diff_count") or len(outside_diff_comments)
lines.append("")
lines.append(f"CodeRabbit outside-diff comments: {outside_diff_count} declared, {len(outside_diff_comments)} parsed")
for comment in outside_diff_comments:
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
if comment["title"]:
lines.append(f" Title: {comment['title']}")
if comment["description"]:
lines.append(f" Description: {comment['description']}")

nitpick_comments = review_feedback.get("nitpick_comments", [])
nitpick_count = review_feedback.get("nitpick_count") or len(nitpick_comments)
lines.append("")
lines.append(f"CodeRabbit nitpick comments: {nitpick_count} declared, {len(nitpick_comments)} parsed")
for comment in nitpick_comments:
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
if comment["title"]:
lines.append(f" Title: {comment['title']}")
if comment["description"]:
lines.append(f" Description: {comment['description']}")

latest_commit_review = result.get("latest_commit_review", {})
latest_commit = latest_commit_review.get("latest_commit", {})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@
GF_ConfigSchema_010 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_011 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_012 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_013 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
Loading
Loading