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
64 changes: 64 additions & 0 deletions .github/code_review/prompts/findings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
你现在的任务不是重新做代码审查,而是将已经生成的 `review.md` 提取为结构化结果,供后续脚本用于行内评论、阻断判断和其他自动化处理。

输入文件:
1. 仓库根目录下的 `review.md`:这是主依据,包含第一阶段已经确认的问题。
2. 仓库根目录下的 `pr.diff`:仅用于辅助定位文件路径和行号,必要时可用于补全位置;不要基于 `pr.diff` 新增问题。

任务要求:
1. 只提取 `review.md` 中已经明确列出的问题,不要新增、扩展或重写任何新问题。
2. 不要重新判断代码是否有问题;第一阶段 review 的结论已经成立,你的任务只是标准化提取。
3. 如果 `review.md` 中某个问题缺少明确的文件路径或行号,不要编造;但不要跳过该问题,仍应保留该 finding,并将 `inline_candidate` 设为 `false`。在这种情况下,`path`、`start_line`、`end_line` 可留空字符串、0 或与定位失败一致的保守值,但不能让该问题从结果中消失。
4. 允许参考 `pr.diff` 来确认问题对应的目标文件路径、起始行和结束行,但不要因此新增新的发现。
5. 输出必须是严格合法的 JSON,且只能输出 JSON,不要输出 Markdown、解释文字、代码块、前后缀或额外说明。
6. 输出必须直接以 `{` 开始、以 `}` 结束;不要使用 ```json、``` 或任何其他 Markdown 代码块围栏包裹 JSON。

严重级别映射:
- `Critical` -> `critical`
- `Warning` -> `warning`
- `Suggestion` -> `suggestion`

输出 JSON 格式:
```json
{
"findings": [
{
"severity": "critical",
"path": "relative/path/to/file.py",
"start_line": 12,
"end_line": 12,
"title": "问题标题",
"body": "1-3 句简洁说明,适合直接用于 GitHub 行内评论。",
"inline_candidate": true
}
]
}
```

字段要求:
1. `severity`:必须是 `critical`、`warning`、`suggestion` 之一。
2. `path`:仓库相对路径,例如 `tests/ai_test.py`;如果确实无法稳定定位,可为空字符串,但该 finding 仍需保留。
3. `start_line`:问题起始行号,必须优先使用变更后文件的目标行号;如果确实无法稳定定位,可为 `0`,但该 finding 仍需保留。
4. `end_line`:
- 单行问题时,填写与 `start_line` 相同的值;
- 范围问题时,填写结束行号。
5. `title`:简短问题标题,适合作为行内评论标题,不要过长。
6. `body`:1-3 句简洁说明,说明风险和建议修复方向,适合直接显示在代码旁边。
7. `inline_candidate`:
- `true`:适合做行内评论,问题能明确归因到一个文件的一行或一小段范围,且适合直接挂在代码旁解释。
- `false`:更适合放在总评论中,例如跨文件、跨逻辑链路、整体性结论、位置不稳定或无法明确锚定到 diff 行的问题。

提取规则:
1. 如果 `review.md` 中同一个问题覆盖多个独立位置,只有在这些位置都适合单独挂行内评论时,才拆成多条 findings;否则保持为一条并将 `inline_candidate` 设为 `false`。
2. 如果一个问题已经在第一阶段被合并表述为一个综合问题(例如“命令注入 + 路径穿越”),不要在这里再次拆分成多个新问题,除非 `review.md` 本身已经明确拆开。
3. `body` 不要照抄整段 review 原文;应提炼成适合行内评论展示的短说明。
4. 不允许因为定位失败、信息缺失或不适合行内评论而省略第一阶段已经明确列出的问题;这类问题必须保留在 `findings` 中,并通过 `inline_candidate=false` 与保守字段值表达。
5. 如果 `review.md` 中没有任何问题,输出:
```json
{"findings":[]}
```

校验要求:
1. 输出必须能被标准 JSON 解析器直接解析。
2. 所有字符串必须使用双引号。
3. 不要在 JSON 外再输出任何额外字符。
4. 不要输出 ```json 或 ```,也不要输出任何注释。
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,4 @@

## 测试建议

如果需要补充测试,用 1-2 条说明建议覆盖的场景;如果不需要,请说明“暂无额外测试建议”。
如果需要补充测试,用 1-2 条说明建议覆盖的场景;如果不需要,请说明“暂无额外测试建议”。
24 changes: 24 additions & 0 deletions .github/code_review/scripts/evaluate_gate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env python3

import json
import os
import sys


def main():
findings_path = os.environ.get("FINDINGS_PATH", "findings.json")
with open(findings_path, "r", encoding="utf-8") as f:
data = json.load(f)

findings = data.get("findings", [])
for finding in findings:
if finding.get("severity") == "critical":
print("Critical issues found in AI Code Review, blocking pipeline")
return 1

print("No critical findings, pipeline can continue")
return 0


if __name__ == "__main__":
sys.exit(main())
170 changes: 170 additions & 0 deletions .github/code_review/scripts/post_inline_comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
#!/usr/bin/env python3

import json
import os
import re
import sys
import urllib.error
import urllib.request


def read_findings(path):
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
return data.get("findings", [])


def build_position_map(diff_path):
with open(diff_path, "r", encoding="utf-8") as f:
lines = f.readlines()

position_map = {}
current_path = None
new_line_no = None
position = 0
in_hunk = False

for raw in lines:
line = raw.rstrip("\n")

if line.startswith("diff --git "):
current_path = None
new_line_no = None
position = 0
in_hunk = False
continue

if line.startswith("+++ b/"):
current_path = line[6:]
if current_path not in position_map:
position_map[current_path] = {}
continue

if line.startswith("@@ "):
match = re.match(r"@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@", line)
if match:
new_line_no = int(match.group(1))
in_hunk = True
continue

if not in_hunk or current_path is None:
continue

if not line:
continue

prefix = line[0]
if prefix not in (" ", "+", "-"):
continue

position += 1

if prefix == " ":
position_map[current_path][new_line_no] = position
new_line_no += 1
elif prefix == "+":
position_map[current_path][new_line_no] = position
new_line_no += 1
elif prefix == "-":
continue

return position_map


def build_payload(commit_id, path, position, title, body):
comment_body = "**%s**" % title
if body:
comment_body += "\n\n%s" % body
return {
"body": comment_body,
"commit_id": commit_id,
"path": path,
"position": position,
}


def post_inline_comment(api_url, token, payload):
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
api_url,
data=data,
method="POST",
headers={
"Authorization": "Bearer %s" % token,
"Accept": "application/vnd.github+json",
"Content-Type": "application/json",
},
)
try:
with urllib.request.urlopen(req) as resp:
return True, "status=%s" % resp.status
except urllib.error.HTTPError as e:
try:
err = e.read().decode("utf-8", errors="ignore")
except Exception:
err = ""
return False, "status=%s body=%s" % (e.code, err)


def main():
findings_path = os.environ.get("FINDINGS_PATH", "findings.json")
diff_path = os.environ.get("PR_DIFF_PATH", "pr.diff")
api_url = os.environ["INLINE_COMMENT_API_URL"]
token = os.environ["GITHUB_TOKEN"]
commit_id = os.environ["GITHUB_HEAD_SHA"]

findings = read_findings(findings_path)
if not findings:
print("no findings found, skip inline comments")
return 0

position_map = build_position_map(diff_path)
created = 0

for finding in findings:
if finding.get("severity") != "critical":
continue
if not finding.get("inline_candidate"):
continue

path = finding.get("path")
start_line = finding.get("start_line")
end_line = finding.get("end_line")
title = finding.get("title", "").strip()
body = finding.get("body", "").strip()

if not path or not start_line or not title:
print("skip finding without required fields: %s" % json.dumps(finding, ensure_ascii=False))
continue

start_line = int(start_line)
end_line = int(end_line) if end_line else None
target_line = start_line

file_positions = position_map.get(path, {})
position = file_positions.get(target_line)
if position is None and end_line and end_line > start_line:
position = file_positions.get(end_line)
if position is not None:
target_line = end_line

target = "%s:%s-%s" % (path, start_line, end_line) if end_line and end_line > start_line else "%s:%s" % (path, start_line)

if position is None:
print("skip finding without diff position: %s" % target)
continue

payload = build_payload(commit_id, path, position, title, body)
ok, msg = post_inline_comment(api_url, token, payload)
if ok:
created += 1
print("inline comment created: %s position=%s %s" % (target, position, msg))
else:
print("inline comment failed: %s position=%s %s" % (target, position, msg))

print("inline comments created: %s" % created)
return 0


if __name__ == "__main__":
sys.exit(main())
75 changes: 75 additions & 0 deletions .github/code_review/scripts/post_review_comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env python3

import json
import os
import re
import sys
import urllib.error
import urllib.request


def load_review(path):
with open(path, "r", encoding="utf-8") as f:
return f.read().strip()


def add_github_links(body, repo_url, head_sha):
pattern = re.compile(r"`((?:\.?[\w.-]+/)*[\w.-]+\.[\w.+-]+):(\d+)(?:-(\d+))?`")

def repl(match):
file_path = match.group(1)
start = match.group(2)
end = match.group(3)
label = "%s:%s%s" % (file_path, start, ("-%s" % end) if end else "")
anchor = "#L%s%s" % (start, ("-L%s" % end) if end else "")
url = "%s/blob/%s/%s%s" % (repo_url, head_sha, file_path, anchor)
return "[`%s`](%s)" % (label, url)

return pattern.sub(repl, body)


def post_comment(api_url, token, body):
payload = json.dumps({"body": body}).encode("utf-8")
req = urllib.request.Request(
api_url,
data=payload,
method="POST",
headers={
"Authorization": "Bearer %s" % token,
"Accept": "application/vnd.github+json",
"Content-Type": "application/json",
},
)
try:
with urllib.request.urlopen(req) as resp:
return resp.status, resp.read().decode("utf-8", errors="ignore")
except urllib.error.HTTPError as e:
return e.code, e.read().decode("utf-8", errors="ignore")


def main():
review_path = os.environ.get("REVIEW_PATH", "review.md")
api_url = os.environ["COMMENT_API_URL"]
repo_url = os.environ["GITHUB_WEB_REPO_URL"].rstrip("/")
head_sha = os.environ["GITHUB_HEAD_SHA"]
token = os.environ["GITHUB_TOKEN"]

body = load_review(review_path)
if not body:
body = "AI Code Review 未生成有效结果,请检查流水线日志。"

body = add_github_links(body, repo_url, head_sha)
status, response = post_comment(api_url, token, "## AI Code Review\n\n%s" % body)

print("comment http code: %s" % status)
if status < 200 or status >= 300:
print("failed to create github comment")
print(response)
return 1

print("comment created successfully")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading