This is an automated email from the ASF dual-hosted git repository.

hello-stephen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 81eeced65c2 [fix](build) Serialize automated reviews for each PR 
(#68699)
81eeced65c2 is described below

commit 81eeced65c2dc6be534d53815cc1006b9498a22a
Author: shuke <[email protected]>
AuthorDate: Tue Oct 13 19:23:50 2026 +0800

    [fix](build) Serialize automated reviews for each PR (#68699)
    
    Related PR: #68646, #68579
    
    Problem Summary:
    
    Repeated `/review` comments can start overlapping reviews of the same
    PR. When one job posts first, the existing duplicate-submission guard
    rejects the other job's final candidate, even if it contains different
    findings. The outer completion check then replaces that rejection with
    "No final review submission was declared".
    
    Serialize the `code-review` job by PR number across comment and dispatch
    triggers. Use `cancel-in-progress: false` and `queue: max` so running
    and pending review requests are retained. Other PRs and the runtime
    canary remain independent. The PR head and review context are resolved
    after the queued job starts.
    
    If the duplicate guard still rejects a candidate, save its reason and
    complete proposed submission separately from the POST intent, propagate
    that reason to the runner, and upload only that JSON as a 14-day
    artifact. The candidate remains unsubmitted and cannot prove completion.
    
    This is scoped to the workflow, submission helper and regression tests.
    Existing duplicate protection, exclusive POST intent, invocation marker,
    full GitHub readback and P0/P1 verdict logic are preserved.
---
 .github/scripts/submit_review.py         | 10 +++-
 .github/scripts/test_submit_review.py    | 85 ++++++++++++++++++++++++++++++++
 .github/workflows/code-review-runner.yml | 23 ++++++++-
 3 files changed, 115 insertions(+), 3 deletions(-)

diff --git a/.github/scripts/submit_review.py b/.github/scripts/submit_review.py
index 6db8997b3b9..358c05a95d4 100644
--- a/.github/scripts/submit_review.py
+++ b/.github/scripts/submit_review.py
@@ -29,6 +29,7 @@ from pathlib import Path
 
 
 SUBMISSION_FILE = "review-submission.json"
+REJECTION_FILE = "review-submission-rejected.json"
 RUN_FILE = "review-run.json"
 RESULT_FILE = "review-result.json"
 PRIORITY = re.compile(r"^\[P([0-3])\]\s+\S")
@@ -163,6 +164,9 @@ def verify_completion(context, run, remaining, *, 
submit=False, submission=None)
     path = context / SUBMISSION_FILE
     if submission is None:
         if not path.exists():
+            rejection = context / REJECTION_FILE
+            if rejection.exists():
+                raise ValueError(json.loads(rejection.read_text())["reason"])
             raise ValueError("No final review submission was declared")
         submission = json.loads(path.read_text())
     submission = validate_submission(submission)
@@ -176,7 +180,11 @@ def verify_completion(context, run, remaining, *, 
submit=False, submission=None)
         if any(r.get("user", {}).get("login") == "github-actions[bot]"
                and r.get("commit_id") == run["head_sha"]
                and (r.get("submitted_at") or "") >= run["started_at"] for r in 
reviews):
-            raise ValueError("Another bot review was submitted during this 
run; refusing duplicate submission")
+            reason = "Another bot review was submitted during this run; 
refusing duplicate submission"
+            # This is a rejected candidate, not a POST intent or proof of 
delivery.
+            # Keep it separate so completion still requires exact GitHub 
readback.
+            write_json(context / REJECTION_FILE, {"reason": reason, 
"submission": submission})
+            raise ValueError(reason)
         if path.exists():
             raise ValueError("Final submission was attempted but cannot be 
verified; refusing to repost")
         # Persist the complete intent before POST: if the response is lost, the
diff --git a/.github/scripts/test_submit_review.py 
b/.github/scripts/test_submit_review.py
index 50dbfed228d..546bfecede6 100644
--- a/.github/scripts/test_submit_review.py
+++ b/.github/scripts/test_submit_review.py
@@ -20,6 +20,7 @@ import copy
 import io
 import itertools
 import json
+import re
 import subprocess
 import tempfile
 import threading
@@ -212,6 +213,70 @@ class FinalReviewTest(unittest.TestCase):
             self.submit()
         self.assertEqual(1, len(self.posts))
 
+    def test_rejected_submission_preserves_distinct_findings_and_reason(self):
+        self.reviews = [{"id": 98, "user": {"login": "github-actions[bot]"},
+                         "commit_id": self.run["head_sha"],
+                         "submitted_at": self.run["started_at"], "body": "A 
different finding"}]
+        for level in (1, 2):
+            with self.subTest(priority=level):
+                submission = self.submission(level)
+                with self.assertRaisesRegex(ValueError, "Another bot review") 
as error:
+                    self.submit(submission)
+                rejected = json.loads((self.context / 
submitter.REJECTION_FILE).read_text())
+                self.assertEqual(submission, rejected["submission"])
+                self.assertEqual(str(error.exception), rejected["reason"])
+                with self.assertRaisesRegex(ValueError, "Another bot review"):
+                    self.verify()
+        self.assertEqual([], self.posts)
+        self.assertFalse((self.context / submitter.SUBMISSION_FILE).exists())
+
+    def test_review_before_start_does_not_block_next_run(self):
+        self.reviews = [{"id": 98, "user": {"login": "github-actions[bot]"},
+                         "commit_id": self.run["head_sha"],
+                         "submitted_at": "2026-09-28T23:59:59Z", "body": 
"Earlier review"}]
+        self.assertEqual("failure", self.submit()["state"])
+        self.assertEqual(1, len(self.posts))
+        self.assertFalse((self.context / submitter.REJECTION_FILE).exists())
+
+    def test_verified_submission_takes_precedence_over_rejection(self):
+        submitter.write_json(self.context / submitter.REJECTION_FILE, {
+            "reason": "Earlier rejected candidate", "submission": 
self.submission(2),
+        })
+        result = self.submit()
+        self.assertEqual(result, self.verify())
+        self.assertEqual(1, len(self.posts))
+
+    def test_runner_reports_rejection_instead_of_missing_submission(self):
+        (self.context / "codex_goal_prompt.txt").write_text("Review the PR")
+        args = SimpleNamespace(context_dir=self.context, cwd=self.context, **{
+            k: self.run[k] for k in ("repository", "pr_number", "head_sha", 
"base_sha")
+        }, model="gpt-6-sol", effort="xhigh", budget_seconds=60)
+
+        def attempt(command, events, stderr, timeout, reaper=None):
+            run = json.loads((self.context / submitter.RUN_FILE).read_text())
+            self.reviews = [{"user": {"login": "github-actions[bot]"},
+                             "commit_id": run["head_sha"], "submitted_at": 
run["started_at"]}]
+            with self.assertRaisesRegex(ValueError, "Another bot review"):
+                submitter.verify_completion(self.context, run, self.remaining, 
submit=True,
+                                            submission=self.submission())
+            # The model can finish normally after the helper rejects its 
candidate.
+            events.write_text(json.dumps({"type": "thread.started", 
"thread_id":
+                "0199a213-81c0-7800-8aa1-bbab2a035a53"}) + "\n" +
+                json.dumps({"type": "turn.completed"}) + "\n")
+            stderr.write_text("")
+            return 0
+
+        with mock.patch.object(runner, "run_attempt", side_effect=attempt), \
+             mock.patch.object(runner, "check_resume_target") as guard, \
+             redirect_stderr(io.StringIO()):
+            self.assertEqual(1, runner.run_review(args))
+        guard.assert_not_called()
+        error = runner.read_events(self.context / 
"codex-events.jsonl")[-1]["error"]["message"]
+        self.assertIn("Another bot review", error)
+        self.assertNotIn("No final review submission", error)
+        self.assertEqual([], self.posts)
+        self.assertFalse((self.context / submitter.RESULT_FILE).exists())
+
     def test_simultaneous_helper_invocations_post_only_once(self):
         barrier = threading.Barrier(2, timeout=5)
         original_open = Path.open
@@ -376,6 +441,26 @@ class FinalReviewTest(unittest.TestCase):
         self.assertEqual([], self.posts)
 
 
+class ReviewWorkflowTest(unittest.TestCase):
+    def test_queue_is_per_pr_and_rejected_artifact_is_explicit(self):
+        workflow = (Path(__file__).resolve().parents[1] / 
"workflows/code-review-runner.yml").read_text()
+        review_job = workflow.split("  code-review:\n", 1)[1].split("  
aggregate-status:", 1)[0]
+        concurrency = re.search(r"^    concurrency:\n((?:      .*\n)+)", 
review_job, re.MULTILINE)
+        self.assertIsNotNone(concurrency)
+        self.assertIn("github.event.issue.number || inputs.pr_number || 
github.run_id", concurrency[1])
+        self.assertIn("cancel-in-progress: false", concurrency[1])
+        self.assertIn("queue: max", concurrency[1])
+        self.assertNotIn("sha", concurrency[1])
+        self.assertNotRegex(workflow, r"(?m)^concurrency:")
+        artifact = review_job.split("      - name: Preserve rejected final 
review\n", 1)[1]
+        artifact = artifact.split("      - name:", 1)[0]
+        self.assertIn("always()", artifact)
+        self.assertIn("steps.review.outcome == 'failure'", artifact)
+        self.assertIn("path: ${{ env.REVIEW_CONTEXT_DIR }}/" + 
submitter.REJECTION_FILE, artifact)
+        self.assertIn("include-hidden-files: true", artifact)
+        self.assertIn("if-no-files-found: ignore", artifact)
+
+
 class GitHubAPITest(unittest.TestCase):
     def test_read_all_pages_and_bound_timeout(self):
         with mock.patch.object(submitter.subprocess, "run", 
return_value=SimpleNamespace(
diff --git a/.github/workflows/code-review-runner.yml 
b/.github/workflows/code-review-runner.yml
index fc227900abc..e5626ce624b 100644
--- a/.github/workflows/code-review-runner.yml
+++ b/.github/workflows/code-review-runner.yml
@@ -201,14 +201,20 @@ jobs:
 
   code-review:
     runs-on: ubuntu-24.04
+    # Serialize actual reviews of a PR, including comment and dispatch 
triggers.
+    # Keep every pending request rather than replacing the previous queued job.
+    concurrency:
+      group: code-review-pr-${{ github.event.issue.number || inputs.pr_number 
|| github.run_id }}
+      cancel-in-progress: false
+      queue: max
     outputs:
       base_sha: ${{ steps.review_inputs.outputs.base_sha }}
       head_sha: ${{ steps.review_inputs.outputs.head_sha }}
       manage_status: ${{ steps.review_inputs.outputs.manage_status }}
       review_state: ${{ steps.review.outputs.review_state }}
       pr_number: ${{ steps.review_inputs.outputs.pr_number }}
-    # Pre-finalization steps can use 218 minutes and auth sync can use 8 more,
-    # leaving 12 minutes for runner setup and post-job cleanup.
+    # Pre-finalization steps can use 220 minutes and auth sync can use 8 more,
+    # leaving 10 minutes for runner setup and post-job cleanup.
     timeout-minutes: 238
     if: >-
       inputs.runtime_canary != true &&
@@ -973,6 +979,19 @@ jobs:
             
"review_state=\(.state)\np0=\(.p0)\np1=\(.p1)\nconverged=\(.converged)\nrounds=\(.rounds)"'
 \
             "$REVIEW_CONTEXT_DIR/review-result.json" >> "$GITHUB_OUTPUT"
 
+      - name: Preserve rejected final review
+        if: ${{ always() && steps.review_context.outcome == 'success' && 
steps.review.outcome == 'failure' }}
+        continue-on-error: true
+        timeout-minutes: 2
+        uses: actions/upload-artifact@v4
+        with:
+          name: rejected-code-review-${{ github.run_id }}-${{ 
github.run_attempt }}
+          path: ${{ env.REVIEW_CONTEXT_DIR }}/review-submission-rejected.json
+          # The context directory is hidden; include only the explicit JSON 
above.
+          include-hidden-files: true
+          if-no-files-found: ignore
+          retention-days: 14
+
       - name: Record invalid Codex auth
         id: auth_invalid_record
         if: ${{ always() && steps.auth.outcome == 'success' && 
steps.review.outputs.auth_invalid_reason == 'refresh_token_reused' }}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to