From 92918ce3800de7664ab4cf8978fdceb02fb27e1c Mon Sep 17 00:00:00 2001 From: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com> Date: Thu, 16 Jul 2026 06:24:52 +0800 Subject: [PATCH] ci(pr-agent): use shared review runner (#6127) --- .github/workflows/pr-agent.yml | 506 +-------------------------------- docs/pr-agent.md | 18 +- 2 files changed, 19 insertions(+), 505 deletions(-) diff --git a/.github/workflows/pr-agent.yml b/.github/workflows/pr-agent.yml index 63472d98..0fb35e77 100644 --- a/.github/workflows/pr-agent.yml +++ b/.github/workflows/pr-agent.yml @@ -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".*?", - " ", - 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 = "" - 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".*?\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*(?=)", - 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 = "\npr_agent:summary\n" - owned_block = re.compile( - r"(?ims)^##\s+(?:PR-Agent\s+摘要|PR-Agent\s+Summary)\s*\n\s*" - r"\s*pr_agent:summary\s*\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"") - 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"", - f"**{finding['header']}**", - "", - finding["content"], - ]), - }) - - marker = "" - 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(\"\")) or (.body | startswith(\"\")) or (.body | startswith(\"\")) or (.body | startswith(\"\")))) | .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 diff --git a/docs/pr-agent.md b/docs/pr-agent.md index a13def77..be64ae36 100644 --- a/docs/pr-agent.md +++ b/docs/pr-agent.md @@ -1,6 +1,6 @@ # PR-Agent 使用说明 -本仓库通过 GitHub Actions 运行 PR-Agent,帮助贡献者维护 PR 摘要、获取代码审查结果和提出 PR 相关问题。 +本仓库通过 PR Review Runner 运行 PR-Agent,帮助贡献者维护 PR 摘要、获取代码审查结果和提出 PR 相关问题。 ## 自动执行 @@ -25,26 +25,24 @@ PR 带有 `skip pr-agent` 标签,或标题以 `[Auto]`、`Auto` 开头时, /ask 这次改动有没有遗漏权限校验? ``` -- `/describe`:更新 PR Body 内按语言显示的 `PR-Agent 摘要` 或 `PR-Agent Summary`,并保留贡献者原有的 PR 描述。 +- `/describe`:更新 PR Body 内的 `PR-Agent 摘要`,并保留贡献者原有的 PR 描述。 - `/review`:发起一次代码审查。 - `/ask ...`:就当前 PR 提问,回复会发布在普通 PR 评论中。 +本仓库禁用 `/improve` 及其等价别名。其他命令是否可用由 runner 所包含的 PR-Agent 能力决定。 + 手工命令仅允许以下 GitHub 身份关联的用户使用:`OWNER`、`MEMBER`、`COLLABORATOR`、`CONTRIBUTOR`、`FIRST_TIME_CONTRIBUTOR`。 新建的合法命令评论会触发执行;编辑后仍为合法命令的评论也会触发。编辑普通讨论评论不会调用模型。 ## 审查结果 -`/describe` 的结果位于 PR Body 中按语言显示的 `PR-Agent 摘要` 或 `PR-Agent Summary` 区域,用于概览本次变更。 +本仓库固定使用中文生成 PR-Agent 内容。`/describe` 的结果位于 PR Body 的 `PR-Agent 摘要` 区域,用于概览本次变更。 -`/review` 和自动审查会通过原生 GitHub Review 发布,结果位于 Review 页签,标题固定为 `PR-Agent Code Review`。审查摘要包含可点击的 `文件:行号` 链接;可定位到本次变更的具体问题会在对应代码行以行内评论呈现,无法行内定位的问题仍通过摘要中的链接呈现。 +`/review` 和自动审查会通过原生 GitHub Review 发布,结果位于 Review 页签,标题固定为 `PR-Agent Code Review`。可定位到本次变更的问题会在对应代码行以行内评论呈现,并使用 high、medium 或 low 风险标识。 -审查不会额外创建专用的摘要评论。未发现需要处理的问题时,Review 会显示: - -> 本次变更无需提出审查意见,暂无其他反馈。 +Review 摘要会自然概括本次变更和整体审查结论,不会复制行内评论。未发现需要处理的问题时,摘要会概括变更并自然说明暂无其他反馈。审查不会额外创建专用的 issue comment 摘要。 ## 安全边界 -workflow 使用固定 digest 的 PR-Agent 容器镜像,不使用浮动标签。自动审查通过 `pull_request_target` 在目标仓库上下文中读取 PR 信息,但不会 checkout 或执行 PR 分支代码。 - -权限保持最小化:只授予读取仓库内容所需的 `contents: read`,以及更新 PR Body、发布 Review 和回复 PR 评论所需的写权限;不会向仓库推送代码或创建提交。 +自动审查通过 `pull_request_target` 在目标仓库上下文中读取 PR 信息,并使用共享 runner 的 `latest` 镜像,但不会 checkout 或执行 PR 分支代码。权限保持最小化:只授予读取仓库内容所需的 `contents: read`,以及更新 PR Body、发布 Review 和回复 PR 评论所需的写权限;不会向仓库推送代码或创建提交。