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
68 changes: 64 additions & 4 deletions .github/aicodingflow-tests/test_resolve_pr_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ def test_pull_request_event_reuses_existing_payload(self) -> None:
event,
)

def test_pull_request_target_event_reuses_existing_payload(self) -> None:
with tempfile.TemporaryDirectory() as directory:
event_path = Path(directory) / "event.json"
event = {"pull_request": pr_payload(number=12)}
event_path.write_text(json.dumps(event), encoding="utf-8")

self.assertEqual(
resolver.resolve_event("owner/repo", "pull_request_target", event_path, ""),
event,
)

def test_workflow_dispatch_fetches_pr_payload(self) -> None:
fetched = pr_payload(number=12)
with mock.patch.object(resolver, "fetch_pr", return_value=fetched) as fetch_pr:
Expand Down Expand Up @@ -131,6 +142,36 @@ def test_review_state_marks_same_repo_non_draft_pr_reviewable(self) -> None:
},
)

def test_review_state_marks_pull_request_target_non_draft_pr_reviewable(self) -> None:
state = resolver.review_state(
{"pull_request": pr_payload(number=14)},
"owner/repo",
"pull_request_target",
)

self.assertEqual(state["reviewable"], "true")
self.assertEqual(state["skip_reason"], "")

def test_review_state_skips_pull_request_target_fork_pr(self) -> None:
state = resolver.review_state(
{"pull_request": pr_payload(number=15, head_repo="fork/repo")},
"owner/repo",
"pull_request_target",
)

self.assertEqual(state["reviewable"], "false")
self.assertEqual(state["skip_reason"], "head repository is not the current repository")

def test_review_state_skips_pull_request_target_draft_pr(self) -> None:
state = resolver.review_state(
{"pull_request": pr_payload(number=16, draft=True)},
"owner/repo",
"pull_request_target",
)

self.assertEqual(state["reviewable"], "false")
self.assertEqual(state["skip_reason"], "draft")

def test_review_state_skips_draft_and_closed_prs(self) -> None:
self.assertEqual(
resolver.review_state({"pull_request": pr_payload(draft=True)}, "owner/repo")["reviewable"],
Expand All @@ -141,12 +182,21 @@ def test_review_state_skips_draft_and_closed_prs(self) -> None:
"false",
)

def test_review_state_allows_open_non_draft_fork_prs(self) -> None:
self.assertEqual(
resolver.review_state({"pull_request": pr_payload(head_repo="fork/repo")}, "owner/repo")["reviewable"],
"true",
def test_review_state_allows_open_non_draft_fork_prs_for_manual_entry(self) -> None:
state = resolver.review_state({"pull_request": pr_payload(head_repo="fork/repo")}, "owner/repo")

self.assertEqual(state["reviewable"], "true")
self.assertEqual(state["skip_reason"], "")

def test_review_state_allows_manual_workflow_dispatch_for_fork_pr(self) -> None:
state = resolver.review_state(
{"pull_request": pr_payload(head_repo="fork/repo")},
"owner/repo",
"workflow_dispatch",
)

self.assertEqual(state["reviewable"], "true")

def test_review_state_allows_valid_comment_review_for_open_non_draft_pr(self) -> None:
self.assertEqual(
resolver.review_state(
Expand All @@ -157,6 +207,16 @@ def test_review_state_allows_valid_comment_review_for_open_non_draft_pr(self) ->
"true",
)

def test_review_state_skips_issue_comment_review_for_fork_pr(self) -> None:
state = resolver.review_state(
{"pull_request": pr_payload(head_repo="fork/repo"), "review_command": True},
"owner/repo",
"issue_comment",
)

self.assertEqual(state["reviewable"], "false")
self.assertEqual(state["skip_reason"], "head repository is not the current repository")

def test_review_state_skips_issue_comments_without_valid_review_command(self) -> None:
state = resolver.review_state(
{"pull_request": pr_payload(draft=False), "review_command": False},
Expand Down
11 changes: 7 additions & 4 deletions .github/aicodingflow-tests/test_review_workflow_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,22 @@ def test_workflows_use_node24_action_runtime(self) -> None:
if action.startswith("actions/upload-artifact@"):
self.assertEqual(action, "actions/upload-artifact@v7")

def test_review_workflow_uses_manual_and_comment_triggers_only(self) -> None:
def test_review_workflow_accepts_pull_request_target_and_manual_comment_triggers(self) -> None:
data = workflow(".github/workflows/review-pr.yml")
triggers = data[True]

self.assertNotIn("pull_request", triggers)
self.assertNotIn("pull_request_target", triggers)
self.assertEqual(
triggers["pull_request_target"]["types"],
["opened", "reopened", "synchronize", "ready_for_review"],
)
self.assertEqual(data["permissions"]["statuses"], "write")
self.assertEqual(triggers["issue_comment"]["types"], ["created"])
self.assertTrue(triggers["workflow_dispatch"]["inputs"]["pr_number"]["required"])
job_gate = data["jobs"]["preflight"]["if"]
self.assertIn("github.event_name == 'pull_request_target'", job_gate)
self.assertIn("github.event.pull_request.head.repo.full_name == github.repository", job_gate)
self.assertIn("github.event_name == 'workflow_dispatch'", job_gate)
self.assertNotIn("github.event_name == 'pull_request'", job_gate)
self.assertNotIn("github.event.pull_request.head.repo.full_name == github.repository", job_gate)
self.assertIn("github.event_name == 'issue_comment'", job_gate)
self.assertIn("github.event.issue.pull_request != null", job_gate)
self.assertIn("contains(github.event.comment.body, format('@{0}', vars.AGENT_LOGIN))", job_gate)
Expand Down
11 changes: 7 additions & 4 deletions .github/scripts/resolve_pr_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,10 @@ def resolve_event(
pr_number: str,
agent_login: str = "",
) -> dict[str, Any]:
if event_name == "pull_request":
if event_name in {"pull_request", "pull_request_target"}:
Comment thread
Terry-Mao marked this conversation as resolved.
event = load_json(event_path)
if "pull_request" not in event:
raise SystemExit("pull_request event payload is missing pull_request")
raise SystemExit(f"{event_name} event payload is missing pull_request")
return event

if event_name == "issue_comment":
Expand Down Expand Up @@ -111,18 +111,21 @@ def review_state(event: dict[str, Any], repo: str, event_name: str = "") -> dict
draft = bool(pr.get("draft"))
state = str(pr.get("state") or "").lower()
is_open = state == "open"
is_same_repo_entry = event_name not in {"pull_request_target", "issue_comment"} or head_repo == repo
is_comment_review = event_name == "issue_comment" and event.get("review_command") is True
if event_name == "workflow_dispatch":
reviewable = is_open
elif event_name == "issue_comment":
reviewable = is_open and not draft and is_comment_review
reviewable = is_open and not draft and is_comment_review and is_same_repo_entry
else:
reviewable = is_open and not draft
reviewable = is_open and not draft and is_same_repo_entry

if not is_open:
skip_reason = "closed"
elif draft and event_name != "workflow_dispatch":
skip_reason = "draft"
elif event_name in {"pull_request_target", "issue_comment"} and not is_same_repo_entry:
skip_reason = "head repository is not the current repository"
elif event_name == "issue_comment" and not is_comment_review:
skip_reason = "missing valid @AGENT_LOGIN /review command"
else:
Expand Down
20 changes: 13 additions & 7 deletions .github/workflows/review-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ on:
required: true
issue_comment:
types: [created]
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
Comment thread
Terry-Mao marked this conversation as resolved.

permissions:
contents: read
Expand All @@ -21,14 +23,18 @@ concurrency:
jobs:
preflight:
if: >-
github.event_name == 'workflow_dispatch' ||
(
github.event_name == 'issue_comment' &&
github.event.issue.pull_request != null &&
vars.AGENT_LOGIN != '' &&
contains(github.event.comment.body, format('@{0}', vars.AGENT_LOGIN)) &&
contains(github.event.comment.body, '/review')
)
github.event_name == 'pull_request_target' ||
github.event_name == 'workflow_dispatch' ||
(
github.event_name == 'issue_comment' &&
github.event.issue.pull_request != null &&
vars.AGENT_LOGIN != '' &&
contains(github.event.comment.body, format('@{0}', vars.AGENT_LOGIN)) &&
contains(github.event.comment.body, '/review')
)
) &&
(github.event_name != 'pull_request_target' || github.event.pull_request.head.repo.full_name == github.repository)
Comment thread
Terry-Mao marked this conversation as resolved.
runs-on: ubuntu-latest
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
Expand Down
13 changes: 6 additions & 7 deletions docs/product/raw/pr-review-verdict.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ AI PR Review 默认由目标项目的 CI 成功路径通过 `workflow_dispatch`
时,应在自己的 CI 成功路径中 dispatch `review-pr.yml`,而不是修改已安装的
`review-pr.yml`。

AI PR Review 也可以手动通过 `workflow_dispatch` 输入 PR number 触发。当 open 且非
draft 的 PR conversation comment 中包含发给 Actions variable `AGENT_LOGIN` 指定账号的
AI PR Review 也会由 `pull_request_target` 监听同仓库 PR 的 opened、reopened、synchronize
和 ready_for_review 事件触发;该自动入口只接受同仓库 head,fork PR 会被 workflow gate 和 resolver 两层跳过。维护者仍可通过 `workflow_dispatch` 显式评审 fork PR。当 open 且非
draft、同仓库 head 的 PR conversation comment 中包含发给 Actions variable `AGENT_LOGIN` 指定账号的
body-level command `@AGENT_LOGIN /review` 时,也可以从 `issue_comment` 事件触发。
手动触发和 comment 触发会先解析目标 PR,再复用 review 流程。`review-pr.yml` 不直接监听
GitHub `pull_request` 事件。
手动触发、comment 触发和 `pull_request_target` 触发会先解析目标 PR,再复用 review 流程。
`review-pr.yml` 不监听 GitHub `pull_request` 事件。

`workflow_dispatch` 触发的 review workflow 允许 `github-actions[bot]` 运行 Codex
review action,因此使用 GitHub Actions 自带 `GITHUB_TOKEN` 的 CI dispatch 可以运行 AI
Expand All @@ -29,9 +30,7 @@ mention、quoted line、fenced code block、普通句子中的提及,以及带
`@AGENT_LOGIN /review ...` 都不会触发 AI review。普通 issue comment 和 PR inline
review comment 也不是该入口。

只有 open、非 draft 且 head repository 与当前仓库一致的 PR 会继续进入 AI review。
closed PR、draft PR 或来自 fork 的 PR 会被跳过,不会运行 agent、发布 review 或上传
review artifact。
自动 `pull_request_target` 入口只有 open、非 draft 且 head repository 与当前仓库一致的 PR 会继续进入 AI review。有效 comment 入口仅可评审同仓库 head PR;维护者可通过 `workflow_dispatch` 显式评审 fork 或 draft PR。closed PR 会被跳过,draft PR 只在自动和 comment 入口被跳过。

GitHub workflow 运行 AI PR Review 时,`pr_diff.txt` 来自 GitHub PR diff API,而不是
checkout 后对 base/head SHA 执行本地 `git diff`。workflow 会在读取 diff 前后校验 PR
Expand Down
10 changes: 5 additions & 5 deletions docs/product/wiki/concepts/ai-pr-review-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ AI PR Review 负责在 PR 满足可评审条件时运行机器评审,并根据

## 触发入口

- AI PR Review 默认由目标项目 CI 成功路径通过 `workflow_dispatch` 输入 PR number 触发;AICodingFlow 参考 `CI` workflow 在非 draft、同仓库 head PR 的测试和编译检查成功后 dispatch `review-pr.yml`。
- AI PR Review 可由 `pull_request_target` 在同仓库 PR 的 opened、reopened、synchronize、ready_for_review 事件触发,也可由目标项目 CI 成功路径通过 `workflow_dispatch` 输入 PR number 触发。
- 目标项目已有自己的 CI 时,应在自己的 CI 成功路径中 dispatch `review-pr.yml`,而不是修改已安装的 `review-pr.yml`。
- `workflow_dispatch` 可以通过 PR number 手动触发 review。
- PR conversation comment mention Actions variable `AGENT_LOGIN` 指定账号时,可以从 `issue_comment` 事件触发。
- `review-pr.yml` 不直接监听 GitHub `pull_request` 事件。
- 同仓库 head、open 且非 draft PR conversation comment mention Actions variable `AGENT_LOGIN` 指定账号时,可以从 `issue_comment` 事件触发。
- `review-pr.yml` 不监听 GitHub `pull_request` 事件;自动 `pull_request_target` 入口会被触发器门控和 resolver 双重限制为同仓库 head,维护者仍可通过 `workflow_dispatch` 显式评审 fork PR。
- 手动触发和 comment 触发会先解析目标 PR,再复用 review 流程。
- `workflow_dispatch` 触发允许 `github-actions[bot]` 运行 Codex review action;使用 GitHub App token、PAT 或第三方 CI dispatch 时,bot allowlist 需要匹配实际触发 actor,或改用有人类账号权限的 token 触发。
- Comment 触发要求 `@AGENT_LOGIN /review` 独占一行,允许前后空白。
Expand All @@ -30,8 +30,8 @@ AI PR Review 负责在 PR 满足可评审条件时运行机器评审,并根据

## Reviewable 条件

- 只有 open、非 draft 且 head repository 与当前仓库一致的 PR 会继续进入 AI review。
- closed PR、draft PR 或来自 fork 的 PR 会被跳过。
- 自动 `pull_request_target` 入口只有 open、非 draft 且 head repository 与当前仓库一致的 PR 会继续进入 AI review;`workflow_dispatch` 可显式评审 fork 或 draft PR,comment 入口仅接受同仓库 head PR。
- closed PR 会被跳过;draft PR 只在自动 `pull_request_target` 和 comment 入口被跳过。
- 被跳过的 PR 不会运行 agent、发布 review 或上传 review artifact。

## Skill 选择
Expand Down
10 changes: 5 additions & 5 deletions docs/product/wiki/summaries/pr-review-verdict.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,17 @@ Source: [docs/product/raw/pr-review-verdict.md](../../raw/pr-review-verdict.md)

## 触发与 skill 选择

- AI PR Review 默认由目标项目 CI 成功路径通过 `workflow_dispatch` 输入 PR number 触发;AICodingFlow 参考 `CI` workflow 在非 draft、同仓库 head PR 的测试和编译检查成功后 dispatch `review-pr.yml`。
- AI PR Review 可由 `pull_request_target` 在同仓库 PR 的 opened、reopened、synchronize、ready_for_review 事件触发,也可由目标项目 CI 成功路径通过 `workflow_dispatch` 输入 PR number 触发。
- 目标项目已有自己的 CI 时,应在自己的 CI 成功路径中 dispatch `review-pr.yml`,而不是修改已安装的 `review-pr.yml`。
- AI PR Review 也可以手动通过 `workflow_dispatch` 输入 PR number 触发,或由 open 且非 draft PR conversation comment 中的 `@AGENT_LOGIN /review` body-level command 从 `issue_comment` 事件触发。
- `review-pr.yml` 不直接监听 GitHub `pull_request` 事件。
- AI PR Review 也可以手动通过 `workflow_dispatch` 输入 PR number 触发,或由同仓库 head、open 且非 draft PR conversation comment 中的 `@AGENT_LOGIN /review` body-level command 从 `issue_comment` 事件触发。
- `review-pr.yml` 不监听 GitHub `pull_request` 事件;自动 `pull_request_target` 入口会被触发器门控和 resolver 双重限制为同仓库 head,维护者仍可通过 `workflow_dispatch` 显式评审 fork PR。
- `workflow_dispatch` 触发允许 `github-actions[bot]` 运行 Codex review action;如果目标仓库改用 GitHub App token、PAT 或第三方 CI dispatch,需要让 bot allowlist 与实际触发 actor 匹配,或改用有人类账号权限的 token 触发。
- 手动触发和 comment 触发会先解析目标 PR,再复用 review 流程。
- Comment 触发要求 `@AGENT_LOGIN /review` 独占一行,允许前后空白。
- 裸 `/review`、单纯 `@AGENT_LOGIN` mention、quoted line、fenced code block、普通句子中的提及,以及带额外参数的 `@AGENT_LOGIN /review ...` 都不会触发 AI review。
- 普通 issue comment 和 PR inline review comment 不是 comment 触发入口。
- 只有 open、非 draft 且 head repository 与当前仓库一致的 PR 会继续进入 AI review。
- closed PR、draft PR 或来自 fork 的 PR 会被跳过,不会运行 agent、发布 review 或上传 review artifact。
- 自动 `pull_request_target` 入口只有 open、非 draft 且 head repository 与当前仓库一致的 PR 会继续进入 AI review;`workflow_dispatch` 可显式评审 fork PR;comment 入口仅接受同仓库 head PR。
- closed PR 会被跳过;draft PR 只在自动 `pull_request_target` 和 comment 入口被跳过,`workflow_dispatch` 仍可显式评审 draft PR。
- spec-only PR 使用 `review-spec-repo`;其他 code PR 使用 `review-pr-repo`。
- 仓库本地 wrapper skill 补充本仓库评审偏好,不改变核心输出契约。

Expand Down
Loading