mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-10 07:54:14 +08:00
ci(pr-agent): use shared review runner (#6127)
This commit is contained in:
506
.github/workflows/pr-agent.yml
vendored
506
.github/workflows/pr-agent.yml
vendored
@@ -2,8 +2,7 @@ name: PR-Agent
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
# PR-Agent 通过 base repo 上下文读取 PR diff 并发布 Review,不 checkout 或执行 PR 分支代码。
|
||||
# pull_request_target 允许 fork PR 使用仓库 secrets,因此 workflow 只运行固定 digest 的 PR-Agent 容器。
|
||||
# Fork 审查需要目标仓库凭据;该 job 仅通过 GitHub API 读取 PR 内容,不 checkout 或执行 PR 分支代码。
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
@@ -11,17 +10,13 @@ on:
|
||||
- review_requested
|
||||
- synchronize
|
||||
issue_comment:
|
||||
# 手动命令只在 PR 评论中有意义;编辑后的命令也可重新触发。
|
||||
types:
|
||||
- created
|
||||
- edited
|
||||
|
||||
permissions:
|
||||
# 读取仓库内容和 PR diff。
|
||||
contents: read
|
||||
# 更新 PR 描述、发布 PR Review 或修改 PR 相关元数据。
|
||||
pull-requests: write
|
||||
# PR 评论在 GitHub API 中属于 issue comments,手动问答需要该权限。
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
@@ -29,21 +24,11 @@ jobs:
|
||||
if: >-
|
||||
github.event.sender.type != 'Bot' &&
|
||||
(
|
||||
(
|
||||
github.event_name == 'pull_request_target'
|
||||
) ||
|
||||
github.event_name == 'pull_request_target' ||
|
||||
(
|
||||
github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request != null &&
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR", "CONTRIBUTOR", "FIRST_TIME_CONTRIBUTOR"]'), github.event.comment.author_association) &&
|
||||
(
|
||||
github.event.comment.body == '/describe' ||
|
||||
startsWith(github.event.comment.body, '/describe ') ||
|
||||
github.event.comment.body == '/review' ||
|
||||
startsWith(github.event.comment.body, '/review ') ||
|
||||
github.event.comment.body == '/ask' ||
|
||||
startsWith(github.event.comment.body, '/ask ')
|
||||
)
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR", "CONTRIBUTOR", "FIRST_TIME_CONTRIBUTOR"]'), github.event.comment.author_association)
|
||||
)
|
||||
)
|
||||
concurrency:
|
||||
@@ -53,484 +38,15 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Capture PR context
|
||||
id: pr_context
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pull="$(mktemp)"
|
||||
gh api "repos/${REPO}/pulls/${PR_NUMBER}" > "${pull}"
|
||||
python3 - "${pull}" >> "${GITHUB_OUTPUT}" <<'PY'
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
pull = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
||||
title = pull.get("title") or ""
|
||||
body = pull.get("body") or ""
|
||||
labels = {item.get("name", "") for item in pull.get("labels") or []}
|
||||
skip_pr_agent = "true" if "skip pr-agent" in labels or re.search(r"^(?:\[Auto\]|Auto)", title) else "false"
|
||||
body = re.sub(
|
||||
r"<!-- pr-agent-summary:start -->.*?<!-- pr-agent-summary:end -->",
|
||||
" ",
|
||||
body,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
text = f"{title}\n{body}"
|
||||
cjk_count = len(re.findall(r"[\u4e00-\u9fff]", text))
|
||||
latin_words = len(re.findall(r"\b[A-Za-z][A-Za-z]{2,}\b", text))
|
||||
if cjk_count >= 4 or latin_words < 8:
|
||||
response_language = "zh-CN"
|
||||
summary_heading = "PR-Agent 摘要"
|
||||
else:
|
||||
response_language = "en-US"
|
||||
summary_heading = "PR-Agent Summary"
|
||||
print(f"head_sha={pull['head']['sha']}")
|
||||
print(f"changed_files={pull.get('changed_files') or 0}")
|
||||
print(f"response_language={response_language}")
|
||||
print(f"summary_heading={summary_heading}")
|
||||
print(f"skip_pr_agent={skip_pr_agent}")
|
||||
PY
|
||||
|
||||
- name: Prepare PR-Agent description markers
|
||||
if: >-
|
||||
steps.pr_context.outputs.skip_pr_agent != 'true' &&
|
||||
(
|
||||
github.event_name == 'pull_request_target' ||
|
||||
github.event.comment.body == '/describe' ||
|
||||
startsWith(github.event.comment.body, '/describe ')
|
||||
)
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
|
||||
SUMMARY_HEADING: ${{ steps.pr_context.outputs.summary_heading }}
|
||||
CHANGED_FILES: ${{ steps.pr_context.outputs.changed_files }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
payload="$(mktemp)"
|
||||
python3 - "${payload}" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
pull = json.loads(subprocess.check_output(
|
||||
["gh", "api", f"repos/{os.environ['REPO']}/pulls/{os.environ['PR_NUMBER']}"],
|
||||
text=True,
|
||||
))
|
||||
body = pull.get("body") or ""
|
||||
start = "<!-- pr-agent-summary:start -->"
|
||||
end = "<!-- pr-agent-summary:end -->"
|
||||
heading = os.environ.get("SUMMARY_HEADING") or "PR-Agent 摘要"
|
||||
changed_files = int(os.environ.get("CHANGED_FILES") or 0)
|
||||
block = f"## {heading}\n\n{start}\npr_agent:summary\n{end}"
|
||||
start_index = body.find(start)
|
||||
end_index = body.find(end, start_index + len(start)) if start_index >= 0 else -1
|
||||
owned_block = re.compile(
|
||||
r"(?ims)^##\s+(?:PR-Agent\s+摘要|PR-Agent\s+Summary)\s*\n\s*"
|
||||
r"<!-- pr-agent-summary:start -->.*?<!-- pr-agent-summary:end -->\s*"
|
||||
)
|
||||
if changed_files == 0:
|
||||
updated = owned_block.sub("", body)
|
||||
if updated != body:
|
||||
updated = updated.rstrip()
|
||||
elif start_index >= 0 and end_index >= 0:
|
||||
body = re.sub(
|
||||
r"(?im)^##\s+(PR-Agent\s+摘要|PR-Agent\s+Summary)\s*\n\s*(?=<!-- pr-agent-summary:start -->)",
|
||||
f"## {heading}\n\n",
|
||||
body,
|
||||
)
|
||||
start_index = body.find(start)
|
||||
end_index = body.find(end, start_index + len(start))
|
||||
updated = body[:start_index] + f"{start}\npr_agent:summary\n{end}" + body[end_index + len(end):]
|
||||
elif start_index >= 0 or end in body:
|
||||
updated = body
|
||||
elif body.strip():
|
||||
updated = f"{body.rstrip()}\n\n{block}\n"
|
||||
else:
|
||||
updated = f"{block}\n"
|
||||
if updated != body:
|
||||
with open(sys.argv[1], "w", encoding="utf-8") as handle:
|
||||
json.dump({"body": updated}, handle, ensure_ascii=False)
|
||||
PY
|
||||
if [ -s "${payload}" ]; then
|
||||
gh api --method PATCH "repos/${REPO}/pulls/${PR_NUMBER}" --input "${payload}" >/dev/null
|
||||
fi
|
||||
|
||||
- name: Update PR description
|
||||
if: >-
|
||||
steps.pr_context.outputs.skip_pr_agent != 'true' &&
|
||||
steps.pr_context.outputs.changed_files != '0' &&
|
||||
(
|
||||
github.event_name == 'pull_request_target' ||
|
||||
github.event.comment.body == '/describe' ||
|
||||
startsWith(github.event.comment.body, '/describe ')
|
||||
)
|
||||
uses: docker://pragent/pr-agent:0.39.0-github_action@sha256:b253845caa8c7ff5ce8be78f32996647982bdd4890826a962b78eff2e385a825
|
||||
- name: Run PR Review
|
||||
uses: docker://ghcr.io/infinitypacer/pr-review-runner:latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
OPENAI.API_BASE: ${{ secrets.OPENAI_API_BASE }}
|
||||
config.model: 'gpt-5.6-terra'
|
||||
config.fallback_models: '["gpt-5.5", "gpt-5.4"]'
|
||||
config.custom_model_max_tokens: '1050000'
|
||||
config.reasoning_effort: 'medium'
|
||||
config.ai_timeout: '900'
|
||||
config.response_language: ${{ steps.pr_context.outputs.response_language }}
|
||||
config.large_patch_policy: 'clip'
|
||||
config.ignore_pr_title: '["^\\[Auto\\]", "^Auto"]'
|
||||
config.ignore_pr_labels: '["skip pr-agent"]'
|
||||
github_action_config.auto_review: 'false'
|
||||
github_action_config.auto_describe: 'true'
|
||||
github_action_config.auto_improve: 'false'
|
||||
github_action_config.pr_actions: '["opened", "reopened", "ready_for_review", "review_requested"]'
|
||||
github_action_config.handle_push_trigger: 'true'
|
||||
github_action_config.push_commands: '["/describe"]'
|
||||
pr_description.generate_ai_title: 'false'
|
||||
pr_description.publish_labels: 'false'
|
||||
pr_description.publish_description_as_comment: 'false'
|
||||
pr_description.publish_description_as_comment_persistent: 'false'
|
||||
pr_description.enable_pr_diagram: 'false'
|
||||
pr_description.enable_pr_type: 'false'
|
||||
pr_description.enable_help_text: 'false'
|
||||
pr_description.enable_help_comment: 'false'
|
||||
pr_description.enable_semantic_files_types: 'false'
|
||||
pr_description.collapsible_file_list: 'adaptive'
|
||||
pr_description.add_original_user_description: 'true'
|
||||
pr_description.use_description_markers: 'true'
|
||||
pr_description.final_update_message: 'false'
|
||||
pr_description.extra_instructions: |
|
||||
Match the configured response language.
|
||||
Summarize the change goal, key implementation details, compatibility impact, tests, and notable risks.
|
||||
Use 2-4 bullets for small pull requests and 4-8 bullets for larger changes.
|
||||
Avoid file lists and local command transcripts.
|
||||
|
||||
- name: Remove unfilled PR-Agent description marker
|
||||
if: >-
|
||||
always() &&
|
||||
steps.pr_context.outputs.skip_pr_agent != 'true' &&
|
||||
(
|
||||
github.event_name == 'pull_request_target' ||
|
||||
github.event.comment.body == '/describe' ||
|
||||
startsWith(github.event.comment.body, '/describe ')
|
||||
)
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
payload="$(mktemp)"
|
||||
python3 - "${payload}" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
pull = json.loads(subprocess.check_output(
|
||||
["gh", "api", f"repos/{os.environ['REPO']}/pulls/{os.environ['PR_NUMBER']}"],
|
||||
text=True,
|
||||
))
|
||||
body = pull.get("body") or ""
|
||||
placeholder = "<!-- pr-agent-summary:start -->\npr_agent:summary\n<!-- pr-agent-summary:end -->"
|
||||
owned_block = re.compile(
|
||||
r"(?ims)^##\s+(?:PR-Agent\s+摘要|PR-Agent\s+Summary)\s*\n\s*"
|
||||
r"<!-- pr-agent-summary:start -->\s*pr_agent:summary\s*<!-- pr-agent-summary:end -->\s*"
|
||||
)
|
||||
if placeholder in body:
|
||||
updated = owned_block.sub("", body).rstrip()
|
||||
if updated != body:
|
||||
with open(sys.argv[1], "w", encoding="utf-8") as handle:
|
||||
json.dump({"body": updated}, handle, ensure_ascii=False)
|
||||
PY
|
||||
if [ -s "${payload}" ]; then
|
||||
gh api --method PATCH "repos/${REPO}/pulls/${PR_NUMBER}" --input "${payload}" >/dev/null
|
||||
fi
|
||||
|
||||
- name: Analyze PR review
|
||||
id: review_analysis
|
||||
if: >-
|
||||
steps.pr_context.outputs.skip_pr_agent != 'true' &&
|
||||
(
|
||||
github.event_name == 'pull_request_target' ||
|
||||
github.event.comment.body == '/review' ||
|
||||
startsWith(github.event.comment.body, '/review ')
|
||||
)
|
||||
uses: docker://pragent/pr-agent:0.39.0-github_action@sha256:b253845caa8c7ff5ce8be78f32996647982bdd4890826a962b78eff2e385a825
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
OPENAI.API_BASE: ${{ secrets.OPENAI_API_BASE }}
|
||||
config.model: ${{ github.event_name == 'issue_comment' && 'gpt-5.6-sol' || 'gpt-5.6-terra' }}
|
||||
config.fallback_models: '["gpt-5.5", "gpt-5.4"]'
|
||||
config.custom_model_max_tokens: '1050000'
|
||||
config.reasoning_effort: 'xhigh'
|
||||
config.ai_timeout: '900'
|
||||
config.response_language: ${{ steps.pr_context.outputs.response_language }}
|
||||
config.large_patch_policy: 'clip'
|
||||
config.ignore_pr_title: '["^\\[Auto\\]", "^Auto"]'
|
||||
config.ignore_pr_labels: '["skip pr-agent"]'
|
||||
config.publish_output: 'false'
|
||||
github_action_config.auto_review: 'true'
|
||||
github_action_config.auto_describe: 'false'
|
||||
github_action_config.auto_improve: 'false'
|
||||
github_action_config.pr_actions: '["opened", "reopened", "ready_for_review", "review_requested"]'
|
||||
github_action_config.handle_push_trigger: 'true'
|
||||
github_action_config.push_commands: '["/review"]'
|
||||
github_action_config.enable_output: 'true'
|
||||
pr_reviewer.num_max_findings: '4'
|
||||
pr_reviewer.require_score_review: 'false'
|
||||
pr_reviewer.require_tests_review: 'false'
|
||||
pr_reviewer.require_security_review: 'false'
|
||||
pr_reviewer.require_estimate_effort_to_review: 'false'
|
||||
pr_reviewer.require_estimate_contribution_time_cost: 'false'
|
||||
pr_reviewer.require_can_be_split_review: 'false'
|
||||
pr_reviewer.require_todo_scan: 'false'
|
||||
pr_reviewer.require_ticket_analysis_review: 'false'
|
||||
pr_reviewer.enable_review_labels_effort: 'false'
|
||||
pr_reviewer.enable_review_labels_security: 'false'
|
||||
pr_reviewer.extra_instructions: |
|
||||
Return key_issues_to_review only for concrete behavior defects introduced by this pull request.
|
||||
Each finding must identify the affected behavior, a reachable trigger, and the existing contract or invariant it violates.
|
||||
Use issue_content to state the smallest correction boundary, not a code patch.
|
||||
Do not report style preferences, comments, refactors, architecture alternatives, speculative races, extra hardening, optional tests, or hypothetical concerns.
|
||||
Return no findings when the evidence is incomplete.
|
||||
|
||||
- name: Publish review comments and summary
|
||||
if: >-
|
||||
steps.pr_context.outputs.skip_pr_agent != 'true' &&
|
||||
steps.review_analysis.outcome == 'success' &&
|
||||
(
|
||||
github.event_name == 'pull_request_target' ||
|
||||
github.event.comment.body == '/review' ||
|
||||
startsWith(github.event.comment.body, '/review ')
|
||||
)
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
|
||||
REVIEWED_HEAD_SHA: ${{ steps.pr_context.outputs.head_sha }}
|
||||
CHANGED_FILES: ${{ steps.pr_context.outputs.changed_files }}
|
||||
RESPONSE_LANGUAGE: ${{ steps.pr_context.outputs.response_language }}
|
||||
REVIEW_JSON: ${{ steps.review_analysis.outputs.review }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
current_head_sha="$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.head.sha')"
|
||||
if [ "${current_head_sha}" != "${REVIEWED_HEAD_SHA}" ]; then
|
||||
echo "PR head changed during analysis; skip stale review publication."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
review_payload="$(mktemp)"
|
||||
python3 - "${review_payload}" <<'PY'
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from urllib.parse import quote
|
||||
|
||||
review_raw = os.environ.get("REVIEW_JSON") or "{}"
|
||||
review = json.loads(review_raw)
|
||||
if not review_raw.strip() or review == {}:
|
||||
if int(os.environ.get("CHANGED_FILES") or 0):
|
||||
raise SystemExit("Review analysis produced no structured output for a non-empty PR.")
|
||||
review = {}
|
||||
|
||||
repo = os.environ["REPO"]
|
||||
number = os.environ["PR_NUMBER"]
|
||||
head_sha = os.environ["REVIEWED_HEAD_SHA"]
|
||||
language = os.environ.get("RESPONSE_LANGUAGE") or "zh-CN"
|
||||
|
||||
def paged(endpoint):
|
||||
result = json.loads(subprocess.check_output(
|
||||
["gh", "api", "--paginate", "--slurp", endpoint], text=True
|
||||
))
|
||||
if result and all(isinstance(page, list) for page in result):
|
||||
return [item for page in result for item in page]
|
||||
return result
|
||||
|
||||
files = paged(f"repos/{repo}/pulls/{number}/files?per_page=100")
|
||||
comments = paged(f"repos/{repo}/pulls/{number}/comments?per_page=100")
|
||||
reviews = paged(f"repos/{repo}/pulls/{number}/reviews?per_page=100")
|
||||
|
||||
hunk_pattern = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@")
|
||||
changed_lines = {}
|
||||
for file_data in files:
|
||||
path = str(file_data.get("filename") or "")
|
||||
line = None
|
||||
lines = set()
|
||||
for patch_line in (file_data.get("patch") or "").splitlines():
|
||||
hunk = hunk_pattern.match(patch_line)
|
||||
if hunk:
|
||||
line = int(hunk.group(1))
|
||||
continue
|
||||
if line is None or patch_line.startswith("\\"):
|
||||
continue
|
||||
if patch_line.startswith("+") and not patch_line.startswith("+++"):
|
||||
lines.add(line)
|
||||
line += 1
|
||||
elif patch_line.startswith("-") and not patch_line.startswith("---"):
|
||||
continue
|
||||
else:
|
||||
line += 1
|
||||
changed_lines[path] = lines
|
||||
|
||||
def fingerprint(path, line):
|
||||
normalized = "\n".join((path, str(line)))
|
||||
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:16]
|
||||
|
||||
current_fingerprints = set()
|
||||
current_locations = set()
|
||||
marker_pattern = re.compile(r"<!-- pr-agent-review:([0-9a-f]{16}) -->")
|
||||
for comment in comments:
|
||||
if comment.get("user", {}).get("login") != "github-actions[bot]":
|
||||
continue
|
||||
if comment.get("line") is None:
|
||||
continue
|
||||
match = marker_pattern.search(str(comment.get("body") or ""))
|
||||
if match:
|
||||
current_fingerprints.add(match.group(1))
|
||||
path = str(comment.get("path") or "")
|
||||
try:
|
||||
line = int(comment.get("line") or 0)
|
||||
except (TypeError, ValueError):
|
||||
line = 0
|
||||
if path and line > 0:
|
||||
current_locations.add((path, line))
|
||||
|
||||
def code_url(path, line):
|
||||
return f"https://github.com/{repo}/blob/{head_sha}/{quote(path, safe='/')}#L{line}"
|
||||
|
||||
issues = review.get("key_issues_to_review") or []
|
||||
findings = []
|
||||
seen = set()
|
||||
for issue in issues:
|
||||
if not isinstance(issue, dict):
|
||||
continue
|
||||
path = str(issue.get("relevant_file") or "").strip()
|
||||
header = str(issue.get("issue_header") or "").strip()
|
||||
content = str(issue.get("issue_content") or "").strip()
|
||||
try:
|
||||
line = int(issue.get("start_line") or 0)
|
||||
except (TypeError, ValueError):
|
||||
line = 0
|
||||
if not path or not header or not content or line < 1:
|
||||
continue
|
||||
finding_key = (path, line, header.lower(), " ".join(content.split()).lower())
|
||||
if finding_key in seen:
|
||||
continue
|
||||
seen.add(finding_key)
|
||||
findings.append({
|
||||
"path": path,
|
||||
"line": line,
|
||||
"header": header,
|
||||
"content": content,
|
||||
"fingerprint": fingerprint(path, line),
|
||||
})
|
||||
|
||||
new_comments = []
|
||||
for finding in findings:
|
||||
if finding["line"] not in changed_lines.get(finding["path"], set()):
|
||||
continue
|
||||
if finding["fingerprint"] in current_fingerprints or (finding["path"], finding["line"]) in current_locations:
|
||||
continue
|
||||
new_comments.append({
|
||||
"path": finding["path"],
|
||||
"line": finding["line"],
|
||||
"side": "RIGHT",
|
||||
"body": "\n".join([
|
||||
f"<!-- pr-agent-review:{finding['fingerprint']} -->",
|
||||
f"**{finding['header']}**",
|
||||
"",
|
||||
finding["content"],
|
||||
]),
|
||||
})
|
||||
|
||||
marker = "<!-- pr-agent-review-summary -->"
|
||||
short_sha = head_sha[:7]
|
||||
commit_url = f"https://github.com/{repo}/commit/{head_sha}"
|
||||
chinese = language == "zh-CN"
|
||||
lines = [marker, "## PR-Agent Code Review", ""]
|
||||
if findings:
|
||||
for finding in findings:
|
||||
location = f"{finding['path']}:{finding['line']}"
|
||||
concise = " ".join(finding["content"].split())[:360]
|
||||
separator = ":" if chinese else ":"
|
||||
lines.append(f"- [{location}]({code_url(finding['path'], finding['line'])}){separator} **{finding['header']}** - {concise}")
|
||||
elif chinese:
|
||||
lines.append("本次变更无需提出审查意见,暂无其他反馈。")
|
||||
else:
|
||||
lines.append("There are no review comments for the current changes. I have no additional feedback to provide.")
|
||||
lines.extend([
|
||||
"",
|
||||
f"审查提交:[{short_sha}]({commit_url})" if chinese else f"Reviewed commit: [{short_sha}]({commit_url})",
|
||||
"",
|
||||
])
|
||||
payload = {
|
||||
"body": "\n".join(lines),
|
||||
"commit_id": head_sha,
|
||||
"event": "COMMENT",
|
||||
}
|
||||
if new_comments:
|
||||
payload["comments"] = new_comments
|
||||
has_matching_summary = not new_comments and any(
|
||||
existing.get("user", {}).get("login") == "github-actions[bot]"
|
||||
and existing.get("commit_id") == head_sha
|
||||
and str(existing.get("body") or "") == payload["body"]
|
||||
for existing in reviews
|
||||
)
|
||||
# 同一提交的手工重审仍会完成分析;完全相同的结果不重复发布 Review。
|
||||
if not has_matching_summary:
|
||||
with open(sys.argv[1], "w", encoding="utf-8") as handle:
|
||||
json.dump(payload, handle, ensure_ascii=False)
|
||||
PY
|
||||
|
||||
latest_head_sha="$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.head.sha')"
|
||||
if [ "${latest_head_sha}" != "${REVIEWED_HEAD_SHA}" ]; then
|
||||
echo "PR head changed while rendering review; skip stale review publication."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -s "${review_payload}" ]; then
|
||||
gh api --method POST "repos/${REPO}/pulls/${PR_NUMBER}/reviews" --input "${review_payload}" >/dev/null
|
||||
fi
|
||||
old_summary_ids="$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments?per_page=100" --jq ".[] | select(.user.login == \"github-actions[bot]\" and ((.body | startswith(\"<!-- pr-agent-update-notification -->\")) or (.body | startswith(\"<!-- pr-agent-code-review-summary -->\")) or (.body | startswith(\"<!-- pr-agent-review-summary -->\")) or (.body | startswith(\"<!-- pr-agent-lab:review -->\")))) | .id")"
|
||||
while IFS= read -r comment_id; do
|
||||
[ -z "${comment_id}" ] && continue
|
||||
gh api --method DELETE "repos/${REPO}/issues/comments/${comment_id}" >/dev/null
|
||||
done <<< "${old_summary_ids}"
|
||||
|
||||
- name: Answer PR question
|
||||
if: >-
|
||||
steps.pr_context.outputs.skip_pr_agent != 'true' &&
|
||||
github.event_name == 'issue_comment' &&
|
||||
(
|
||||
github.event.comment.body == '/ask' ||
|
||||
startsWith(github.event.comment.body, '/ask ')
|
||||
)
|
||||
uses: docker://pragent/pr-agent:0.39.0-github_action@sha256:b253845caa8c7ff5ce8be78f32996647982bdd4890826a962b78eff2e385a825
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
OPENAI.API_BASE: ${{ secrets.OPENAI_API_BASE }}
|
||||
config.model: 'gpt-5.6-terra'
|
||||
config.fallback_models: '["gpt-5.5", "gpt-5.4"]'
|
||||
config.custom_model_max_tokens: '1050000'
|
||||
config.reasoning_effort: 'high'
|
||||
config.ai_timeout: '900'
|
||||
config.response_language: ${{ steps.pr_context.outputs.response_language }}
|
||||
config.large_patch_policy: 'clip'
|
||||
config.ignore_pr_title: '["^\\[Auto\\]", "^Auto"]'
|
||||
config.ignore_pr_labels: '["skip pr-agent"]'
|
||||
github_action_config.auto_review: 'false'
|
||||
github_action_config.auto_describe: 'false'
|
||||
github_action_config.auto_improve: 'false'
|
||||
PRR_AUTO_REVIEW_SCOPE: all
|
||||
PRR_ALLOWED_ASSOCIATIONS: '["OWNER", "MEMBER", "COLLABORATOR", "CONTRIBUTOR", "FIRST_TIME_CONTRIBUTOR"]'
|
||||
PRR_DISABLED_COMMANDS: '["/improve"]'
|
||||
PRR_SKIP_LABEL: skip pr-agent
|
||||
PRR_SKIP_TITLE_PATTERN: '^(?:\[Auto\]|Auto)'
|
||||
config.response_language: zh-CN
|
||||
|
||||
Reference in New Issue
Block a user