From d0404d9a4f9aed3568da02e0b5538ada64cff72c Mon Sep 17 00:00:00 2001 From: weishu Date: Sun, 8 Mar 2026 11:13:56 +0800 Subject: [PATCH] Add support for PR live review --- .github/prompts/codex-pr-review.md | 90 +++++++++++++++++++-------- .github/workflows/codex-pr-review.yml | 53 ++++++++++++---- 2 files changed, 106 insertions(+), 37 deletions(-) diff --git a/.github/prompts/codex-pr-review.md b/.github/prompts/codex-pr-review.md index cda8f931..f9ceeeef 100644 --- a/.github/prompts/codex-pr-review.md +++ b/.github/prompts/codex-pr-review.md @@ -1,6 +1,6 @@ # HAPI PR Review Assistant -Review newly opened pull requests for the HAPI project and provide a concise, high-signal review comment. +Review opened or updated pull requests for the HAPI project and provide a concise, high-signal review comment. ## Security @@ -23,31 +23,56 @@ Repo rules: TypeScript strict; Bun workspaces (run `bun` from repo root); path a ## PR Context (required) -Before any analysis, load PR metadata and diff from the GitHub Actions event payload. +Before any analysis, load PR metadata, latest head SHA, and diff from the GitHub Actions event payload. + +Workflow-provided env: +- `CURRENT_HEAD_SHA` - PR head SHA for this run +- `LATEST_BOT_REVIEW_ID` - most recent prior HAPI Bot review id, if any +- `LATEST_BOT_REVIEW_COMMIT` - commit SHA reviewed by that prior HAPI Bot review, if any +- `IS_FOLLOW_UP_REVIEW` - `true` when contributor pushed new commits after the last HAPI Bot review ```bash pr_number=$(jq -r '.pull_request.number' "$GITHUB_EVENT_PATH") repo=$(jq -r '.repository.full_name' "$GITHUB_EVENT_PATH") -gh pr view "$pr_number" -R "$repo" --json number,title,body,labels,author,additions,deletions,changedFiles,files +current_head_sha="${CURRENT_HEAD_SHA:-$(jq -r '.pull_request.head.sha' "$GITHUB_EVENT_PATH")}" +latest_bot_review_id="${LATEST_BOT_REVIEW_ID:-}" +latest_bot_review_commit="${LATEST_BOT_REVIEW_COMMIT:-}" +is_follow_up_review="${IS_FOLLOW_UP_REVIEW:-false}" + +gh pr view "$pr_number" -R "$repo" --json number,title,body,labels,author,additions,deletions,changedFiles,files,headRefOid gh pr diff "$pr_number" -R "$repo" + +if [ "$is_follow_up_review" = "true" ] && [ -n "$latest_bot_review_id" ]; then + gh api "repos/$repo/pulls/$pr_number/reviews/$latest_bot_review_id" + gh api "repos/$repo/pulls/$pr_number/reviews/$latest_bot_review_id/comments" + + if [ -n "$latest_bot_review_commit" ] && [ "$latest_bot_review_commit" != "$current_head_sha" ]; then + gh api -H "Accept: application/vnd.github.v3.diff" \ + "repos/$repo/compare/$latest_bot_review_commit...$current_head_sha" + fi +fi ``` ## Task 1. **Load context (progressive)**: `README.md`, `AGENTS.md`, then only needed package README/source files. -2. **Review the PR diff**: correctness, security, regressions, data loss, performance, and maintainability. -3. **Check tests**: note missing or inadequate coverage. -4. **Respond** with an evidence-based review comment (no code changes). +2. **Determine review mode**: `initial` when no prior HAPI Bot review exists for another commit, otherwise `follow-up after new commits`. +3. **Review the latest PR diff in full**: correctness, security, regressions, data loss, performance, and maintainability. +4. **Follow-up context**: when `IS_FOLLOW_UP_REVIEW=true`, use the previous HAPI Bot review and compare diff only as context for what changed since the last bot pass. Do not limit the review to those changes. +5. **Check tests**: note missing or inadequate coverage. +6. **Respond** with an evidence-based review comment (no code changes). ## Response Guidelines - **Findings first**: order by severity (Blocker/Major/Minor/Nit). +- **Mode line**: summary must start with `Review mode: initial` or `Review mode: follow-up after new commits`. - **Evidence**: cite specific files and line numbers using `path:line`. - **No speculation**: if uncertain, say so; if not found, say “Not found in repo/docs”. - **Missing info**: ask only when required; max 4 questions. - **Language**: match the PR’s language (Chinese or English); if mixed, use the dominant language. - **Signature**: end with `*HAPI Bot*`. - **Diff focus**: only comment on added/modified lines; use unchanged code only for context. +- **Fresh-head only**: before posting, re-fetch live PR head SHA; if it differs from `CURRENT_HEAD_SHA`, stop without posting a stale review. - **Attribution**: report only issues introduced or directly triggered by the diff; anchor comments to diff lines, citing related context if needed. - **High signal**: if confidence < 80%, do not report; ask a question if needed. - **No praise**: report issues and risks only. @@ -68,6 +93,7 @@ gh pr diff "$pr_number" -R "$repo" - ... **Summary** +- Must begin with the review mode line - If no issues: explicitly say so and mention residual risks/testing gaps **Testing** @@ -75,30 +101,44 @@ gh pr diff "$pr_number" -R "$repo" ## Post Response to Github -### Inline Comments - -For each validated issue, create an inline comment: +Submit exactly one review for this run. Use a single atomic `create review` API call so summary and inline comments stay attached to the same `CURRENT_HEAD_SHA`. ```bash -gh api repos/{owner}/{repo}/pulls/{pr_number}/comments \ - -f body="**[SEVERITY]** [ISSUE-TYPE] Brief description - -**Why this is a problem**: Detailed explanation. - -**Suggested fix**: -\`\`\`{language} -// Corrected code here -\`\`\`" \ - -f commit_id="{LATEST_COMMIT_SHA}" \ - -f path="{FILE_PATH}" \ - -f line={LINE_NUMBER} \ - -f side="RIGHT" +live_head_sha=$(gh pr view "$pr_number" -R "$repo" --json headRefOid -q .headRefOid) +if [ "$live_head_sha" != "$current_head_sha" ]; then + echo "PR head moved from $current_head_sha to $live_head_sha; skip stale review." + exit 0 +fi ``` -### Summary Report (MANDATORY) +- If there are findings, build one review payload with: + - `event: "COMMENT"` + - `commit_id: "$current_head_sha"` + - `body: "{SUMMARY}"` + - `comments: [...]` containing every inline finding comment +- If there are no findings, submit a summary-only review with the same `event`, `commit_id`, and `body`. +- Prefer writing the JSON payload to a temporary file and posting it with `gh api --input`. -Submit a comprehensive review summary: +Example shape: + +```json +{ + "event": "COMMENT", + "commit_id": "CURRENT_HEAD_SHA", + "body": "FULL_SUMMARY", + "comments": [ + { + "path": "path/to/file.ts", + "line": 123, + "side": "RIGHT", + "body": "**[MAJOR]** ..." + } + ] +} +``` ```bash -gh pr review --comment --body "{SUMMARY}" +gh api "repos/$repo/pulls/$pr_number/reviews" \ + --method POST \ + --input /tmp/hapi-pr-review.json ``` diff --git a/.github/workflows/codex-pr-review.yml b/.github/workflows/codex-pr-review.yml index aa91e61b..90be87f8 100644 --- a/.github/workflows/codex-pr-review.yml +++ b/.github/workflows/codex-pr-review.yml @@ -2,11 +2,11 @@ name: Codex PR Review on: pull_request_target: - types: [opened, ready_for_review] + types: [opened, reopened, ready_for_review, synchronize] concurrency: group: codex-pr-review-${{ github.event.pull_request.number }} - cancel-in-progress: false + cancel-in-progress: true jobs: pr-review: @@ -22,7 +22,7 @@ jobs: review_result: ${{ steps.run_codex.outputs.final-message }} steps: - - name: Check for existing HAPI Bot review + - name: Check HAPI Bot review state id: check_bot uses: actions/github-script@v7 with: @@ -32,6 +32,7 @@ jobs: .split(",") .map((value) => value.trim()) .filter(Boolean); + const currentHeadSha = context.payload.pull_request.head.sha; const reviews = await github.paginate( github.rest.pulls.listReviews, { @@ -41,8 +42,8 @@ jobs: per_page: 100 } ); - const hasBot = reviews.some( - (review) => { + const botReviews = reviews + .filter((review) => { if (!(review?.body || "").includes(marker)) { return false; } @@ -51,24 +52,48 @@ jobs: return false; } return allowedLogins.includes(user.login); - } + }) + .sort((left, right) => { + const leftTime = new Date(left.submitted_at || left.created_at || 0).getTime(); + const rightTime = new Date(right.submitted_at || right.created_at || 0).getTime(); + if (rightTime !== leftTime) { + return rightTime - leftTime; + } + return (right.id || 0) - (left.id || 0); + }); + const latestBotReview = botReviews[0]; + const hasReviewForCurrentHead = botReviews.some( + (review) => review.commit_id === currentHeadSha ); - core.setOutput("has_bot", hasBot ? "true" : "false"); - if (hasBot) { - core.info("Existing HAPI Bot review found; skipping."); + const isFollowUpReview = Boolean( + latestBotReview?.commit_id && latestBotReview.commit_id !== currentHeadSha + ); + + core.setOutput("current_head_sha", currentHeadSha); + core.setOutput("has_review_for_current_head", hasReviewForCurrentHead ? "true" : "false"); + core.setOutput("latest_bot_review_id", latestBotReview ? String(latestBotReview.id) : ""); + core.setOutput("latest_bot_review_commit", latestBotReview?.commit_id || ""); + core.setOutput("is_follow_up_review", isFollowUpReview ? "true" : "false"); + + if (hasReviewForCurrentHead) { + core.info(`Existing HAPI Bot review found for ${currentHeadSha}; skipping.`); + } else if (isFollowUpReview) { + core.info( + `Latest HAPI Bot review was for ${latestBotReview.commit_id}; running follow-up review for ${currentHeadSha}.` + ); } env: HAPI_BOT_LOGINS: ${{ vars.HAPI_BOT_LOGINS }} - name: Checkout repository - if: steps.check_bot.outputs.has_bot != 'true' + if: steps.check_bot.outputs.has_review_for_current_head != 'true' uses: actions/checkout@v4 with: ref: refs/pull/${{ github.event.pull_request.number }}/merge fetch-depth: 0 - name: Pre-fetch base and head refs - if: steps.check_bot.outputs.has_bot != 'true' + if: steps.check_bot.outputs.has_review_for_current_head != 'true' run: | git fetch --no-tags origin \ ${{ github.event.pull_request.base.ref }} \ @@ -76,11 +101,15 @@ jobs: - name: Run Codex for PR Review id: run_codex - if: steps.check_bot.outputs.has_bot != 'true' + if: steps.check_bot.outputs.has_review_for_current_head != 'true' uses: openai/codex-action@v1 env: GH_TOKEN: ${{ github.token }} GITHUB_TOKEN: ${{ github.token }} + CURRENT_HEAD_SHA: ${{ steps.check_bot.outputs.current_head_sha }} + LATEST_BOT_REVIEW_ID: ${{ steps.check_bot.outputs.latest_bot_review_id }} + LATEST_BOT_REVIEW_COMMIT: ${{ steps.check_bot.outputs.latest_bot_review_commit }} + IS_FOLLOW_UP_REVIEW: ${{ steps.check_bot.outputs.is_follow_up_review }} with: openai-api-key: ${{ secrets.OPENAI_API_KEY }} responses-api-endpoint: ${{ secrets.OPENAI_BASE_URL }}