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 44dfa1596fb [fix](ci) Preserve early review failure notifications and 
causes (#69469)
44dfa1596fb is described below

commit 44dfa1596fb309d96dffa5ca4615bcbe5d8af2d3
Author: shuke <[email protected]>
AuthorDate: Mon Nov 2 14:24:14 2026 +0800

    [fix](ci) Preserve early review failure notifications and causes (#69469)
    
    ### What problem does this PR solve?
    
    Related PR: #69386
    
    Problem Summary:
    
    When the pending-status API call fails before repository checkout, the
    failure notification runs outside a Git checkout. Without an explicit
    repository, `gh pr comment` fails with `fatal: not a git repository`;
    both the notification and terminal failure step also misidentify the
    skipped context preparation step as the cause. This occurred in [run
    
38864234303](https://github.com/apache/doris/actions/runs/38864234303/job/116853272967).
    
    Pass `--repo` explicitly and report failed/cancelled pending-status and
    checkout steps by name. Distinguish actual context failure/cancellation
    from skipped or unstarted context in both reporting paths, preserving
    the existing auth/usage-limit recovery messages and failing exit status.
---
 .../scripts/test_review_failure_notification.py    | 171 +++++++++++++++++++++
 .github/workflows/code-review-runner.yml           |  28 +++-
 2 files changed, 197 insertions(+), 2 deletions(-)

diff --git a/.github/scripts/test_review_failure_notification.py 
b/.github/scripts/test_review_failure_notification.py
new file mode 100644
index 00000000000..3af05c3d47f
--- /dev/null
+++ b/.github/scripts/test_review_failure_notification.py
@@ -0,0 +1,171 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Exercise failure reporting before checkout using the actual workflow 
shell."""
+
+import json
+import os
+import re
+import subprocess
+import sys
+import tempfile
+import textwrap
+import unittest
+from pathlib import Path
+
+WORKFLOW = Path(__file__).resolve().parents[1] / 
"workflows/code-review-runner.yml"
+COMMENT = "Comment PR on review failure"
+FAIL = "Fail workflow if review failed"
+
+
+def workflow_step(name):
+    match = re.search(
+        rf"^      - name: {re.escape(name)}\n(.*?)(?=^      - name:|\Z)",
+        WORKFLOW.read_text(), re.MULTILINE | re.DOTALL,
+    )
+    if match is None:
+        raise AssertionError(f"Missing workflow step: {name}")
+    return match.group(1)
+
+
+class ReviewFailureNotificationTest(unittest.TestCase):
+    def setUp(self):
+        temp = tempfile.TemporaryDirectory()
+        self.addCleanup(temp.cleanup)
+        self.root = Path(temp.name)
+        fake_gh = self.root / "gh"
+        fake_gh.write_text(f"#!{sys.executable}\n" + textwrap.dedent('''\
+            import json
+            import os
+            import sys
+            from pathlib import Path
+            Path(os.environ["GH_CALL"]).write_text(json.dumps(sys.argv[1:]))
+            sys.exit(int(os.environ.get("GH_EXIT", "0")))
+        '''))
+        fake_gh.chmod(0o700)
+        self.context = {
+            "github.repository": "example/repo",
+            "github.server_url": "https://github.example.test";,
+            "github.run_id": "1234",
+            "secrets.GITHUB_TOKEN": "fake-token",
+            "steps.review_inputs.outputs.pr_number": "42",
+            "steps.pending_status.outcome": "success",
+            "steps.checkout.outcome": "success",
+            "steps.review_context.outcome": "skipped",
+            "steps.review.outcome": "skipped",
+        }
+
+    def run_step(self, name, gh_exit=0):
+        step = workflow_step(name)
+        self.assertIn("always()", step)
+        env = {
+            **os.environ,
+            "PATH": f"{self.root}{os.pathsep}{os.environ['PATH']}",
+            "GH_CALL": str(self.root / "gh-call.json"),
+            "GH_EXIT": str(gh_exit),
+        }
+        # Render the real env bindings, so a missing/wrong outcome binding 
fails
+        # the behavioral tests instead of being hidden by injected shell vars.
+        bindings = step.split("        env:\n", 1)[1].split("        run:", 
1)[0]
+        for line in bindings.splitlines():
+            key, value = line.strip().split(": ", 1)
+            env[key] = re.sub(
+                r"\$\{\{\s*(.*?)\s*\}\}",
+                lambda match: self.context.get(match.group(1), ""), value,
+            )
+        env["REVIEW_GH_HELPER"] = 
str(Path(__file__).with_name("review_github_api.py"))
+        env["REVIEW_GITHUB_DIAGNOSTICS_DIR"] = str(self.root / "diagnostics")
+        script = textwrap.dedent(step.split("        run: |\n", 1)[1])
+        result = subprocess.run(
+            ["bash", "--noprofile", "--norc", "-eo", "pipefail", "-c", script],
+            env=env, cwd=self.root, capture_output=True, text=True, timeout=10,
+        )
+        self.assertEqual(result.returncode, gh_exit if name == COMMENT else 1,
+                         result.stdout + result.stderr)
+        if name == COMMENT:
+            args = json.loads((self.root / "gh-call.json").read_text())
+            self.assertEqual(args[:3], ["pr", "comment", "42"])
+            self.assertIn("--repo", args)
+            self.assertEqual(args[args.index("--repo") + 1], "example/repo")
+            body = args[args.index("--body") + 1]
+            
self.assertIn("https://github.example.test/example/repo/actions/runs/1234";, 
body)
+            return body
+        return result.stdout
+
+    def assert_both_report(self, expected, *, gh_exit=0):
+        for name in (COMMENT, FAIL):
+            with self.subTest(step=name):
+                self.assertIn(expected, self.run_step(name, gh_exit=gh_exit))
+
+    def test_pending_status_failure_before_checkout(self):
+        self.context.update({
+            "steps.pending_status.outcome": "failure",
+            "steps.checkout.outcome": "skipped",
+        })
+        self.assertFalse((self.root / ".git").exists())
+        self.assert_both_report("'Mark Code Review status as pending' ended 
with failure")
+
+    def 
test_checkout_failure_with_pending_success_or_intentionally_skipped(self):
+        self.context["steps.checkout.outcome"] = "failure"
+        for outcome in ("success", "skipped"):
+            with self.subTest(pending=outcome):
+                self.context["steps.pending_status.outcome"] = outcome
+                self.assert_both_report("'Checkout repository' ended with 
failure")
+
+    def test_cancelled_early_steps_are_identified(self):
+        for step, label in (("pending_status", "Mark Code Review status as 
pending"),
+                            ("checkout", "Checkout repository")):
+            with self.subTest(step=step):
+                self.context[f"steps.{step}.outcome"] = "cancelled"
+                self.assert_both_report(f"'{label}' ended with cancelled")
+                self.context[f"steps.{step}.outcome"] = "success"
+
+    def test_skipped_or_unstarted_context_does_not_claim_context_failure(self):
+        for outcome in ("skipped", ""):
+            with self.subTest(context=outcome):
+                self.context["steps.review_context.outcome"] = outcome
+                self.assert_both_report("Review setup stopped before context 
preparation ran")
+
+    def test_actual_context_failure_and_cancellation(self):
+        for outcome, message in (("failure", "Review context preparation 
failed"),
+                                 ("cancelled", "Review context preparation was 
cancelled")):
+            with self.subTest(context=outcome):
+                self.context["steps.review_context.outcome"] = outcome
+                self.assert_both_report(message)
+
+    def test_auth_and_review_reasons_are_preserved(self):
+        self.context["steps.auth.outputs.failure_reason"] = "No eligible Codex 
account"
+        self.assert_both_report("No eligible Codex account")
+        self.context.pop("steps.auth.outputs.failure_reason")
+        self.context.update({
+            "steps.review_context.outcome": "success",
+            "steps.review.outcome": "failure",
+            "steps.review.outputs.failure_reason": "Final review verification 
failed",
+        })
+        self.assert_both_report("Final review verification failed")
+
+    def test_notification_failure_does_not_replace_terminal_cause(self):
+        self.context.update({
+            "steps.pending_status.outcome": "failure",
+            "steps.checkout.outcome": "skipped",
+        })
+        self.assert_both_report("'Mark Code Review status as pending' ended 
with failure", gh_exit=7)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/.github/workflows/code-review-runner.yml 
b/.github/workflows/code-review-runner.yml
index b9a65d83171..802567c1beb 100644
--- a/.github/workflows/code-review-runner.yml
+++ b/.github/workflows/code-review-runner.yml
@@ -314,6 +314,7 @@ jobs:
           } >> "$GITHUB_OUTPUT"
 
       - name: Mark Code Review status as pending
+        id: pending_status
         if: ${{ steps.review_inputs.outputs.manage_status == 'true' }}
         timeout-minutes: 2
         env:
@@ -339,6 +340,7 @@ jobs:
             -f target_url="${{ github.server_url }}/${{ github.repository 
}}/actions/runs/${{ github.run_id }}"
 
       - name: Checkout repository
+        id: checkout
         timeout-minutes: 10
         uses: actions/checkout@v4
         with:
@@ -1094,6 +1096,8 @@ jobs:
           AUTH_INVALID_RECORD_OUTCOME: ${{ steps.auth_invalid_record.outcome }}
           USAGE_LIMIT_RECORD_OUTCOME: ${{ steps.usage_limit_record.outcome }}
           USAGE_LIMIT_RETRY_AFTER_EPOCH: ${{ 
steps.review.outputs.usage_limit_retry_after_epoch }}
+          PENDING_STATUS_OUTCOME: ${{ steps.pending_status.outcome }}
+          CHECKOUT_OUTCOME: ${{ steps.checkout.outcome }}
           REVIEW_CONTEXT_OUTCOME: ${{ steps.review_context.outcome }}
           REVIEW_FAILURE_REASON: ${{ steps.review.outputs.failure_reason }}
           REVIEW_OUTCOME: ${{ steps.review.outcome }}
@@ -1130,8 +1134,17 @@ jobs:
           elif [ -n "$USAGE_LIMIT_RETRY_AFTER_EPOCH" ]; then
             error_msg="Codex reached its usage limit, but the workflow failed 
to record the account context."
             recovery_msg="Please inspect the workflow logs and trigger /review 
again after the recording problem is resolved."
-          elif [ "$REVIEW_CONTEXT_OUTCOME" != "success" ]; then
+          elif [ "$PENDING_STATUS_OUTCOME" = "failure" ] || [ 
"$PENDING_STATUS_OUTCOME" = "cancelled" ]; then
+            error_msg="Step 'Mark Code Review status as pending' ended with 
$PENDING_STATUS_OUTCOME; inspect its logs for the original error."
+          elif [ "$CHECKOUT_OUTCOME" = "failure" ] || [ "$CHECKOUT_OUTCOME" = 
"cancelled" ]; then
+            error_msg="Step 'Checkout repository' ended with 
$CHECKOUT_OUTCOME; inspect its logs for the original error."
+          elif [ "$REVIEW_CONTEXT_OUTCOME" = "failure" ]; then
             error_msg="Review context preparation failed before Codex ran; 
inspect the 'Prepare authoritative PR context and required AGENTS guides' step."
+          elif [ "$REVIEW_CONTEXT_OUTCOME" = "cancelled" ]; then
+            error_msg="Review context preparation was cancelled; inspect the 
'Prepare authoritative PR context and required AGENTS guides' step."
+          elif [ "$REVIEW_CONTEXT_OUTCOME" != "success" ]; then
+            # Skipped/unstarted context is a consequence of an earlier setup 
failure.
+            error_msg="Review setup stopped before context preparation ran; 
inspect the first failed or cancelled setup step."
           else
             error_msg="${REVIEW_FAILURE_REASON:-Review step was 
$REVIEW_OUTCOME (possibly timeout or cancelled)}"
           fi
@@ -1151,6 +1164,8 @@ jobs:
         env:
           REVIEW_INPUTS_OUTCOME: ${{ steps.review_inputs.outcome }}
           AUTH_FAILURE_REASON: ${{ steps.auth.outputs.failure_reason }}
+          PENDING_STATUS_OUTCOME: ${{ steps.pending_status.outcome }}
+          CHECKOUT_OUTCOME: ${{ steps.checkout.outcome }}
           REVIEW_CONTEXT_OUTCOME: ${{ steps.review_context.outcome }}
           REVIEW_FAILURE_REASON: ${{ steps.review.outputs.failure_reason }}
           REVIEW_OUTCOME: ${{ steps.review.outcome }}
@@ -1159,8 +1174,17 @@ jobs:
             error_msg="Step 'Resolve review inputs' ended with 
$REVIEW_INPUTS_OUTCOME; inspect its logs for the original error."
           elif [ -n "$AUTH_FAILURE_REASON" ]; then
             error_msg="$AUTH_FAILURE_REASON"
-          elif [ "$REVIEW_CONTEXT_OUTCOME" != "success" ]; then
+          elif [ "$PENDING_STATUS_OUTCOME" = "failure" ] || [ 
"$PENDING_STATUS_OUTCOME" = "cancelled" ]; then
+            error_msg="Step 'Mark Code Review status as pending' ended with 
$PENDING_STATUS_OUTCOME; inspect its logs for the original error."
+          elif [ "$CHECKOUT_OUTCOME" = "failure" ] || [ "$CHECKOUT_OUTCOME" = 
"cancelled" ]; then
+            error_msg="Step 'Checkout repository' ended with 
$CHECKOUT_OUTCOME; inspect its logs for the original error."
+          elif [ "$REVIEW_CONTEXT_OUTCOME" = "failure" ]; then
             error_msg="Review context preparation failed before Codex ran; 
inspect the 'Prepare authoritative PR context and required AGENTS guides' step."
+          elif [ "$REVIEW_CONTEXT_OUTCOME" = "cancelled" ]; then
+            error_msg="Review context preparation was cancelled; inspect the 
'Prepare authoritative PR context and required AGENTS guides' step."
+          elif [ "$REVIEW_CONTEXT_OUTCOME" != "success" ]; then
+            # Skipped/unstarted context is a consequence of an earlier setup 
failure.
+            error_msg="Review setup stopped before context preparation ran; 
inspect the first failed or cancelled setup step."
           else
             error_msg="${REVIEW_FAILURE_REASON:-Review step was 
$REVIEW_OUTCOME (possibly timeout or cancelled)}"
           fi


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

Reply via email to