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 d4f052b9efa [fix](ci) Check terminal events before accepting review 
recovery (#68372)
d4f052b9efa is described below

commit d4f052b9efa8154d201998ef18ebe9f00f59ada7
Author: shuke <[email protected]>
AuthorDate: Tue Sep 22 16:01:46 2026 +0800

    [fix](ci) Check terminal events before accepting review recovery (#68372)
    
    ### What problem does this PR solve?
    
    After a capacity error, the review helper can accept `codex exec --goal
    resume` exiting with status 0 even when the attempt reports
    `turn.failed` or never produces a terminal event. The helper then stops
    retrying and the workflow replaces the underlying error with “no new
    pull request review was submitted.”
    
    Require both a zero process exit and a completed latest turn before
    accepting an attempt. Explicit capacity errors remain eligible for the
    existing bounded retry sequence even when the process exits with 0.
    Authentication, usage-limit and other errors fail without retry; a
    missing terminal event fails explicitly instead of borrowing a previous
    attempt’s capacity error or completion. Existing PR-state checks,
    duplicate-submission protection and the shared deadline are retained.
    
    Each attempt now logs its exit status, last turn event and event count.
    The installation step logs the actual goal binary’s SHA-256 to make
    runtime failures reproducible.
    
    Related PR: #67624
    
    ### Validation
    
    - Linux Python 3.12: all 147 pipeline tests passed, including real
    process cleanup and workflow/auth-quarantine integration tests. Tests
    ran without network access or host mounts.
    - Added 12 regression tests for zero-exit capacity retries and
    exhaustion, zero-exit authentication failures, missing terminal events,
    event ordering and workflow error propagation.
    - The new helper tests fail against the previous implementation,
    including false success and premature retry termination; they pass after
    the fix.
    - macOS helper suite: 55 tests, passed with 3 Linux-only skips.
    - Ruff checks and helper/test formatting, actionlint, workflow YAML
    parsing, all 22 shell blocks with `bash -n`, and `git diff --check`
    passed.
    
    **Rollout limit:** this validates helper behavior with controlled Codex
    output and real Linux subprocesses. The actual OSS goal binary was not
    available for validation (anonymous read is denied); no claim is made
    that the underlying binary’s goal/resume issue is fixed. Before
    production rollout, validate that exact binary’s resumed turn execution
    and terminal-event behavior. The change fails incomplete resumes
    explicitly; it does not blindly retry empty successful exits.
---
 .github/scripts/run_review_with_resume.py      |  53 +++++++--
 .github/scripts/test_review_auth_quarantine.py |  14 +++
 .github/scripts/test_run_review_with_resume.py | 143 +++++++++++++++++++++++++
 .github/workflows/code-review-runner.yml       |   3 +
 4 files changed, 203 insertions(+), 10 deletions(-)

diff --git a/.github/scripts/run_review_with_resume.py 
b/.github/scripts/run_review_with_resume.py
index 0803ad4c147..c27a65c9fdf 100755
--- a/.github/scripts/run_review_with_resume.py
+++ b/.github/scripts/run_review_with_resume.py
@@ -105,14 +105,41 @@ def read_events(path):
     return events
 
 
-def failure(events, status, stderr_path):
-    for event_type in ("turn.failed", "error"):
-        for event in reversed(events):
-            if event.get("type") == event_type:
-                error = event.get("error") or event
-                return error.get("message") or f"Codex exited with status 
{status}"
+def attempt_result(events, status, stderr_path):
+    """Use the latest turn in this attempt, never a previous attempt's 
failure."""
+    terminal = next(
+        (
+            event
+            for event in reversed(events)
+            if event.get("type")
+            in ("turn.started", "turn.completed", "turn.failed", "error")
+        ),
+        {},
+    )
+    event_type = terminal.get("type", "missing")
+    if event_type == "turn.completed":
+        message = (
+            None
+            if status == 0
+            else f"Codex exited with status {status} after turn.completed"
+        )
+        return event_type, message
+    if event_type in ("turn.failed", "error"):
+        error = terminal.get("error") or terminal
+        message = (
+            error.get("message")
+            or f"Codex reported {event_type} (exit status {status})"
+        )
+        return event_type, message
+    # A zero exit after only thread.started (or an unfinished new turn) does
+    # not prove recovery. Do not reuse an older capacity event to retry it.
+    if status == 0:
+        return (
+            event_type,
+            "Codex exited with status 0 without a terminal turn event; review 
is incomplete",
+        )
     lines = stderr_path.read_text(errors="replace").splitlines()
-    return next(
+    return event_type, next(
         (line for line in reversed(lines) if line.strip()),
         f"Codex exited with status {status}",
     )
@@ -388,8 +415,14 @@ def run_review(args, reaper=None):
                 return fail(
                     f"Codex was interrupted or timed out (status {status}); 
not resuming"
                 )
-            message = failure(events, status, stderr_path)
-            if status != 0 and (
+            terminal, message = attempt_result(events, status, stderr_path)
+            print(
+                f"Finished Codex review attempt {attempt + 1}/4 "
+                f"(exit_status={status}, terminal_event={terminal}, 
events={len(events)})",
+                file=sys.stderr,
+                flush=True,
+            )
+            if message is not None and (
                 message != CAPACITY_MESSAGE or attempt == len(RETRY_DELAYS)
             ):
                 return fail(message)
@@ -399,7 +432,7 @@ def run_review(args, reaper=None):
                     "Codex resumed a different session; refusing further 
attempts"
                 )
             thread_id = current_id
-            if status == 0:
+            if message is None:
                 return 0
             require_rollout(Path(os.environ["CODEX_HOME"]), thread_id, 
args.cwd)
             # Check parser support without authenticating or starting a model 
request.
diff --git a/.github/scripts/test_review_auth_quarantine.py 
b/.github/scripts/test_review_auth_quarantine.py
index 2f414d97391..cc9b509cba7 100755
--- a/.github/scripts/test_review_auth_quarantine.py
+++ b/.github/scripts/test_review_auth_quarantine.py
@@ -373,9 +373,23 @@ class ReviewAuthQuarantineTest(unittest.TestCase):
             {"type": "turn.failed", "error": {"message": "Request timed out"}},
         ], expected_invalid=False)
 
+    def test_zero_exit_auth_failure_still_reaches_quarantine(self):
+        _, outputs = self.fail_review(FAKE_CODEX_STATUS="0")
+        self.assertIn(REUSED_MESSAGE, outputs)
+        self.assertNotIn("no new pull request review", outputs)
+
+    def 
test_zero_exit_without_terminal_event_does_not_pass_with_a_review(self):
+        _, outputs = self.fail_review(
+            events=[], expected_invalid=False, FAKE_CODEX_STATUS="0"
+        )
+        # The fake GitHub API reports a review, but an incomplete attempt must
+        # still fail rather than borrowing that review as proof of completion.
+        self.assertIn("without a terminal turn event", outputs)
+
     def test_success_is_not_quarantined_even_with_earlier_stderr_error(self):
         _, outputs = self.run_step(
             "Run automated code review", FAKE_CODEX_STATUS="0",
+            FAKE_CODEX_EVENTS=json.dumps({"type": "turn.completed", "usage": 
{}}),
             FAKE_CODEX_STDERR='{"code":"refresh_token_reused"}',
         )
         self.assertNotIn("auth_invalid_reason", outputs)
diff --git a/.github/scripts/test_run_review_with_resume.py 
b/.github/scripts/test_run_review_with_resume.py
index 24f91723f4b..54a69fb7916 100755
--- a/.github/scripts/test_run_review_with_resume.py
+++ b/.github/scripts/test_run_review_with_resume.py
@@ -188,6 +188,149 @@ class ResumeReviewTest(unittest.TestCase):
         self.assertEqual("done", (self.context / 
"codex-final-message.txt").read_text())
         self.target_check.assert_called_once()
 
+    def test_zero_exit_capacity_on_resume_still_retries(self):
+        self.assertEqual(
+            0,
+            self.execute(
+                [
+                    {"events": [thread_event(), failed()]},
+                    {"events": [thread_event(), failed()], "status": 0},
+                    {"events": [thread_event(), completed()], "status": 0},
+                ]
+            ),
+        )
+        self.assertEqual(3, len(self.commands))
+        self.assertEqual([30, 60], self.sleeps)
+        self.assertEqual(2, self.target_check.call_count)
+        self.assertEqual("completed", 
exporter.latest_turn_result(self.events())[0])
+
+    def test_zero_exit_capacity_stops_at_the_retry_limit(self):
+        self.assertEqual(
+            1, self.execute([{"events": [thread_event(), failed()], "status": 
0}] * 4)
+        )
+        self.assertEqual([30, 60, 120], self.sleeps)
+        self.assertEqual(4, len(self.commands))
+        self.assertEqual(runner.CAPACITY_MESSAGE, self.last_error())
+
+    def test_zero_exit_error_event_can_identify_capacity(self):
+        self.assertEqual(
+            0,
+            self.execute(
+                [
+                    {
+                        "events": [
+                            thread_event(),
+                            {"type": "error", "message": 
runner.CAPACITY_MESSAGE},
+                        ],
+                        "status": 0,
+                    },
+                    {"events": [thread_event(), completed()], "status": 0},
+                ]
+            ),
+        )
+        self.assertEqual([30], self.sleeps)
+
+    def test_zero_exit_auth_usage_and_generic_failures_do_not_retry(self):
+        for message in (
+            "refresh_token_reused",
+            "You've hit your usage limit.",
+            "HTTP 500",
+        ):
+            with self.subTest(message=message), tempfile.TemporaryDirectory() 
as tmp:
+                self.args.context_dir = Path(tmp)
+                (self.args.context_dir / 
"codex_goal_prompt.txt").write_text("review")
+                self.assertEqual(
+                    1,
+                    self.execute(
+                        [{"events": [thread_event(), failed(message)], 
"status": 0}]
+                    ),
+                )
+                events = runner.read_events(
+                    self.args.context_dir / "codex-events.jsonl"
+                )
+                self.assertEqual(message, events[-1]["error"]["message"])
+        self.assertEqual([], self.sleeps)
+        self.target_check.assert_not_called()
+
+    def test_zero_exit_without_terminal_event_fails_closed(self):
+        self.assertEqual(1, self.execute([{"events": [thread_event()], 
"status": 0}]))
+        self.assertIn("without a terminal turn event", self.last_error())
+        self.assertEqual([], self.sleeps)
+
+    def test_empty_resume_does_not_reuse_the_previous_capacity_error(self):
+        self.assertEqual(
+            1,
+            self.execute(
+                [
+                    {"events": [thread_event(), failed()]},
+                    {"events": [thread_event()], "status": 0},
+                ]
+            ),
+        )
+        self.assertEqual(2, len(self.commands))
+        self.assertEqual([30], self.sleeps)
+        self.assertIn("without a terminal turn event", self.last_error())
+        self.assertNotEqual(runner.CAPACITY_MESSAGE, self.last_error())
+
+    def 
test_later_completion_supersedes_earlier_error_in_the_same_attempt(self):
+        self.assertEqual(
+            0,
+            self.execute(
+                [{"events": [thread_event(), failed(), completed()], "status": 
0}]
+            ),
+        )
+        self.assertEqual([], self.sleeps)
+
+    def test_later_failure_supersedes_earlier_completion(self):
+        self.assertEqual(
+            1,
+            self.execute(
+                [
+                    {
+                        "events": [thread_event(), completed(), failed("auth 
failed")],
+                        "status": 0,
+                    }
+                ]
+            ),
+        )
+        self.assertEqual("auth failed", self.last_error())
+        self.assertEqual([], self.sleeps)
+
+    def test_unfinished_new_turn_does_not_reuse_an_earlier_completion(self):
+        self.assertEqual(
+            1,
+            self.execute(
+                [
+                    {
+                        "events": [
+                            thread_event(),
+                            completed(),
+                            {"type": "turn.started"},
+                        ],
+                        "status": 0,
+                    }
+                ]
+            ),
+        )
+        self.assertIn("without a terminal turn event", self.last_error())
+        self.assertEqual([], self.sleeps)
+
+    def 
test_completion_with_nonzero_exit_does_not_retry_old_stderr_capacity(self):
+        self.assertEqual(
+            1,
+            self.execute(
+                [
+                    {
+                        "events": [thread_event(), completed()],
+                        "status": 1,
+                        "stderr": runner.CAPACITY_MESSAGE,
+                    }
+                ]
+            ),
+        )
+        self.assertIn("status 1 after turn.completed", self.last_error())
+        self.assertEqual([], self.sleeps)
+
     def test_retry_count_is_bounded(self):
         self.assertEqual(1, self.execute([{"events": [thread_event(), 
failed()]}] * 4))
         self.assertEqual([30, 60, 120], self.sleeps)
diff --git a/.github/workflows/code-review-runner.yml 
b/.github/workflows/code-review-runner.yml
index 75b168d4d96..dcb97cf206d 100644
--- a/.github/workflows/code-review-runner.yml
+++ b/.github/workflows/code-review-runner.yml
@@ -200,6 +200,9 @@ jobs:
           sudo install -m 0755 "$tmp_dir/codex-goal" "$codex_target"
           "$codex_cmd" exec --help | grep -q -- '--goal'
           "$codex_cmd" --version
+          # The deployed goal binary currently reports only 0.0.0. Record its
+          # identity so a resume failure can be reproduced with the same build.
+          sha256sum "$codex_target"
         env:
           OSS_AK: ${{ secrets.OSS_AK }}
           OSS_SK: ${{ secrets.OSS_SK }}


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

Reply via email to