mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-07-08 21:07:18 +08:00
ci(pr-agent): simplify review workflow (#6082)
This commit is contained in:
631
.github/workflows/pr-agent.yml
vendored
631
.github/workflows/pr-agent.yml
vendored
@@ -28,7 +28,6 @@ permissions:
|
||||
jobs:
|
||||
pr-agent:
|
||||
name: PR-Agent inline review
|
||||
# PR 事件自动处理;评论命令仅允许指定身份在 PR 下触发,避免任意评论消耗模型配额。
|
||||
if: >-
|
||||
github.event.sender.type != 'Bot' &&
|
||||
(
|
||||
@@ -48,10 +47,10 @@ jobs:
|
||||
)
|
||||
)
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.issue.number || github.ref }}-${{ github.event_name == 'issue_comment' && (github.event.comment.body == '/ask' || startsWith(github.event.comment.body, '/ask ')) && github.event.comment.id || 'write' }}
|
||||
cancel-in-progress: false
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.issue.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request_target' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Detect PR review language
|
||||
id: pr_language
|
||||
@@ -59,14 +58,12 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
|
||||
EVENT_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pr_info="$(mktemp)"
|
||||
gh api "repos/${REPO}/pulls/${PR_NUMBER}" > "${pr_info}"
|
||||
python3 - "${pr_info}" >> "${GITHUB_OUTPUT}" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -76,7 +73,7 @@ jobs:
|
||||
body = pr.get("body") or ""
|
||||
labels = {item.get("name", "") for item in pr.get("labels") or []}
|
||||
skip_pr_agent = "true" if "skip pr-agent" in labels or re.search(r"^(?:\[Auto\]|Auto)", title) else "false"
|
||||
head_sha = os.environ.get("EVENT_HEAD_SHA") or pr.get("head", {}).get("sha") or ""
|
||||
head_sha = pr.get("head", {}).get("sha") or ""
|
||||
|
||||
body = re.sub(
|
||||
r"\n*##\s+(PR-Agent\s+摘要|PR-Agent Summary)\s*\n\s*"
|
||||
@@ -93,8 +90,9 @@ jobs:
|
||||
)
|
||||
body = re.sub(r"```.*?```", " ", body, flags=re.DOTALL)
|
||||
|
||||
cjk_count = len(re.findall(r"[\u4e00-\u9fff]", body))
|
||||
latin_words = len(re.findall(r"\b[A-Za-z][A-Za-z]{2,}\b", body))
|
||||
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:
|
||||
response_language = "zh-CN"
|
||||
@@ -116,29 +114,10 @@ jobs:
|
||||
print(f"head_sha={head_sha}")
|
||||
PY
|
||||
|
||||
- name: Check PR head before side effects
|
||||
id: run_state
|
||||
if: steps.pr_language.outputs.skip_pr_agent != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
|
||||
RUN_HEAD_SHA: ${{ steps.pr_language.outputs.head_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
current_head_sha="$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.head.sha')"
|
||||
is_stale=false
|
||||
if [ -n "${RUN_HEAD_SHA}" ] && [ "${current_head_sha}" != "${RUN_HEAD_SHA}" ]; then
|
||||
echo "PR head changed from ${RUN_HEAD_SHA} to ${current_head_sha}; skip stale PR-Agent run."
|
||||
is_stale=true
|
||||
fi
|
||||
echo "is_stale=${is_stale}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Prepare PR-Agent description markers
|
||||
id: prepare_description
|
||||
if: >-
|
||||
steps.pr_language.outputs.skip_pr_agent != 'true' &&
|
||||
steps.run_state.outputs.is_stale != 'true' &&
|
||||
(
|
||||
github.event_name == 'pull_request_target' ||
|
||||
(
|
||||
@@ -154,15 +133,11 @@ jobs:
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
|
||||
SUMMARY_HEADING: ${{ steps.pr_language.outputs.summary_heading }}
|
||||
RUN_HEAD_SHA: ${{ steps.pr_language.outputs.head_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
current_body="$(mktemp)"
|
||||
latest_body="$(mktemp)"
|
||||
latest_response="$(mktemp)"
|
||||
next_body="$(mktemp)"
|
||||
payload="$(mktemp)"
|
||||
etag_file="$(mktemp)"
|
||||
body_backup="${RUNNER_TEMP}/pr-agent-body-before-describe.md"
|
||||
placeholder_body="${RUNNER_TEMP}/pr-agent-body-with-placeholder.md"
|
||||
|
||||
@@ -182,9 +157,7 @@ jobs:
|
||||
end = "<!-- pr-agent-summary:end -->"
|
||||
placeholder = "pr_agent:summary"
|
||||
summary_heading = os.environ.get("SUMMARY_HEADING") or "PR-Agent 摘要"
|
||||
run_head_sha = os.environ.get("RUN_HEAD_SHA") or ""
|
||||
run_marker = f"<!-- pr-agent-run-head:{run_head_sha} -->\n" if run_head_sha else ""
|
||||
agent_block = f"## {summary_heading}\n\n{start}\n{run_marker}{placeholder}\n{end}\n"
|
||||
agent_block = f"## {summary_heading}\n\n{start}\n{placeholder}\n{end}\n"
|
||||
body = re.sub(
|
||||
r"(?im)^##\s+(PR-Agent\s+摘要|PR-Agent Summary)\s*\n\s*(?=<!-- pr-agent-summary:start -->)",
|
||||
f"## {summary_heading}\n\n",
|
||||
@@ -194,7 +167,7 @@ jobs:
|
||||
start_index = body.find(start)
|
||||
end_index = body.find(end)
|
||||
if start_index >= 0 and end_index > start_index:
|
||||
next_body = body[: start_index + len(start)] + f"\n{run_marker}{placeholder}\n" + body[end_index:]
|
||||
next_body = body[: start_index + len(start)] + f"\n{placeholder}\n" + body[end_index:]
|
||||
else:
|
||||
separator = "\n\n" if body.strip() else ""
|
||||
next_body = body.rstrip() + separator + agent_block
|
||||
@@ -204,40 +177,8 @@ jobs:
|
||||
cp "${next_body}" "${placeholder_body}"
|
||||
|
||||
body_changed=false
|
||||
can_describe=true
|
||||
if ! cmp -s "${current_body}" "${next_body}"; then
|
||||
gh api -i "repos/${REPO}/pulls/${PR_NUMBER}" > "${latest_response}"
|
||||
python3 - "${latest_response}" "${latest_body}" "${etag_file}" <<'PY'
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
raw = Path(sys.argv[1]).read_text()
|
||||
parts = re.split(r"\r?\n\r?\n", raw)
|
||||
headers = parts[-2] if len(parts) > 1 else ""
|
||||
body = parts[-1]
|
||||
etag = ""
|
||||
last_modified = ""
|
||||
for line in headers.splitlines():
|
||||
lower = line.lower()
|
||||
if lower.startswith("etag:"):
|
||||
etag = line.split(":", 1)[1].strip()
|
||||
elif lower.startswith("last-modified:"):
|
||||
last_modified = line.split(":", 1)[1].strip()
|
||||
Path(sys.argv[2]).write_text(json.loads(body).get("body") or "")
|
||||
condition_header = ""
|
||||
if etag and not etag.lower().startswith("w/"):
|
||||
condition_header = f"If-Match: {etag}"
|
||||
elif last_modified:
|
||||
condition_header = f"If-Unmodified-Since: {last_modified}"
|
||||
Path(sys.argv[3]).write_text(condition_header)
|
||||
PY
|
||||
if ! cmp -s "${current_body}" "${latest_body}"; then
|
||||
echo "PR body changed while preparing PR-Agent markers; skip placeholder update."
|
||||
can_describe=false
|
||||
else
|
||||
python3 - "${next_body}" "${payload}" <<'PY'
|
||||
python3 - "${next_body}" "${payload}" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -245,125 +186,17 @@ jobs:
|
||||
body = Path(sys.argv[1]).read_text()
|
||||
Path(sys.argv[2]).write_text(json.dumps({"body": body}, ensure_ascii=False))
|
||||
PY
|
||||
condition_header="$(cat "${etag_file}")"
|
||||
if [ -z "${condition_header}" ]; then
|
||||
echo "No conditional validator available; skip placeholder update."
|
||||
can_describe=false
|
||||
else
|
||||
gh api --method PATCH "repos/${REPO}/pulls/${PR_NUMBER}" -H "${condition_header}" --input "${payload}" >/dev/null
|
||||
body_changed=true
|
||||
fi
|
||||
fi
|
||||
gh api --method PATCH "repos/${REPO}/pulls/${PR_NUMBER}" --input "${payload}" >/dev/null
|
||||
body_changed=true
|
||||
fi
|
||||
echo "body_changed=${body_changed}" >> "${GITHUB_OUTPUT}"
|
||||
echo "can_describe=${can_describe}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Check PR head before PR-Agent
|
||||
id: pragent_head
|
||||
if: >-
|
||||
steps.pr_language.outputs.skip_pr_agent != 'true' &&
|
||||
steps.run_state.outputs.is_stale != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
|
||||
RUN_HEAD_SHA: ${{ steps.pr_language.outputs.head_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
current_head_sha="$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.head.sha')"
|
||||
is_stale=false
|
||||
if [ -n "${RUN_HEAD_SHA}" ] && [ "${current_head_sha}" != "${RUN_HEAD_SHA}" ]; then
|
||||
echo "PR head changed from ${RUN_HEAD_SHA} to ${current_head_sha}; skip stale PR-Agent run."
|
||||
is_stale=true
|
||||
fi
|
||||
echo "is_stale=${is_stale}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Check existing PR-Agent review summary
|
||||
id: review_history
|
||||
if: >-
|
||||
steps.pr_language.outputs.skip_pr_agent != 'true' &&
|
||||
steps.run_state.outputs.is_stale != 'true' &&
|
||||
steps.pragent_head.outputs.is_stale != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
|
||||
RUN_HEAD_SHA: ${{ steps.pr_language.outputs.head_sha }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
CAN_DESCRIBE: ${{ steps.prepare_description.outputs.can_describe }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
can_describe="${CAN_DESCRIBE:-true}"
|
||||
should_improve=true
|
||||
should_publish_existing_summary=false
|
||||
push_commands='["/describe", "/improve"]'
|
||||
if [ "${can_describe}" = "false" ]; then
|
||||
push_commands='["/improve"]'
|
||||
fi
|
||||
if [ "${EVENT_NAME}" = "pull_request_target" ]; then
|
||||
existing_summary_id="$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments?per_page=100" --jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | startswith(\"<!-- pr-agent-code-review-summary -->\")) and (.body | contains(\"/commit/${RUN_HEAD_SHA}\"))) | .id" | tail -n 1)"
|
||||
existing_inline_id=""
|
||||
if [ -z "${existing_summary_id}" ]; then
|
||||
comments="$(mktemp)"
|
||||
gh api --paginate "repos/${REPO}/pulls/${PR_NUMBER}/comments?per_page=100" --jq '.[] | @json' > "${comments}"
|
||||
existing_inline_id="$(python3 - "${comments}" "${RUN_HEAD_SHA}" <<'PY'
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
comments_path = Path(sys.argv[1])
|
||||
head_sha = sys.argv[2]
|
||||
marker_re = re.compile(r"^\s*<!--\s*pr-agent-inline-comment(?::[^>]*)?\s*-->\s*", re.IGNORECASE)
|
||||
|
||||
def is_pr_agent_suggestion(body: str) -> bool:
|
||||
return bool(marker_re.search(body or ""))
|
||||
|
||||
for line in comments_path.read_text().splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
item = json.loads(line)
|
||||
if (
|
||||
item.get("user", {}).get("login") == "github-actions[bot]"
|
||||
and item.get("commit_id") == head_sha
|
||||
and is_pr_agent_suggestion(item.get("body") or "")
|
||||
):
|
||||
print(item.get("id") or "")
|
||||
break
|
||||
PY
|
||||
)"
|
||||
fi
|
||||
if [ -n "${existing_summary_id}" ] || [ -n "${existing_inline_id}" ]; then
|
||||
should_improve=false
|
||||
if [ -z "${existing_summary_id}" ] && [ -n "${existing_inline_id}" ]; then
|
||||
should_publish_existing_summary=true
|
||||
fi
|
||||
if [ "${can_describe}" = "false" ]; then
|
||||
push_commands='[]'
|
||||
else
|
||||
push_commands='["/describe"]'
|
||||
fi
|
||||
echo "PR-Agent code review summary already exists for ${RUN_HEAD_SHA}; skip duplicate inline review."
|
||||
fi
|
||||
fi
|
||||
echo "should_improve=${should_improve}" >> "${GITHUB_OUTPUT}"
|
||||
echo "should_publish_existing_summary=${should_publish_existing_summary}" >> "${GITHUB_OUTPUT}"
|
||||
echo "push_commands=${push_commands}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Snapshot PR-Agent review state
|
||||
- name: Snapshot PR-Agent inline comments
|
||||
id: inline_state_before
|
||||
if: >-
|
||||
steps.pr_language.outputs.skip_pr_agent != 'true' &&
|
||||
steps.run_state.outputs.is_stale != 'true' &&
|
||||
steps.pragent_head.outputs.is_stale != 'true' &&
|
||||
(
|
||||
(
|
||||
github.event_name == 'pull_request_target' &&
|
||||
(
|
||||
steps.review_history.outputs.should_improve == 'true' ||
|
||||
steps.review_history.outputs.should_publish_existing_summary == 'true'
|
||||
)
|
||||
) ||
|
||||
github.event_name == 'pull_request_target' ||
|
||||
(
|
||||
github.event_name == 'issue_comment' &&
|
||||
(
|
||||
@@ -376,31 +209,15 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
|
||||
RUN_HEAD_SHA: ${{ steps.pr_language.outputs.head_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
inline_ids="$(gh api --paginate "repos/${REPO}/pulls/${PR_NUMBER}/comments?per_page=100" --jq '.[] | select(.user.login == "github-actions[bot]") | .id' | jq -sc '.')"
|
||||
inline_ids_b64="$(printf '%s' "${inline_ids}" | base64 -w0)"
|
||||
review_ids="$(gh api --paginate "repos/${REPO}/pulls/${PR_NUMBER}/reviews?per_page=100" --jq '.[] | select(.user.login == "github-actions[bot]") | .id' | jq -sc '.')"
|
||||
review_ids_b64="$(printf '%s' "${review_ids}" | base64 -w0)"
|
||||
echo "inline_ids_b64=${inline_ids_b64}" >> "${GITHUB_OUTPUT}"
|
||||
echo "review_ids_b64=${review_ids_b64}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Run PR-Agent
|
||||
id: pragent
|
||||
if: >-
|
||||
steps.pr_language.outputs.skip_pr_agent != 'true' &&
|
||||
steps.run_state.outputs.is_stale != 'true' &&
|
||||
steps.pragent_head.outputs.is_stale != 'true' &&
|
||||
!(
|
||||
steps.prepare_description.outputs.can_describe == 'false' &&
|
||||
github.event_name == 'issue_comment' &&
|
||||
(
|
||||
github.event.comment.body == '/describe' ||
|
||||
startsWith(github.event.comment.body, '/describe ')
|
||||
)
|
||||
)
|
||||
timeout-minutes: 40
|
||||
if: steps.pr_language.outputs.skip_pr_agent != 'true'
|
||||
# 使用版本号加 digest 固定容器构建,避免 tag 被重推后改变运行内容。
|
||||
uses: docker://pragent/pr-agent:0.39.0-github_action@sha256:b253845caa8c7ff5ce8be78f32996647982bdd4890826a962b78eff2e385a825
|
||||
env:
|
||||
@@ -426,13 +243,13 @@ jobs:
|
||||
|
||||
# PR 初次进入评审或后续 push 时,更新 PR 摘要并发布 GitHub Review 行内建议。
|
||||
github_action_config.auto_review: "false"
|
||||
github_action_config.auto_describe: ${{ steps.prepare_description.outputs.can_describe != 'false' }}
|
||||
github_action_config.auto_improve: ${{ steps.review_history.outputs.should_improve }}
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
|
||||
# synchronize 由 push_commands 单独处理;每次 push 更新摘要和行内 Review,旧行评由 GitHub 标记 outdated。
|
||||
github_action_config.pr_actions: '["opened", "reopened", "ready_for_review", "review_requested"]'
|
||||
github_action_config.handle_push_trigger: "true"
|
||||
github_action_config.push_commands: ${{ steps.review_history.outputs.push_commands }}
|
||||
github_action_config.push_commands: '["/describe", "/improve"]'
|
||||
|
||||
# 保留 action outputs,便于后续 workflow 编排或排查。
|
||||
github_action_config.enable_output: "true"
|
||||
@@ -461,10 +278,10 @@ jobs:
|
||||
pr_code_suggestions.extra_instructions: |
|
||||
Match the configured response language.
|
||||
Only provide substantive issues that maintainers should address; avoid style-only, preference-only, or low-value suggestions.
|
||||
Start every suggestion with one of these Markdown prefixes: 🔴 **High Risk**:, 🟡 **Medium Risk**:, or 🔵 **Low Risk**:.
|
||||
For prioritized issues, start the suggestion body with one of these Markdown prefixes: 🔴 **High Risk**:, 🟡 **Medium Risk**:, or 🔵 **Low Risk**:.
|
||||
pr_code_suggestions.focus_only_on_problems: "true"
|
||||
pr_code_suggestions.suggestions_score_threshold: "8"
|
||||
pr_code_suggestions.num_code_suggestions_per_chunk: "2"
|
||||
pr_code_suggestions.suggestions_score_threshold: "3"
|
||||
pr_code_suggestions.num_code_suggestions_per_chunk: "3"
|
||||
pr_code_suggestions.commitable_code_suggestions: "true"
|
||||
pr_code_suggestions.publish_output_no_suggestions: "false"
|
||||
pr_questions.use_conversation_history: "true"
|
||||
@@ -473,214 +290,11 @@ jobs:
|
||||
# github_action_config.auto_improve: "true"
|
||||
# config.verbosity_level: "1"
|
||||
|
||||
- name: Mark PR-Agent inline comments
|
||||
if: >-
|
||||
always() &&
|
||||
steps.pr_language.outputs.skip_pr_agent != 'true' &&
|
||||
steps.run_state.outputs.is_stale != 'true' &&
|
||||
steps.pragent_head.outputs.is_stale != 'true' &&
|
||||
steps.inline_state_before.outputs.inline_ids_b64 != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
|
||||
BEFORE_INLINE_IDS_B64: ${{ steps.inline_state_before.outputs.inline_ids_b64 }}
|
||||
BEFORE_REVIEW_IDS_B64: ${{ steps.inline_state_before.outputs.review_ids_b64 }}
|
||||
PR_AGENT_INLINE_MARKER: "<!-- pr-agent-inline-comment:${{ github.run_id }} -->"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
before_ids="$(printf '%s' "${BEFORE_INLINE_IDS_B64:-W10=}" | base64 -d)"
|
||||
before_review_ids="$(printf '%s' "${BEFORE_REVIEW_IDS_B64:-W10=}" | base64 -d)"
|
||||
comments="$(mktemp)"
|
||||
reviews="$(mktemp)"
|
||||
patches="$(mktemp)"
|
||||
gh api --paginate "repos/${REPO}/pulls/${PR_NUMBER}/comments?per_page=100" --jq '.[] | @json' > "${comments}"
|
||||
gh api --paginate "repos/${REPO}/pulls/${PR_NUMBER}/reviews?per_page=100" --jq '.[] | @json' > "${reviews}"
|
||||
python3 - "${before_ids}" "${before_review_ids}" "${comments}" "${reviews}" "${patches}" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
before_ids = set(json.loads(sys.argv[1] or "[]"))
|
||||
before_review_ids = set(json.loads(sys.argv[2] or "[]"))
|
||||
comments_path = Path(sys.argv[3])
|
||||
reviews_path = Path(sys.argv[4])
|
||||
patches_path = Path(sys.argv[5])
|
||||
marker = os.environ["PR_AGENT_INLINE_MARKER"]
|
||||
marker_re = re.compile(r"^\s*<!--\s*pr-agent-inline-comment(?::[^>]*)?\s*-->\s*", re.IGNORECASE)
|
||||
risk_prefixes = ("🔴 **High Risk**:", "🟡 **Medium Risk**:", "🔵 **Low Risk**:")
|
||||
|
||||
def is_pr_agent_inline_body(body: str) -> bool:
|
||||
visible = (body or "").lstrip()
|
||||
return visible.startswith("**Suggestion:**") or visible.startswith(risk_prefixes)
|
||||
|
||||
new_review_ids = set()
|
||||
for line in reviews_path.read_text().splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
item = json.loads(line)
|
||||
if (
|
||||
item.get("user", {}).get("login") == "github-actions[bot]"
|
||||
and item.get("id") not in before_review_ids
|
||||
):
|
||||
new_review_ids.add(item["id"])
|
||||
|
||||
patches = []
|
||||
for line in comments_path.read_text().splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
item = json.loads(line)
|
||||
body = item.get("body") or ""
|
||||
if (
|
||||
item.get("user", {}).get("login") == "github-actions[bot]"
|
||||
and item.get("id") not in before_ids
|
||||
and item.get("pull_request_review_id") in new_review_ids
|
||||
and is_pr_agent_inline_body(body)
|
||||
and not marker_re.search(body)
|
||||
):
|
||||
patches.append({"id": item["id"], "body": f"{marker}\n{body}"})
|
||||
|
||||
patches_path.write_text("\n".join(json.dumps(item, ensure_ascii=False) for item in patches))
|
||||
PY
|
||||
|
||||
if [ ! -s "${patches}" ]; then
|
||||
echo "No PR-Agent inline comments to mark."
|
||||
exit 0
|
||||
fi
|
||||
patch_failed=false
|
||||
while IFS= read -r patch || [ -n "${patch}" ]; do
|
||||
[ -z "${patch}" ] && continue
|
||||
comment_id="$(printf '%s' "${patch}" | jq -r '.id')"
|
||||
payload="$(mktemp)"
|
||||
err="$(mktemp)"
|
||||
printf '%s' "${patch}" | jq '{body:.body}' > "${payload}"
|
||||
if ! gh api --method PATCH "repos/${REPO}/pulls/comments/${comment_id}" --input "${payload}" >/dev/null 2>"${err}"; then
|
||||
cat "${err}" >&2
|
||||
echo "PR-Agent inline comment ${comment_id} could not be marked."
|
||||
patch_failed=true
|
||||
fi
|
||||
done < "${patches}"
|
||||
if [ "${patch_failed}" = "true" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Clean stale PR-Agent inline comments
|
||||
id: post_pragent_state
|
||||
if: >-
|
||||
always() &&
|
||||
steps.pr_language.outputs.skip_pr_agent != 'true' &&
|
||||
steps.run_state.outputs.is_stale != 'true' &&
|
||||
steps.pragent_head.outputs.is_stale != 'true' &&
|
||||
steps.inline_state_before.outputs.inline_ids_b64 != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
|
||||
RUN_HEAD_SHA: ${{ steps.pr_language.outputs.head_sha }}
|
||||
BEFORE_INLINE_IDS_B64: ${{ steps.inline_state_before.outputs.inline_ids_b64 }}
|
||||
BEFORE_REVIEW_IDS_B64: ${{ steps.inline_state_before.outputs.review_ids_b64 }}
|
||||
PR_AGENT_INLINE_MARKER: "<!-- pr-agent-inline-comment:${{ github.run_id }} -->"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
current_head_sha="$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.head.sha')"
|
||||
is_stale=false
|
||||
if [ -n "${RUN_HEAD_SHA}" ] && [ "${current_head_sha}" != "${RUN_HEAD_SHA}" ]; then
|
||||
is_stale=true
|
||||
fi
|
||||
echo "is_stale=${is_stale}" >> "${GITHUB_OUTPUT}"
|
||||
if [ "${is_stale}" != "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
before_ids="$(printf '%s' "${BEFORE_INLINE_IDS_B64:-W10=}" | base64 -d)"
|
||||
before_review_ids="$(printf '%s' "${BEFORE_REVIEW_IDS_B64:-W10=}" | base64 -d)"
|
||||
comments="$(mktemp)"
|
||||
reviews="$(mktemp)"
|
||||
delete_ids="$(mktemp)"
|
||||
gh api --paginate "repos/${REPO}/pulls/${PR_NUMBER}/comments?per_page=100" --jq '.[] | @json' > "${comments}"
|
||||
gh api --paginate "repos/${REPO}/pulls/${PR_NUMBER}/reviews?per_page=100" --jq '.[] | @json' > "${reviews}"
|
||||
python3 - "${before_ids}" "${before_review_ids}" "${comments}" "${reviews}" "${delete_ids}" "${RUN_HEAD_SHA}" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
before_ids = set(json.loads(sys.argv[1] or "[]"))
|
||||
before_review_ids = set(json.loads(sys.argv[2] or "[]"))
|
||||
comments_path = Path(sys.argv[3])
|
||||
reviews_path = Path(sys.argv[4])
|
||||
delete_path = Path(sys.argv[5])
|
||||
head_sha = sys.argv[6]
|
||||
marker = os.environ["PR_AGENT_INLINE_MARKER"]
|
||||
|
||||
def is_pr_agent_suggestion(item: dict) -> bool:
|
||||
return (item.get("body") or "").lstrip().startswith(marker)
|
||||
|
||||
new_review_ids = set()
|
||||
for line in reviews_path.read_text().splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
item = json.loads(line)
|
||||
if (
|
||||
item.get("user", {}).get("login") == "github-actions[bot]"
|
||||
and item.get("id") not in before_review_ids
|
||||
):
|
||||
new_review_ids.add(item["id"])
|
||||
|
||||
delete_ids = []
|
||||
for line in comments_path.read_text().splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
item = json.loads(line)
|
||||
if (
|
||||
item.get("user", {}).get("login") == "github-actions[bot]"
|
||||
and item.get("id") not in before_ids
|
||||
and item.get("pull_request_review_id") in new_review_ids
|
||||
and is_pr_agent_suggestion(item)
|
||||
):
|
||||
delete_ids.append(str(item["id"]))
|
||||
|
||||
delete_path.write_text("\n".join(delete_ids))
|
||||
PY
|
||||
|
||||
if [ ! -s "${delete_ids}" ]; then
|
||||
echo "No stale PR-Agent inline comments to clean."
|
||||
exit 0
|
||||
fi
|
||||
delete_failed=false
|
||||
while IFS= read -r comment_id || [ -n "${comment_id}" ]; do
|
||||
[ -z "${comment_id}" ] && continue
|
||||
err="$(mktemp)"
|
||||
if ! gh api --method DELETE "repos/${REPO}/pulls/comments/${comment_id}" >/dev/null 2>"${err}"; then
|
||||
if grep -qiE "HTTP 404|Not Found" "${err}"; then
|
||||
echo "PR-Agent inline comment ${comment_id} was already removed; continue."
|
||||
else
|
||||
cat "${err}" >&2
|
||||
echo "PR-Agent inline comment ${comment_id} could not be deleted."
|
||||
delete_failed=true
|
||||
fi
|
||||
fi
|
||||
done < "${delete_ids}"
|
||||
if [ "${delete_failed}" = "true" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Publish PR-Agent code review summary
|
||||
if: >-
|
||||
steps.pr_language.outputs.skip_pr_agent != 'true' &&
|
||||
steps.run_state.outputs.is_stale != 'true' &&
|
||||
steps.pragent_head.outputs.is_stale != 'true' &&
|
||||
steps.post_pragent_state.outputs.is_stale != 'true' &&
|
||||
(
|
||||
(
|
||||
github.event_name == 'pull_request_target' &&
|
||||
(
|
||||
steps.review_history.outputs.should_improve == 'true' ||
|
||||
steps.review_history.outputs.should_publish_existing_summary == 'true'
|
||||
)
|
||||
) ||
|
||||
github.event_name == 'pull_request_target' ||
|
||||
(
|
||||
github.event_name == 'issue_comment' &&
|
||||
(
|
||||
@@ -694,21 +308,16 @@ jobs:
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
|
||||
BEFORE_INLINE_IDS_B64: ${{ steps.inline_state_before.outputs.inline_ids_b64 }}
|
||||
BEFORE_REVIEW_IDS_B64: ${{ steps.inline_state_before.outputs.review_ids_b64 }}
|
||||
RUN_HEAD_SHA: ${{ steps.pr_language.outputs.head_sha }}
|
||||
SUMMARY_LANGUAGE: ${{ steps.pr_language.outputs.summary_language }}
|
||||
USE_EXISTING_INLINE_SUMMARY: ${{ github.event_name == 'pull_request_target' && steps.review_history.outputs.should_publish_existing_summary == 'true' }}
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
OPENAI_API_BASE: ${{ secrets.OPENAI_API_BASE }}
|
||||
SUMMARY_MODEL: gpt-5.5
|
||||
PR_AGENT_INLINE_MARKER: "<!-- pr-agent-inline-comment:${{ github.run_id }} -->"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
before_ids="$(printf '%s' "${BEFORE_INLINE_IDS_B64:-W10=}" | base64 -d)"
|
||||
before_review_ids="$(printf '%s' "${BEFORE_REVIEW_IDS_B64:-W10=}" | base64 -d)"
|
||||
pr_info="$(mktemp)"
|
||||
comments="$(mktemp)"
|
||||
reviews="$(mktemp)"
|
||||
review_data="$(mktemp)"
|
||||
payload="$(mktemp)"
|
||||
|
||||
@@ -724,76 +333,34 @@ jobs:
|
||||
commit_url="https://github.com/${REPO}/commit/${HEAD_SHA}"
|
||||
|
||||
gh api --paginate "repos/${REPO}/pulls/${PR_NUMBER}/comments?per_page=100" --jq '.[] | @json' > "${comments}"
|
||||
gh api --paginate "repos/${REPO}/pulls/${PR_NUMBER}/reviews?per_page=100" --jq '.[] | @json' > "${reviews}"
|
||||
|
||||
python3 - "${before_ids}" "${before_review_ids}" "${comments}" "${reviews}" "${review_data}" "${HEAD_SHA}" "${pr_url}" "${commit_url}" "${short_sha}" <<'PY'
|
||||
python3 - "${before_ids}" "${comments}" "${review_data}" "${HEAD_SHA}" "${pr_url}" "${commit_url}" "${short_sha}" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
before_ids = set(json.loads(sys.argv[1] or "[]"))
|
||||
before_review_ids = set(json.loads(sys.argv[2] or "[]"))
|
||||
comments_path = Path(sys.argv[3])
|
||||
reviews_path = Path(sys.argv[4])
|
||||
output_path = Path(sys.argv[5])
|
||||
head_sha = sys.argv[6]
|
||||
pr_url = sys.argv[7]
|
||||
commit_url = sys.argv[8]
|
||||
short_sha = sys.argv[9]
|
||||
marker = os.environ["PR_AGENT_INLINE_MARKER"]
|
||||
use_existing_inline_summary = (os.environ.get("USE_EXISTING_INLINE_SUMMARY") or "").lower() == "true"
|
||||
marker_re = re.compile(
|
||||
r"^\s*<!--\s*pr-agent-inline-comment(?::[^>]*)?\s*-->\s*",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
def normalize_suggestion_body(body: str) -> str:
|
||||
return marker_re.sub("", body or "", count=1).strip()
|
||||
|
||||
def has_inline_marker(item: dict) -> bool:
|
||||
return bool(marker_re.search(item.get("body") or ""))
|
||||
|
||||
def has_current_run_marker(item: dict) -> bool:
|
||||
return (item.get("body") or "").lstrip().startswith(marker)
|
||||
comments_path = Path(sys.argv[2])
|
||||
output_path = Path(sys.argv[3])
|
||||
head_sha = sys.argv[4]
|
||||
pr_url = sys.argv[5]
|
||||
commit_url = sys.argv[6]
|
||||
short_sha = sys.argv[7]
|
||||
|
||||
comments = []
|
||||
for line in comments_path.read_text().splitlines():
|
||||
if line.strip():
|
||||
comments.append(json.loads(line))
|
||||
|
||||
new_review_ids = set()
|
||||
for line in reviews_path.read_text().splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
item = json.loads(line)
|
||||
if (
|
||||
item.get("user", {}).get("login") == "github-actions[bot]"
|
||||
and item.get("id") not in before_review_ids
|
||||
and item.get("commit_id") == head_sha
|
||||
):
|
||||
new_review_ids.add(item["id"])
|
||||
|
||||
if use_existing_inline_summary:
|
||||
new_comments = [
|
||||
item for item in comments
|
||||
if item.get("user", {}).get("login") == "github-actions[bot]"
|
||||
and item.get("commit_id") == head_sha
|
||||
and has_inline_marker(item)
|
||||
]
|
||||
else:
|
||||
new_comments = [
|
||||
item for item in comments
|
||||
if item.get("user", {}).get("login") == "github-actions[bot]"
|
||||
and item.get("id") not in before_ids
|
||||
and item.get("commit_id") == head_sha
|
||||
and item.get("pull_request_review_id") in new_review_ids
|
||||
and has_current_run_marker(item)
|
||||
]
|
||||
new_comments = [
|
||||
item for item in comments
|
||||
if item.get("user", {}).get("login") == "github-actions[bot]"
|
||||
and item.get("id") not in before_ids
|
||||
and item.get("commit_id") == head_sha
|
||||
]
|
||||
suggestions = []
|
||||
for item in new_comments:
|
||||
body = normalize_suggestion_body(item.get("body") or "")
|
||||
body = (item.get("body") or "").strip()
|
||||
first_line = next((line.strip() for line in body.splitlines() if line.strip()), "")
|
||||
suggestions.append({
|
||||
"path": item.get("path"),
|
||||
@@ -814,6 +381,7 @@ jobs:
|
||||
python3 - "${review_data}" "${payload}" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import textwrap
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
@@ -908,91 +476,58 @@ jobs:
|
||||
data = json.loads(response.read().decode("utf-8"))
|
||||
content = data["choices"][0]["message"]["content"].strip()
|
||||
return content or None
|
||||
except (KeyError, IndexError, TypeError, TimeoutError, urllib.error.URLError, urllib.error.HTTPError, json.JSONDecodeError):
|
||||
except (KeyError, TimeoutError, urllib.error.URLError, urllib.error.HTTPError, json.JSONDecodeError):
|
||||
return None
|
||||
|
||||
summary = llm_summary() or fallback_summary()
|
||||
commit_label = "审查提交:" if use_chinese else "Reviewed commit:"
|
||||
body = "\n".join([
|
||||
marker,
|
||||
"## Code Review",
|
||||
"",
|
||||
summary.strip(),
|
||||
"",
|
||||
f'{commit_label} [{review_data["short_sha"]}]({review_data["commit_url"]})',
|
||||
"",
|
||||
])
|
||||
body = textwrap.dedent(f"""\
|
||||
{marker}
|
||||
## Code Review
|
||||
|
||||
{summary}
|
||||
|
||||
{commit_label} [{review_data["short_sha"]}]({review_data["commit_url"]})
|
||||
""")
|
||||
payload_path.write_text(json.dumps({"body": body}, ensure_ascii=False))
|
||||
PY
|
||||
|
||||
latest_head_sha="$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.head.sha')"
|
||||
if [ "${latest_head_sha}" != "${HEAD_SHA}" ]; then
|
||||
echo "PR head changed from ${HEAD_SHA} to ${latest_head_sha}; skip stale code review summary."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
new_comment="$(mktemp)"
|
||||
gh api --method POST "repos/${REPO}/issues/${PR_NUMBER}/comments" --input "${payload}" > "${new_comment}"
|
||||
new_comment_id="$(jq -r '.id' "${new_comment}")"
|
||||
new_comment_created_at="$(jq -r '.created_at' "${new_comment}")"
|
||||
comment_ids="$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments?per_page=100" --jq ".[] | select(.id != ${new_comment_id} and .user.login == \"github-actions[bot]\" and ((.body | startswith(\"<!-- pr-agent-code-review-summary -->\")) or (.body | startswith(\"<!-- pr-agent-update-notification -->\"))) and (.created_at < \"${new_comment_created_at}\" or (.created_at == \"${new_comment_created_at}\" and .id < ${new_comment_id}))) | .id")"
|
||||
new_comment_id="$(gh api --method POST "repos/${REPO}/issues/${PR_NUMBER}/comments" --input "${payload}" --jq '.id')"
|
||||
comment_ids="$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments?per_page=100" --jq ".[] | select(.id != ${new_comment_id} and .user.login == \"github-actions[bot]\" and ((.body | startswith(\"<!-- pr-agent-code-review-summary -->\")) or (.body | startswith(\"<!-- pr-agent-update-notification -->\")))) | .id")"
|
||||
|
||||
if [ -z "${comment_ids}" ]; then
|
||||
echo "No previous PR-Agent code review summary to clean."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
delete_failed=false
|
||||
while IFS= read -r comment_id; do
|
||||
[ -z "${comment_id}" ] && continue
|
||||
err="$(mktemp)"
|
||||
if ! gh api --method DELETE "repos/${REPO}/issues/comments/${comment_id}" >/dev/null 2>"${err}"; then
|
||||
if grep -qiE "HTTP 404|Not Found" "${err}"; then
|
||||
echo "PR-Agent summary comment ${comment_id} was already removed; continue."
|
||||
else
|
||||
cat "${err}" >&2
|
||||
echo "PR-Agent summary comment ${comment_id} could not be deleted."
|
||||
delete_failed=true
|
||||
fi
|
||||
fi
|
||||
gh api --method DELETE "repos/${REPO}/issues/comments/${comment_id}" >/dev/null
|
||||
done <<< "${comment_ids}"
|
||||
if [ "${delete_failed}" = "true" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Restore PR-Agent description markers on failure
|
||||
if: >-
|
||||
always() &&
|
||||
failure() &&
|
||||
steps.prepare_description.outputs.body_changed == 'true' &&
|
||||
steps.pr_language.outputs.skip_pr_agent != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
|
||||
RUN_HEAD_SHA: ${{ steps.pr_language.outputs.head_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
body_backup="${RUNNER_TEMP}/pr-agent-body-before-describe.md"
|
||||
placeholder_body="${RUNNER_TEMP}/pr-agent-body-with-placeholder.md"
|
||||
current_body="$(mktemp)"
|
||||
latest_body="$(mktemp)"
|
||||
latest_response="$(mktemp)"
|
||||
payload="$(mktemp)"
|
||||
etag_file="$(mktemp)"
|
||||
|
||||
if [ ! -e "${body_backup}" ] || [ ! -s "${placeholder_body}" ]; then
|
||||
if [ ! -s "${body_backup}" ] || [ ! -s "${placeholder_body}" ]; then
|
||||
echo "No PR body backup found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.body // ""' > "${current_body}"
|
||||
current_head_sha="$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.head.sha')"
|
||||
run_head_sha="${RUN_HEAD_SHA:-}"
|
||||
is_stale=false
|
||||
if [ -n "${run_head_sha}" ] && [ "${current_head_sha}" != "${run_head_sha}" ]; then
|
||||
is_stale=true
|
||||
fi
|
||||
|
||||
python3 - "${body_backup}" "${placeholder_body}" "${current_body}" "${payload}" "${run_head_sha}" "${is_stale}" <<'PY'
|
||||
python3 - "${body_backup}" "${placeholder_body}" "${current_body}" "${payload}" <<'PY'
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
@@ -1002,12 +537,9 @@ jobs:
|
||||
placeholder_body = Path(sys.argv[2]).read_text()
|
||||
current_body = Path(sys.argv[3]).read_text()
|
||||
payload_path = Path(sys.argv[4])
|
||||
run_head_sha = sys.argv[5]
|
||||
is_stale = sys.argv[6] == "true"
|
||||
|
||||
start = "<!-- pr-agent-summary:start -->"
|
||||
end = "<!-- pr-agent-summary:end -->"
|
||||
run_marker = f"<!-- pr-agent-run-head:{run_head_sha} -->" if run_head_sha else ""
|
||||
heading_re = re.compile(r"(?im)^##\s+(PR-Agent\s+摘要|PR-Agent Summary)\s*\n\s*")
|
||||
|
||||
def find_block(body: str) -> tuple[int, int] | None:
|
||||
@@ -1034,67 +566,18 @@ jobs:
|
||||
|
||||
current_section = current_body[current_block[0]:current_block[1]]
|
||||
placeholder_section = placeholder_body[placeholder_block[0]:placeholder_block[1]]
|
||||
current_has_marker = bool(run_marker and run_marker in current_section)
|
||||
current_has_placeholder = "pr_agent:summary" in current_section
|
||||
if not current_has_placeholder and not current_has_marker:
|
||||
if "pr_agent:summary" not in current_section:
|
||||
print("No visible PR-Agent summary placeholder to restore.")
|
||||
raise SystemExit(0)
|
||||
if current_has_placeholder and current_section != placeholder_section:
|
||||
if current_section != placeholder_section:
|
||||
print("Current PR-Agent summary block changed; skip restore.")
|
||||
raise SystemExit(0)
|
||||
|
||||
if current_has_marker and not current_has_placeholder and not is_stale:
|
||||
next_section = current_section.replace(f"{run_marker}\n", "").replace(run_marker, "")
|
||||
next_body = current_body[:current_block[0]] + next_section + current_body[current_block[1]:]
|
||||
next_body = re.sub(r"\n{4,}", "\n\n\n", next_body).rstrip() + "\n"
|
||||
payload_path.write_text(json.dumps({"body": next_body}, ensure_ascii=False))
|
||||
raise SystemExit(0)
|
||||
|
||||
restored_section = ""
|
||||
if backup_block:
|
||||
backup_section = backup_body[backup_block[0]:backup_block[1]]
|
||||
if "pr_agent:summary" not in backup_section:
|
||||
restored_section = backup_section
|
||||
restored_section = backup_body[backup_block[0]:backup_block[1]] if backup_block else ""
|
||||
next_body = current_body[:current_block[0]] + restored_section + current_body[current_block[1]:]
|
||||
next_body = re.sub(r"\n{4,}", "\n\n\n", next_body).rstrip() + "\n"
|
||||
payload_path.write_text(json.dumps({"body": next_body}, ensure_ascii=False))
|
||||
PY
|
||||
if [ -s "${payload}" ]; then
|
||||
gh api -i "repos/${REPO}/pulls/${PR_NUMBER}" > "${latest_response}"
|
||||
python3 - "${latest_response}" "${latest_body}" "${etag_file}" <<'PY'
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
raw = Path(sys.argv[1]).read_text()
|
||||
parts = re.split(r"\r?\n\r?\n", raw)
|
||||
headers = parts[-2] if len(parts) > 1 else ""
|
||||
body = parts[-1]
|
||||
etag = ""
|
||||
last_modified = ""
|
||||
for line in headers.splitlines():
|
||||
lower = line.lower()
|
||||
if lower.startswith("etag:"):
|
||||
etag = line.split(":", 1)[1].strip()
|
||||
elif lower.startswith("last-modified:"):
|
||||
last_modified = line.split(":", 1)[1].strip()
|
||||
Path(sys.argv[2]).write_text(json.loads(body).get("body") or "")
|
||||
condition_header = ""
|
||||
if etag and not etag.lower().startswith("w/"):
|
||||
condition_header = f"If-Match: {etag}"
|
||||
elif last_modified:
|
||||
condition_header = f"If-Unmodified-Since: {last_modified}"
|
||||
Path(sys.argv[3]).write_text(condition_header)
|
||||
PY
|
||||
if ! cmp -s "${current_body}" "${latest_body}"; then
|
||||
echo "PR body changed while preparing restore payload; skip restore."
|
||||
exit 0
|
||||
fi
|
||||
condition_header="$(cat "${etag_file}")"
|
||||
if [ -z "${condition_header}" ]; then
|
||||
echo "No conditional validator available; skip restore."
|
||||
exit 0
|
||||
fi
|
||||
gh api --method PATCH "repos/${REPO}/pulls/${PR_NUMBER}" -H "${condition_header}" --input "${payload}" >/dev/null
|
||||
gh api --method PATCH "repos/${REPO}/pulls/${PR_NUMBER}" --input "${payload}" >/dev/null
|
||||
fi
|
||||
|
||||
@@ -34,7 +34,7 @@ workflow 会在 `/improve` 后发布一条普通 PR 评论:
|
||||
- 评论标题为 `## Code Review`。
|
||||
- 如果本轮有新增行内建议,会基于这些建议生成自然语言总结。
|
||||
- 如果本轮没有新增行内建议,直接发布无更多反馈的简短总结。
|
||||
- 发布新总结后会删除上一条 PR-Agent Code Review 总结评论,避免评论堆叠,同时保留新的通知事件。
|
||||
- 下一次运行前会删除上一条 PR-Agent Code Review 总结评论,避免评论堆叠,同时保留新的通知事件。
|
||||
|
||||
## 常用评论命令
|
||||
|
||||
@@ -58,7 +58,7 @@ workflow 会在 `/improve` 后发布一条普通 PR 评论:
|
||||
|
||||
PR-Agent 配置集中在 `.github/workflows/pr-agent.yml` 中维护。公开说明只描述用户可见行为:
|
||||
|
||||
- 根据用户原始 PR 描述自动选择中文或英文;无法识别时默认中文。
|
||||
- 根据 PR 标题和用户原始描述自动选择中文或英文;无法识别时默认中文。
|
||||
- 保留用户原始 PR 描述,只更新 PR Body 中的 PR-Agent 标记区域。
|
||||
- 不使用 PR-Agent 的 Reviewer Guide 输出。
|
||||
- 不输出 PR Type、额外标签、图表或 describe 评论。
|
||||
@@ -83,4 +83,4 @@ PR-Agent 配置集中在 `.github/workflows/pr-agent.yml` 中维护。公开说
|
||||
|
||||
PR-Agent 依赖的 Docker 镜像在 workflow 中固定版本号和 digest,不使用浮动的 `latest` 或仅依赖可变 tag。
|
||||
|
||||
当前使用 `pull_request_target` 支持 fork PR 自动审查,但 workflow 不 checkout 或执行来自 fork 的代码,只运行固定 digest 的 PR-Agent 容器并通过 GitHub API 读取 PR diff。`issue_comment` 属于 base repo 事件,因此评论命令只允许指定身份触发。
|
||||
当前使用 `pull_request_target` 支持 PR 自动审查,但 workflow 不 checkout 或执行 PR 分支代码,只运行固定 digest 的 PR-Agent 容器并通过 GitHub API 读取 PR diff。`issue_comment` 属于 base repo 事件,因此评论命令只允许指定身份触发。
|
||||
|
||||
Reference in New Issue
Block a user