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 1fc02707cb1 [fix](ci) Verify completed reviews before recovering
wrap-up capacity errors (#68599)
1fc02707cb1 is described below
commit 1fc02707cb1c1cb1d9ea942cfc575bda9c184b93
Author: shuke <[email protected]>
AuthorDate: Tue Sep 29 11:24:50 2026 +0800
[fix](ci) Verify completed reviews before recovering wrap-up capacity
errors (#68599)
Related PR: #68567
Problem Summary:
The review of #66227 in run 36422537817 submitted and verified review
5339520198 with one P1 finding, then hit model capacity during wrap-up.
The
runner waited five minutes and failed its duplicate-review guard. The
existing status publisher also treated any successful execution as a
passing
review without considering P0/P1 findings.
Declare the complete final review through a trusted helper loaded from
the
workflow SHA. Bind its summary, full inline set, and reconfirmed
existing
blockers to a per-invocation marker and target head. Independently
verify
GitHub delivery before accepting completion, including after a terminal
capacity error. Read back ambiguous submissions without reposting.
Preserve
failure for incomplete, foreign, stale, cancelled, or unverifiable work.
Separate execution completion from the PR verdict: completed P0/P1
reviews
block code-review, while completed reviews without P0/P1 pass. Keep
authorized
local PASS/skip overrides. Retain the real failed model turn in
telemetry and
record verified completion separately. Six five-minute retries remain
for
unfinished capacity failures.
---
.github/scripts/emit_litefuse_otel_io.py | 10 +
.github/scripts/resolve_code_review_status.py | 13 +
.github/scripts/run_review_with_resume.py | 30 +-
.github/scripts/submit_review.py | 217 ++++++++++++++
.github/scripts/test_emit_litefuse_otel_io.py | 20 ++
.github/scripts/test_resolve_code_review_status.py | 27 ++
.github/scripts/test_review_auth_quarantine.py | 59 +++-
.github/scripts/test_run_review_with_resume.py | 38 ++-
.github/scripts/test_submit_review.py | 311 +++++++++++++++++++++
.github/workflows/code-review-runner.yml | 74 ++---
10 files changed, 745 insertions(+), 54 deletions(-)
diff --git a/.github/scripts/emit_litefuse_otel_io.py
b/.github/scripts/emit_litefuse_otel_io.py
index 53ef46949af..4da5db39950 100644
--- a/.github/scripts/emit_litefuse_otel_io.py
+++ b/.github/scripts/emit_litefuse_otel_io.py
@@ -379,6 +379,16 @@ def build_ingestion_payload(args, input_text, output_text,
events):
key: value for key, value in trace_metadata.items() if value not in
(None, "")
}
+ # Keep the model turn's real failure/usage while exposing the independently
+ # verified delivery and blocking verdict as a separate trace outcome.
+ completion = next((event for event in reversed(events)
+ if event.get("type") == "review.completed"), None)
+ if completion is not None:
+ trace_metadata["review_completion"] = {
+ key: value for key, value in completion.items()
+ if key not in ("type", "_line_number")
+ }
+
completed_items = [
event
for event in events
diff --git a/.github/scripts/resolve_code_review_status.py
b/.github/scripts/resolve_code_review_status.py
index cc81807e34e..a2d54302577 100644
--- a/.github/scripts/resolve_code_review_status.py
+++ b/.github/scripts/resolve_code_review_status.py
@@ -72,10 +72,23 @@ def resolve_status(
latest_by_context[context] = item
approved_contexts: set[tuple[int, str]] = set()
+ blocked_contexts: set[tuple[int, str]] = set()
for context, item in latest_by_context.items():
match = SOURCE_CONTEXT_RE.fullmatch(context)
if match is not None and item["state"] == "success":
approved_contexts.add((int(match.group(1)),
match.group(2).casefold()))
+ elif (match is not None and item["state"] == "failure"
+ and context.startswith("code-review/source/automated/")):
+ blocked_contexts.add((int(match.group(1)),
match.group(2).casefold()))
+
+ # Preserve the authorized local PASS / skip override. Otherwise a completed
+ # automated review with P0/P1 blocks, rather than appearing to still be
running.
+ blocked = sorted((open_contexts - approved_contexts) & blocked_contexts)
+ if blocked:
+ number, base_sha = blocked[0]
+ return Resolution(
+ "failure", f"Review completed with P0/P1 for PR #{number} at
{base_sha[:12]}.",
+ )
missing = sorted(open_contexts - approved_contexts)
if missing:
diff --git a/.github/scripts/run_review_with_resume.py
b/.github/scripts/run_review_with_resume.py
index a6176cb3616..43fe87a4079 100755
--- a/.github/scripts/run_review_with_resume.py
+++ b/.github/scripts/run_review_with_resume.py
@@ -32,6 +32,8 @@ import uuid
from datetime import datetime, timezone
from pathlib import Path
+from submit_review import RUN_FILE, RESULT_FILE, SUBMISSION_FILE,
verify_completion, write_json
+
# Six same-session capacity retries, each after a fixed five-minute wait.
RETRY_DELAYS = (300,) * 6
CAPACITY_MESSAGE = "Selected model is at capacity. Please try a different
model."
@@ -353,6 +355,12 @@ def run_review(args, reaper=None):
goal_prompt = (context / "codex_goal_prompt.txt").read_text()
deadline = time.monotonic() + args.budget_seconds
started_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
+ run = {
+ "repository": args.repository, "pr_number": args.pr_number,
+ "head_sha": args.head_sha, "base_sha": args.base_sha,
+ "started_at": started_at, "token": str(uuid.uuid4()), "deadline":
deadline,
+ }
+ write_json(context / RUN_FILE, run)
thread_id = None
model = args.model
fallback_model = getattr(args, "fallback_model", None)
@@ -458,9 +466,7 @@ def run_review(args, reaper=None):
)
model = fallback_model
continue
- if message is not None and (
- message != CAPACITY_MESSAGE or capacity_retry ==
len(RETRY_DELAYS)
- ):
+ if message is not None and message != CAPACITY_MESSAGE:
return fail(message)
current_id = session_id(events)
if thread_id and current_id != thread_id:
@@ -468,8 +474,24 @@ def run_review(args, reaper=None):
"Codex resumed a different session; refusing further
attempts"
)
thread_id = current_id
- if message is None:
+ if message is None or (context / SUBMISSION_FILE).exists():
+ # Final submission declares all review work complete. Only an
+ # exact GitHub readback can recover a subsequent capacity
error;
+ # another run's review or a partially posted review cannot
pass.
+ result = verify_completion(context, run, remaining)
+ result["recovered_after_capacity"] = message ==
CAPACITY_MESSAGE
+ write_json(context / RESULT_FILE, result)
+ with aggregate.open("a") as handle:
+ handle.write(json.dumps({"type": "review.completed",
**result}) + "\n")
+ print(
+ f"Verified final review {result['review_id']}: "
+ f"P0={result['p0']}, P1={result['p1']}, "
+
f"recovered_after_capacity={result['recovered_after_capacity']}",
+ file=sys.stderr, flush=True,
+ )
return 0
+ if capacity_retry == len(RETRY_DELAYS):
+ return fail(message)
require_rollout(Path(os.environ["CODEX_HOME"]), thread_id,
args.cwd)
# Check parser support without authenticating or starting a model
request.
subprocess.run(
diff --git a/.github/scripts/submit_review.py b/.github/scripts/submit_review.py
new file mode 100644
index 00000000000..edd8fe9cf18
--- /dev/null
+++ b/.github/scripts/submit_review.py
@@ -0,0 +1,217 @@
+#!/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.
+
+"""Submit and verify a final review bound to this runner invocation."""
+
+import argparse
+import hashlib
+import json
+import re
+import subprocess
+import time
+from collections import Counter
+from pathlib import Path
+
+
+SUBMISSION_FILE = "review-submission.json"
+RUN_FILE = "review-run.json"
+RESULT_FILE = "review-result.json"
+PRIORITY = re.compile(r"^\[P([0-3])\]\s+\S")
+
+
+def write_json(path, value):
+ temporary = path.with_suffix(".tmp")
+ temporary.write_text(json.dumps(value, indent=2) + "\n")
+ temporary.replace(path)
+
+
+def priority(body):
+ match = PRIORITY.match(body)
+ if match is None:
+ raise ValueError("Every finding must start with [P0], [P1], [P2] or
[P3]")
+ return int(match.group(1))
+
+
+def validate_submission(value):
+ if not isinstance(value, dict) or set(value) != {
+ "body", "comments", "existing_blocking_comment_ids"
+ }:
+ raise ValueError("Final review requires body, comments and
existing_blocking_comment_ids")
+ if not isinstance(value["body"], str) or not value["body"].strip():
+ raise ValueError("Final review summary is empty")
+ if not isinstance(value["comments"], list):
+ raise ValueError("comments must be an array")
+ for comment in value["comments"]:
+ if not isinstance(comment, dict) or set(comment) != {"path",
"position", "body"}:
+ raise ValueError("Each finding requires path, position and body")
+ if not isinstance(comment["path"], str) or not comment["path"]:
+ raise ValueError("Finding path is empty")
+ if type(comment["position"]) is not int or comment["position"] <= 0:
+ raise ValueError("Finding position must be a positive diff
position")
+ if not isinstance(comment["body"], str):
+ raise ValueError("Finding body must be text")
+ priority(comment["body"])
+ ids = value["existing_blocking_comment_ids"]
+ if not isinstance(ids, list) or any(type(i) is not int or i <= 0 for i in
ids):
+ raise ValueError("Existing blocking comment IDs must be positive
integers")
+ if len(set(ids)) != len(ids):
+ raise ValueError("Existing blocking comment IDs must be unique")
+ return value
+
+
+def api(path, remaining, *, payload=None, paginated=False):
+ command = ["gh", "api", path]
+ if paginated:
+ command += ["--paginate", "--slurp"]
+ if payload is not None:
+ command += ["--method", "POST", "--input", "-"]
+ result = subprocess.run(
+ command, input=json.dumps(payload) if payload is not None else None,
+ capture_output=True, text=True, check=True, timeout=min(30,
remaining()),
+ )
+ value = json.loads(result.stdout)
+ return [item for page in value for item in page] if paginated else value
+
+
+def check_target(run, remaining):
+ root = f"repos/{run['repository']}/pulls/{run['pr_number']}"
+ pr = api(root, remaining)
+ if (pr["state"] != "open" or pr["head"]["sha"] != run["head_sha"]
+ or pr["base"]["sha"] != run["base_sha"]):
+ raise ValueError("PR base/head or open state changed; final review is
stale")
+ return root
+
+
+def review_payload(submission, run, root, remaining):
+ counts = [0, 0, 0, 0]
+ for comment in submission["comments"]:
+ counts[priority(comment["body"])] += 1
+ # Previously reported findings are not posted again. The reviewer must
+ # explicitly reconfirm which existing P0/P1 findings still apply to this
head.
+ ids = submission["existing_blocking_comment_ids"]
+ if ids:
+ comments = {c["id"]: c for c in api(f"{root}/comments", remaining,
paginated=True)}
+ for comment_id in ids:
+ comment = comments.get(comment_id)
+ if comment is None or comment.get("in_reply_to_id") is not None:
+ raise ValueError(f"Existing blocking finding {comment_id} is
missing or a reply")
+ level = priority(comment["body"])
+ if level > 1:
+ raise ValueError(f"Existing finding {comment_id} is not P0/P1")
+ counts[level] += 1
+ # The marker binds the entire declared final submission to this invocation,
+ # including the deduplicated findings. A parallel run cannot satisfy it.
+ digest = hashlib.sha256(json.dumps(submission,
sort_keys=True).encode()).hexdigest()
+ marker = f"<!-- code-review-completion:{run['token']}:{digest} -->"
+ body = submission["body"]
+ if ids:
+ body += "\n\nExisting P0/P1 findings confirmed for this head: " + ",
".join(
+
f"https://github.com/{run['repository']}/pull/{run['pr_number']}#discussion_r{i}"
+ for i in ids
+ )
+ body += f"\n\n{marker}"
+ return {
+ "commit_id": run["head_sha"],
+ "event": "REQUEST_CHANGES" if counts[0] + counts[1] else "COMMENT",
+ "body": body,
+ "comments": submission["comments"],
+ }, counts, marker
+
+
+def verify_completion(context, run, remaining, *, submit=False,
submission=None):
+ """Never infer completion from an unowned review or a local result file."""
+ path = context / SUBMISSION_FILE
+ if submission is None:
+ if not path.exists():
+ raise ValueError("No final review submission was declared")
+ submission = json.loads(path.read_text())
+ submission = validate_submission(submission)
+ if path.exists() and json.loads(path.read_text()) != submission:
+ raise ValueError("Final submission already declared; refusing a
different review")
+ root = check_target(run, remaining)
+ payload, counts, marker = review_payload(submission, run, root, remaining)
+ reviews = api(f"{root}/reviews", remaining, paginated=True)
+ matching = [r for r in reviews if marker in (r.get("body") or "")]
+ if not matching and submit:
+ 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")
+ 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
+ # runner can read back this exact review without repeating the write.
+ # Exclusive creation also prevents two accidental simultaneous helper
+ # invocations from both posting after their initial read found no
review.
+ with path.open("x") as handle:
+ json.dump(submission, handle)
+ api(f"{root}/reviews", remaining, payload=payload)
+ matching = [r for r in api(f"{root}/reviews", remaining,
paginated=True)
+ if marker in (r.get("body") or "")]
+ if len(matching) != 1:
+ raise ValueError("Could not verify exactly one final review from this
invocation")
+ review = matching[0]
+ expected_state = "CHANGES_REQUESTED" if payload["event"] ==
"REQUEST_CHANGES" else "COMMENTED"
+ if (review.get("user", {}).get("login") != "github-actions[bot]"
+ or review.get("commit_id") != run["head_sha"]
+ or (review.get("submitted_at") or "") < run["started_at"]
+ or review.get("state") != expected_state
+ or review.get("body") != payload["body"]):
+ raise ValueError("Final review author, head, timestamp, state or
summary did not match")
+ # Replies may arrive while the model wraps up. They are not part of the
+ # declared inline set and must not invalidate an otherwise complete review.
+ comments = [c for c in api(f"{root}/reviews/{review['id']}/comments",
remaining, paginated=True)
+ if c.get("in_reply_to_id") is None]
+ expected = Counter((c["path"], c["position"], c["body"]) for c in
payload["comments"])
+ actual = Counter((c["path"], c.get("position"), c["body"]) for c in
comments)
+ if actual != expected or any(
+ c.get("user", {}).get("login") != "github-actions[bot]"
+ or c.get("commit_id") != run["head_sha"] for c in comments
+ ):
+ raise ValueError("Final review inline comments were not completely
verified")
+ # Recheck after the API reads so a moving PR cannot pass an obsolete
review.
+ check_target(run, remaining)
+ return {
+ "state": "failure" if counts[0] + counts[1] else "success",
+ "p0": counts[0], "p1": counts[1], "review_id": review["id"],
+ "review_url": review["html_url"],
+ }
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--context-dir", type=Path, required=True)
+ parser.add_argument("--input-file", type=Path, required=True)
+ args = parser.parse_args()
+ run = json.loads((args.context_dir / RUN_FILE).read_text())
+
+ def remaining():
+ seconds = run["deadline"] - time.monotonic()
+ if seconds <= 0:
+ raise TimeoutError("Review's shared time budget was exhausted")
+ return seconds
+
+ result = verify_completion(
+ args.context_dir, run, remaining, submit=True,
+ submission=json.loads(args.input_file.read_text()),
+ )
+ print(json.dumps(result))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.github/scripts/test_emit_litefuse_otel_io.py
b/.github/scripts/test_emit_litefuse_otel_io.py
index db2def32208..866b3c09ead 100644
--- a/.github/scripts/test_emit_litefuse_otel_io.py
+++ b/.github/scripts/test_emit_litefuse_otel_io.py
@@ -172,6 +172,26 @@ class LitefuseOtelExporterTest(unittest.TestCase):
["doris-ai-review", "codex-jsonl"],
)
+ def test_verified_completion_preserves_capacity_turn_for_diagnostics(self):
+ args = SimpleNamespace(
+ repository="apache/doris", workflow="Code Review", run_id="123",
pr_number="123",
+ head_sha="a" * 40, base_sha="b" * 40, reasoning_effort="xhigh",
+ max_json_chars=20000, max_context_json_chars=0,
trace_name="review",
+ session_id="123", environment="test", model="gpt-6-sol",
+ )
+ completion = {"state": "failure", "p0": 0, "p1": 1, "review_id": 99,
+ "recovered_after_capacity": True}
+ _, payload, _ = MODULE.build_ingestion_payload(args, "review", "", [
+ {"type": "turn.failed", "error": {"message": "capacity"}},
+ {"type": "review.completed", **completion},
+ ])
+ trace = payload["batch"][0]["body"]
+ self.assertEqual(completion, trace["metadata"]["review_completion"])
+ turn = next(event["body"] for event in payload["batch"]
+ if event["body"].get("name") == "codex.turn")
+ self.assertEqual("failed", turn["output"]["status"])
+ self.assertEqual("ERROR", turn["level"])
+
def test_bounds_and_shrinks_failed_turn_status_message(self):
args = SimpleNamespace(
repository="apache/doris",
diff --git a/.github/scripts/test_resolve_code_review_status.py
b/.github/scripts/test_resolve_code_review_status.py
index 113e7cca1bf..c9576ab5085 100644
--- a/.github/scripts/test_resolve_code_review_status.py
+++ b/.github/scripts/test_resolve_code_review_status.py
@@ -110,6 +110,33 @@ class ResolveCodeReviewStatusTest(unittest.TestCase):
result = resolve_status([], [], head_sha=HEAD_SHA)
self.assertEqual("pending", result.state)
+ def test_completed_blocking_review_is_failure(self) -> None:
+ result = resolve_status([pull(123)], [status(1, source="automated",
state="failure")], head_sha=HEAD_SHA)
+ self.assertEqual("failure", result.state)
+ self.assertIn("P0/P1", result.description)
+
+ def test_local_or_skip_success_can_override_blocking_review(self) -> None:
+ for source in ("local", "skip"):
+ with self.subTest(source=source):
+ result = resolve_status([pull(123)], [status(1,
source="automated", state="failure"),
+ status(2, source=source)], head_sha=HEAD_SHA)
+ self.assertEqual("success", result.state)
+
+ def test_stale_blocking_result_is_not_reused(self) -> None:
+ for pulls in ([pull(123, base=OTHER_BASE_SHA)], [pull(124)],
[pull(123, head=OTHER_HEAD_SHA)]):
+ with self.subTest(pulls=pulls):
+ result = resolve_status(pulls, [status(1, source="automated",
state="failure")], head_sha=HEAD_SHA)
+ self.assertEqual("pending", result.state)
+
+ def test_latest_automated_result_replaces_blocker(self) -> None:
+ result = resolve_status([pull(123)], [status(1, source="automated",
state="failure"),
+ status(2, source="automated")], head_sha=HEAD_SHA)
+ self.assertEqual("success", result.state)
+
+ def test_shared_head_blocker_takes_precedence_over_pending(self) -> None:
+ result = resolve_status([pull(123), pull(124)], [status(1,
source="automated", state="failure")], head_sha=HEAD_SHA)
+ self.assertEqual("failure", result.state)
+
def test_rejects_malformed_api_data(self) -> None:
with self.assertRaisesRegex(ResolutionError, "base SHA"):
resolve_status([pull(123, base="short")], [], head_sha=HEAD_SHA)
diff --git a/.github/scripts/test_review_auth_quarantine.py
b/.github/scripts/test_review_auth_quarantine.py
index cc9b509cba7..3d8e4e53e97 100755
--- a/.github/scripts/test_review_auth_quarantine.py
+++ b/.github/scripts/test_review_auth_quarantine.py
@@ -103,6 +103,13 @@ import os
from pathlib import Path
import sys
+if os.environ.get("FAKE_FINAL_REVIEW") == "1":
+ import json, subprocess
+ request = Path(os.environ["REVIEW_CONTEXT_DIR"], "final-input.json")
+ request.write_text(json.dumps({"body": "Complete review", "comments": [],
"existing_blocking_comment_ids": []}))
+ subprocess.run([sys.executable, str(Path(os.environ["RUNNER_TEMP"],
"submit_review.py")),
+ "--context-dir", os.environ["REVIEW_CONTEXT_DIR"],
"--input-file", str(request)],
+ check=True, stdout=subprocess.DEVNULL)
print('{"type":"thread.started","thread_id":"0199a213-81c0-7800-8aa1-bbab2a035a53"}')
print(os.environ.get("FAKE_CODEX_EVENTS", ""))
print(os.environ.get("FAKE_CODEX_STDERR", ""), file=sys.stderr, flush=True)
@@ -118,10 +125,31 @@ import os
from pathlib import Path
import sys
-if sys.argv[1] == "api" and
any("/contents/.github/scripts/run_review_with_resume.py?ref=" in arg for arg
in sys.argv):
- sys.stdout.write(Path(os.environ["FAKE_REVIEW_HELPER"]).read_text())
+helper = next((arg.split("/contents/.github/scripts/", 1)[1].split("?ref=",
1)[0]
+ for arg in sys.argv if "/contents/.github/scripts/" in arg),
None)
+if sys.argv[1] == "api" and helper:
+
sys.stdout.write(Path(os.environ["FAKE_REVIEW_HELPER"]).with_name(helper).read_text())
elif sys.argv[1] == "api":
- print(json.dumps([[{"submitted_at": "2099-01-01T00:00:00Z", "commit_id":
os.environ["HEAD_SHA"]}]]))
+ root = f"repos/{os.environ['REPO']}/pulls/{os.environ['PR_NUMBER']}"
+ path = sys.argv[2]
+ saved = Path(os.environ["REVIEW_CONTEXT_DIR"], "fake-review.json")
+ if path == root:
+ print(json.dumps({"state": "open", "head": {"sha":
os.environ["HEAD_SHA"]},
+ "base": {"sha": os.environ["BASE_SHA"]}}))
+ elif path == root + "/reviews":
+ if "POST" in sys.argv:
+ payload = json.load(sys.stdin)
+ review = {"id": 99, "body": payload["body"], "commit_id":
payload["commit_id"],
+ "user": {"login": "github-actions[bot]"}, "state":
"COMMENTED",
+ "submitted_at": "2099-01-01T00:00:00Z", "html_url":
"https://example.test/review/99"}
+ saved.write_text(json.dumps(review))
+ print(json.dumps(review))
+ else:
+ print(json.dumps([[json.loads(saved.read_text())] if
saved.exists() else []]))
+ elif path.endswith("/reviews/99/comments"):
+ print("[[]]")
+ else:
+ raise AssertionError(path)
else:
Path(os.environ["FAKE_COMMENT_FILE"]).write_text(sys.argv[-1])
'''
@@ -388,12 +416,35 @@ class ReviewAuthQuarantineTest(unittest.TestCase):
def test_success_is_not_quarantined_even_with_earlier_stderr_error(self):
_, outputs = self.run_step(
- "Run automated code review", FAKE_CODEX_STATUS="0",
+ "Run automated code review", FAKE_CODEX_STATUS="0",
FAKE_FINAL_REVIEW="1",
FAKE_CODEX_EVENTS=json.dumps({"type": "turn.completed", "usage":
{}}),
FAKE_CODEX_STDERR='{"code":"refresh_token_reused"}',
)
self.assertNotIn("auth_invalid_reason", outputs)
+ def
test_capacity_after_submission_is_success_without_auth_quarantine(self):
+ _, outputs = self.run_step(
+ "Run automated code review", FAKE_FINAL_REVIEW="1",
FAKE_CODEX_STATUS="1",
+ FAKE_CODEX_EVENTS=json.dumps({"type": "turn.failed", "error": {
+ "message": "Selected model is at capacity. Please try a
different model."
+ }}),
+ )
+ self.assertIn("review_state=success", outputs)
+ self.assertNotIn("auth_invalid_reason", outputs)
+ self.assertNotIn("failure_reason", outputs)
+
+ def test_sync_separates_execution_failure_from_blocking_findings(self):
+ # Exercise the real workflow shell; record POST args instead of
writing a status.
+ for execution, verdict, expected in (("success", "success", "success"),
+ ("success", "failure", "failure"), ("failure", "success",
"pending")):
+ with self.subTest(execution=execution, verdict=verdict):
+ result = subprocess.run(["bash", "-eo", "pipefail", "-c",
+ 'gh() { printf "%s\\n" "$@"; };\n' +
re.sub(r"\$\{\{.*?\}\}", "test", step_script("Sync Code Review check for
current head"))],
+ env={**self.env, "JOB_STATUS": execution,
"REVIEW_CONTEXT_OUTCOME": "success",
+ "REVIEW_OUTCOME": execution, "REVIEW_STATE": verdict,
"REVIEW_P0": "0", "REVIEW_P1": "1"},
+ capture_output=True, text=True, check=True)
+ self.assertIn(f"state={expected}", result.stdout)
+
def test_failure_comment_reports_marker_write_outcome(self):
for outcome in ("success", "failure"):
with self.subTest(outcome=outcome):
diff --git a/.github/scripts/test_run_review_with_resume.py
b/.github/scripts/test_run_review_with_resume.py
index 352f29b423c..ea06e64284e 100755
--- a/.github/scripts/test_run_review_with_resume.py
+++ b/.github/scripts/test_run_review_with_resume.py
@@ -99,6 +99,11 @@ class ResumeReviewTest(unittest.TestCase):
mock.patch.object(runner, "check_resume_target")
)
self.help = self.enterContext(mock.patch.object(runner.subprocess,
"run"))
+ self.verification = self.enterContext(mock.patch.object(
+ runner, "verify_completion", return_value={
+ "state": "success", "p0": 0, "p1": 0, "review_id": 123,
+ }
+ ))
def write_rollout(self, *, thread_id=THREAD, cwd=None):
(self.sessions / f"rollout-2026-09-07-{thread_id}.jsonl").write_text(
@@ -155,6 +160,30 @@ class ResumeReviewTest(unittest.TestCase):
self.target_check.assert_not_called()
self.assertEqual([], self.sleeps)
+ def test_completed_turn_without_verified_delivery_fails(self):
+ self.verification.side_effect = ValueError("No final review submission
was declared")
+ self.assertEqual(1, self.execute([{"events": [thread_event(),
completed()], "status": 0}]))
+ self.assertIn("No final review submission", self.last_error())
+ self.assertFalse((self.context / "review-result.json").exists())
+
+ def test_partial_submission_at_capacity_does_not_resume_or_pass(self):
+ (self.context / "review-submission.json").write_text("{}")
+ self.verification.side_effect = ValueError("Final review inline
comments were not completely verified")
+ self.assertEqual(1, self.execute([{"events": [thread_event(),
failed()]}]))
+ self.assertEqual([], self.sleeps)
+ self.target_check.assert_not_called()
+ self.assertFalse((self.context / "review-result.json").exists())
+
+ def test_only_capacity_can_recover_after_submission(self):
+ (self.context / "review-submission.json").write_text("{}")
+ self.assertEqual(1, self.execute([{"events": [thread_event(),
failed("Other failure")]}]))
+ self.verification.assert_not_called()
+
+ def test_cancellation_after_submission_is_still_failure(self):
+ (self.context / "review-submission.json").write_text("{}")
+ self.assertEqual(1, self.execute([{"events": [thread_event(),
failed()], "status": 143}]))
+ self.verification.assert_not_called()
+
def test_unsupported_model_falls_back_before_review_work(self):
self.args.model = "gpt-6-sol"
self.args.fallback_model = "gpt-5.6-sol"
@@ -768,9 +797,9 @@ HELPER
}
"""
for minutes, setup, expected in (
- (default_minutes, 12, 7068),
- ("150", 30, 8850),
- ("120", 7201, -121),
+ (default_minutes, 12, 7056),
+ ("150", 30, 8820),
+ ("120", 7201, -7322),
):
with (
self.subTest(minutes=minutes, setup=setup),
@@ -1097,6 +1126,9 @@ else:
),
mock.patch.object(runner, "RETRY_DELAYS", (0, 0, 0)),
mock.patch.object(runner, "check_resume_target"),
+ mock.patch.object(runner, "verify_completion", return_value={
+ "state": "success", "p0": 0, "p1": 0, "review_id": 123,
+ }),
):
self.assertEqual(0, runner.run_review(args))
self.assertEqual("2", (root / "request-count").read_text())
diff --git a/.github/scripts/test_submit_review.py
b/.github/scripts/test_submit_review.py
new file mode 100644
index 00000000000..f40ad82d05b
--- /dev/null
+++ b/.github/scripts/test_submit_review.py
@@ -0,0 +1,311 @@
+#!/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.
+
+import copy
+import io
+import json
+import subprocess
+import tempfile
+import threading
+import unittest
+from contextlib import redirect_stderr
+from concurrent.futures import ThreadPoolExecutor
+from pathlib import Path
+from types import SimpleNamespace
+from unittest import mock
+
+import run_review_with_resume as runner
+import submit_review as submitter
+from resolve_code_review_status import resolve_status
+
+
+class FinalReviewTest(unittest.TestCase):
+ def setUp(self):
+ temp = tempfile.TemporaryDirectory()
+ self.addCleanup(temp.cleanup)
+ self.context = Path(temp.name)
+ self.run = {
+ "repository": "apache/doris", "pr_number": "123", "head_sha": "a"
* 40,
+ "base_sha": "b" * 40, "token": "unique-run", "started_at":
"2026-09-29T00:00:00Z",
+ }
+ self.root = "repos/apache/doris/pulls/123"
+ self.pr = {"state": "open", "number": 123,
+ "head": {"sha": self.run["head_sha"]}, "base": {"sha":
self.run["base_sha"]}}
+ self.reviews = []
+ self.comments = []
+ self.existing = []
+ self.posts = []
+ self.api = self.enterContext(mock.patch.object(submitter, "api",
side_effect=self.fake_api))
+ self.remaining = lambda: 60
+
+ def submission(self, priority=1):
+ return {"body": "All checkpoints reviewed.", "comments": [] if
priority is None else [
+ {"path": "file.py", "position": 12, "body": f"[P{priority}] Fix
the verified issue"}
+ ], "existing_blocking_comment_ids": []}
+
+ def fake_api(self, path, remaining, *, payload=None, paginated=False):
+ if path == self.root:
+ return copy.deepcopy(self.pr)
+ if path == self.root + "/reviews":
+ if payload is None:
+ return copy.deepcopy(self.reviews)
+ self.posts.append(copy.deepcopy(payload))
+ self.reviews.append({
+ "id": 99, "user": {"login": "github-actions[bot]"},
+ "commit_id": payload["commit_id"], "submitted_at":
"9999-01-01T00:00:00Z",
+ "body": payload["body"], "html_url":
"https://github.com/apache/doris/pull/123#pullrequestreview-99",
+ "state": "CHANGES_REQUESTED" if payload["event"] ==
"REQUEST_CHANGES" else "COMMENTED",
+ })
+ self.comments = [dict(c, user={"login": "github-actions[bot]"},
+ commit_id=payload["commit_id"]) for c in
payload["comments"]]
+ return copy.deepcopy(self.reviews[-1])
+ if path == self.root + "/reviews/99/comments":
+ return copy.deepcopy(self.comments)
+ if path == self.root + "/comments":
+ return copy.deepcopy(self.existing)
+ self.fail(f"Unexpected API path {path}")
+
+ def submit(self, submission=None):
+ return submitter.verify_completion(self.context, self.run,
self.remaining, submit=True,
+ submission=submission or
self.submission())
+
+ def verify(self):
+ return submitter.verify_completion(self.context, self.run,
self.remaining)
+
+ def test_submits_once_and_verifies_same_complete_review(self):
+ result = self.submit()
+ self.assertEqual("failure", result["state"])
+ self.assertEqual(1, result["p1"])
+ self.assertEqual(result, self.submit())
+ self.assertEqual(result, self.verify())
+ self.assertEqual(1, len(self.posts))
+ self.assertEqual(self.run["head_sha"], self.posts[0]["commit_id"])
+
+ def test_minor_or_empty_review_passes_without_requesting_changes(self):
+ for level in (None, 2, 3):
+ with self.subTest(priority=level):
+ self.reviews.clear()
+ (self.context /
submitter.SUBMISSION_FILE).unlink(missing_ok=True)
+ result = self.submit(self.submission(level))
+ self.assertEqual("success", result["state"])
+ self.assertEqual("COMMENT", self.posts[-1]["event"])
+
+ def
test_existing_confirmed_blocker_is_counted_without_duplicate_inline(self):
+ self.existing = [{"id": 101, "body": "[P0] Previously reported
blocker"}]
+ submission = self.submission(None)
+ submission["existing_blocking_comment_ids"] = [101]
+ result = self.submit(submission)
+ self.assertEqual((1, 0, "failure"), (result["p0"], result["p1"],
result["state"]))
+ self.assertEqual([], self.posts[0]["comments"])
+ self.assertIn("#discussion_r101", self.posts[0]["body"])
+
+ def test_invalid_existing_blocker_cannot_silently_pass(self):
+ submission = self.submission(None)
+ submission["existing_blocking_comment_ids"] = [101]
+ for comments in ([], [{"id": 101, "body": "[P2] Minor"}],
+ [{"id": 101, "body": "[P1] Reply", "in_reply_to_id":
1}]):
+ with self.subTest(comments=comments),
self.assertRaises(ValueError):
+ self.existing = comments
+ self.submit(submission)
+ self.assertEqual([], self.posts)
+
+ def test_missing_intent_cannot_use_other_reviews_or_result_file(self):
+ (self.context /
submitter.RESULT_FILE).write_text('{"state":"success"}')
+ with self.assertRaisesRegex(ValueError, "No final review submission"):
+ self.verify()
+ self.api.assert_not_called()
+
+ def
test_parallel_run_cannot_satisfy_completion_or_cause_duplicate_post(self):
+ self.submit()
+ self.run["token"] = "another-run"
+ with self.assertRaisesRegex(ValueError, "exactly one"):
+ self.verify()
+ with self.assertRaisesRegex(ValueError, "Another bot review"):
+ self.submit()
+ self.assertEqual(1, len(self.posts))
+
+ def test_simultaneous_helper_invocations_post_only_once(self):
+ barrier = threading.Barrier(2, timeout=5)
+ original_open = Path.open
+
+ def concurrent_open(path, mode="r", *args, **kwargs):
+ if path.name == submitter.SUBMISSION_FILE and mode == "x":
+ barrier.wait()
+ return original_open(path, mode, *args, **kwargs)
+
+ with mock.patch.object(Path, "open", concurrent_open),
ThreadPoolExecutor(2) as pool:
+ futures = [pool.submit(self.submit) for _ in range(2)]
+ outcomes = []
+ for future in futures:
+ try:
+ outcomes.append(future.result())
+ except FileExistsError:
+ outcomes.append(None)
+ self.assertEqual(1, sum(outcome is not None for outcome in outcomes))
+ self.assertEqual(1, len(self.posts))
+ self.assertEqual("failure", self.verify()["state"])
+
+ def test_reply_during_wrap_up_does_not_invalidate_final_inline_set(self):
+ self.submit()
+ self.comments.append({"in_reply_to_id": 1, "body": "Thanks", "path":
"file.py",
+ "user": {"login": "human"}})
+ self.assertEqual("failure", self.verify()["state"])
+
+ def test_frozen_submission_cannot_change_on_retry(self):
+ self.submit()
+ with self.assertRaisesRegex(ValueError, "different review"):
+ self.submit(self.submission(None))
+ self.assertEqual(1, len(self.posts))
+
+ def test_partial_or_modified_inline_delivery_fails(self):
+ self.submit()
+ original = copy.deepcopy(self.comments)
+ for comments in ([], original + original,
+ [dict(original[0], position=13)],
+ [dict(original[0], body="[P1] Different")],
+ [dict(original[0], user={"login": "other"})],
+ [dict(original[0], commit_id="c" * 40)]):
+ with self.subTest(comments=comments),
self.assertRaisesRegex(ValueError, "inline"):
+ self.comments = comments
+ self.verify()
+
+ def test_changed_review_identity_or_summary_cannot_pass(self):
+ self.submit()
+ original = copy.deepcopy(self.reviews[0])
+ for changes in ({"commit_id": "c" * 40}, {"user": {"login": "human"}},
+ {"submitted_at": "2020-01-01T00:00:00Z"}, {"state":
"DISMISSED"},
+ {"body": original["body"] + " changed"}):
+ with self.subTest(changes=changes),
self.assertRaisesRegex(ValueError, "did not match"):
+ self.reviews[0] = dict(original, **changes)
+ self.verify()
+
+ def test_pr_move_or_close_rejects_before_submission(self):
+ for change in ({"state": "closed"}, {"head": {"sha": "c" * 40}},
+ {"base": {"sha": "c" * 40}}):
+ original = copy.deepcopy(self.pr)
+ with self.subTest(change=change),
self.assertRaisesRegex(ValueError, "stale"):
+ self.pr.update(change)
+ self.submit()
+ self.pr = original
+ self.assertEqual([], self.posts)
+
+ def test_pr_move_during_readback_does_not_pass(self):
+ self.submit()
+ original_api = self.fake_api
+
+ def move_after_comments(path, *args, **kwargs):
+ result = original_api(path, *args, **kwargs)
+ if path.endswith("/reviews/99/comments"):
+ self.pr["head"]["sha"] = "c" * 40
+ return result
+
+ self.api.side_effect = move_after_comments
+ with self.assertRaisesRegex(ValueError, "stale"):
+ self.verify()
+
+ def test_lost_post_response_recovers_by_readback_without_duplicate(self):
+ original_api = self.fake_api
+
+ def lost_response(path, *args, **kwargs):
+ result = original_api(path, *args, **kwargs)
+ if kwargs.get("payload") is not None:
+ raise subprocess.TimeoutExpired("gh", 30)
+ return result
+
+ self.api.side_effect = lost_response
+ with self.assertRaises(subprocess.TimeoutExpired):
+ self.submit()
+ self.api.side_effect = original_api
+ self.assertEqual("failure", self.verify()["state"])
+ self.submit()
+ self.assertEqual(1, len(self.posts))
+
+ def
test_capacity_after_submission_completes_execution_and_gates_findings(self):
+ # Reproduce #66227: final write/readback, then capacity (also on the
last
+ # allowed attempt). Run the real verifier and SHA-wide status resolver.
+ for level in (None, 0, 1, 2):
+ with self.subTest(priority=level), tempfile.TemporaryDirectory()
as tmp:
+ context = Path(tmp)
+ (context / "codex_goal_prompt.txt").write_text("Review the PR")
+ args = SimpleNamespace(context_dir=context, cwd=context, **{
+ k: self.run[k] for k in ("repository", "pr_number",
"head_sha", "base_sha")
+ }, model="gpt-6-sol", effort="xhigh", budget_seconds=60)
+ self.reviews.clear()
+
+ def attempt(command, events, stderr, timeout, reaper=None):
+ run = json.loads((context /
submitter.RUN_FILE).read_text())
+ submitter.verify_completion(context, run, self.remaining,
submit=True,
+
submission=self.submission(level))
+ events.write_text(json.dumps({"type": "thread.started",
"thread_id":
+ "0199a213-81c0-7800-8aa1-bbab2a035a53"}) + "\n" +
json.dumps({
+ "type": "turn.failed", "error": {"message":
runner.CAPACITY_MESSAGE}}) + "\n")
+ stderr.write_text("")
+ return 1
+
+ with mock.patch.object(runner, "run_attempt",
side_effect=attempt), \
+ mock.patch.object(runner, "RETRY_DELAYS", ()), \
+ mock.patch.object(runner, "check_resume_target") as
guard, \
+ mock.patch.object(runner.time, "sleep") as sleep,
redirect_stderr(io.StringIO()):
+ self.assertEqual(0, runner.run_review(args))
+ guard.assert_not_called()
+ sleep.assert_not_called()
+ result = json.loads((context /
submitter.RESULT_FILE).read_text())
+ self.assertTrue(result["recovered_after_capacity"])
+ state = "failure" if level in (0, 1) else "success"
+ self.assertEqual(state, result["state"])
+ resolution = resolve_status([self.pr], [{"id": 1, "state":
result["state"],
+ "context":
f"code-review/source/automated/pr-123/base-{self.run['base_sha']}"}],
+ head_sha=self.run["head_sha"])
+ self.assertEqual(state, resolution.state)
+
+ def test_unconfirmed_post_is_never_repeated(self):
+ submission = self.submission()
+ submitter.write_json(self.context / submitter.SUBMISSION_FILE,
submission)
+ with self.assertRaisesRegex(ValueError, "refusing to repost"):
+ self.submit(submission)
+ self.assertEqual([], self.posts)
+
+ def test_api_unavailable_is_not_completion(self):
+ self.submit()
+ self.api.side_effect = subprocess.CalledProcessError(1, "gh")
+ with self.assertRaises(subprocess.CalledProcessError):
+ self.verify()
+
+ def test_schema_requires_explicit_severity_and_all_fields(self):
+ for value in ({"body": "summary"}, dict(self.submission(),
comments="no"),
+ dict(self.submission(),
existing_blocking_comment_ids=[1, 1]),
+ dict(self.submission(), comments=[{"path": "f",
"position": 1, "body": "Bug"}]),
+ dict(self.submission(), comments=[{"path": "f",
"position": 0, "body": "[P1] Bug"}])):
+ with self.subTest(value=value), self.assertRaises(ValueError):
+ self.submit(value)
+ self.assertEqual([], self.posts)
+
+
+class GitHubAPITest(unittest.TestCase):
+ def test_read_all_pages_and_bound_timeout(self):
+ with mock.patch.object(submitter.subprocess, "run",
return_value=SimpleNamespace(
+ stdout='[[{"id":1}], [{"id":2}]]'
+ )) as command:
+ self.assertEqual([{"id": 1}, {"id": 2}], submitter.api("reviews",
lambda: 7, paginated=True))
+ self.assertEqual(7, command.call_args.kwargs["timeout"])
+ self.assertIn("--paginate", command.call_args.args[0])
+ self.assertIn("--slurp", command.call_args.args[0])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/.github/workflows/code-review-runner.yml
b/.github/workflows/code-review-runner.yml
index 55c4f69e0ed..099430613d5 100644
--- a/.github/workflows/code-review-runner.yml
+++ b/.github/workflows/code-review-runner.yml
@@ -739,15 +739,14 @@ jobs:
## Final response format
- After completing the review, you MUST provide a final summary
opinion based on the rules defined in AGENTS.md and the code-review skill. The
summary must include conclusions for each applicable critical checkpoint.
- - If the overall quality of PR is good and there are no critical
blocking issues (even if there are some tolerable minor issues), submit an
opinion on approval using: gh pr review PLACEHOLDER_PR_NUMBER --comment --body
"<summary>"
- - Note that when submitting review comments in this way, the
content will not be escaped, so you need to input multi-line text with line
breaks directly, rather than using `\n`.
- - If issues found, submit a review with inline comments plus a
comprehensive summary body. Use GitHub Reviews API to ensure comments are
inline:
- - Inline comment bodies may include GitHub suggested changes
blocks when you can propose a precise patch.
- - Prefer suggested changes for small, self-contained fixes (for
example typos, trivial refactors, or narrowly scoped code corrections).
- - Do not force suggested changes for broad, architectural, or
multi-file issues; explain those normally.
- - Build a JSON array of comments like: [{ "path": "<file>",
"position": <diff_position>, "body": "..." }]
- - Submit via: gh api
repos/PLACEHOLDER_REPO/pulls/PLACEHOLDER_PR_NUMBER/reviews --input <json_file>
- - The JSON file should contain:
{"event":"REQUEST_CHANGES","body":"<summary>","comments":[...]}
+ - After the required convergence rounds and final sweep, declare the
COMPLETE final review in a JSON file with exactly these fields:
+ {"body":"<comprehensive
summary>","comments":[{"path":"<file>","position":<diff_position>,"body":"[P1]
<finding title and explanation>"}],"existing_blocking_comment_ids":[]}
+ - Every new finding must be inline and start with [P0], [P1], [P2]
or [P3]. Suggested changes are welcome for precise, self-contained fixes.
+ - Include ALL accepted new findings in comments. Use an empty array
when there are none. In existing_blocking_comment_ids, list the numeric IDs of
every already-reported P0/P1 inline finding you independently confirmed still
applies to this head. Do not duplicate those findings. Explain their
disposition in the summary; use an empty array only if none still apply.
+ - Submit only after all review completion criteria are satisfied,
using the trusted helper: python3 "$RUNNER_TEMP/submit_review.py" --context-dir
PLACEHOLDER_CONTEXT_DIR --input-file <json_file>
+ - The helper pins the head, adds this invocation's completion
marker, submits the review and verifies the entire summary and inline comment
set. P0/P1 results request changes; other completed reviews are comments. This
is the ONLY final GitHub submission path. Do not submit reviews or inline
comments directly with gh.
+ - If the helper reports an error, inspect it and verify the outcome
before proceeding. Do not change or delete its review-run.json or
review-submission.json files. A retry with the identical JSON reads back a
submitted review instead of posting it again.
+ - Mark the goal complete only after the helper reports the verified
review ID. A completed review with P0/P1 is still a completed execution; the
workflow applies the separate blocking verdict.
PROMPT
sed -i "s|PLACEHOLDER_REPO|${REPO}|g"
"$REVIEW_CONTEXT_DIR/review_prompt.txt"
@@ -859,16 +858,17 @@ jobs:
HELPER_REF: ${{ github.workflow_sha || github.sha }}
run: |
review_step_started_at=$SECONDS
- review_started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
+ for script in run_review_with_resume.py submit_review.py; do
+ gh api \
+ -H "Accept: application/vnd.github.raw" \
+
"repos/${REPO}/contents/.github/scripts/${script}?ref=${HELPER_REF}" \
+ > "$RUNNER_TEMP/$script"
+ done
helper="$RUNNER_TEMP/run_review_with_resume.py"
- gh api \
- -H "Accept: application/vnd.github.raw" \
-
"repos/${REPO}/contents/.github/scripts/run_review_with_resume.py?ref=${HELPER_REF}"
\
- > "$helper"
# Share the step's budget; reserve two minutes for process cleanup
and
- # the existing GitHub review verification. Helper download counts
too.
+ # GitHub review verification. Both helper downloads count too.
budget_seconds=$((REVIEW_TIMEOUT_MINUTES * 60 - (SECONDS -
review_step_started_at) - 120))
set +e
# GitHub-hosted runners are ephemeral. Avoid workspace-write here
because
@@ -935,32 +935,6 @@ jobs:
fi
fi
- if [ -z "$failure_reason" ]; then
- reviews_file="$REVIEW_CONTEXT_DIR/pr_reviews_after_codex.json"
- reviews_api_ok=false
- review_verified=false
- for attempt in 1 2 3 4 5 6; do
- if gh api --paginate --slurp
"repos/${REPO}/pulls/${PR_NUMBER}/reviews" > "$reviews_file"; then
- reviews_api_ok=true
- if jq -e --arg started_at "$review_started_at" --arg head_sha
"$HEAD_SHA" '
- (add // [])
- | map(select((.submitted_at // "") >= $started_at and
(.commit_id // "") == $head_sha))
- | length > 0
- ' "$reviews_file" >/dev/null; then
- review_verified=true
- break
- fi
- fi
- sleep 5
- done
-
- if [ "$review_verified" != "true" ] && [ "$reviews_api_ok" !=
"true" ]; then
- failure_reason="Codex completed, but the workflow could not
verify pull request reviews through GitHub API."
- elif [ "$review_verified" != "true" ]; then
- failure_reason="Codex completed, but no new pull request review
was submitted for the current head SHA."
- fi
- fi
-
if [ -n "$failure_reason" ]; then
{
echo "failure_reason<<EOF"
@@ -970,6 +944,12 @@ jobs:
exit 1
fi
+ # Execution succeeded even when verified findings block the PR check.
+ # This result is written only after the runner's own API readback.
+ jq -er 'select(.state == "success" or .state == "failure") |
+ "review_state=\(.state)\np0=\(.p0)\np1=\(.p1)"' \
+ "$REVIEW_CONTEXT_DIR/review-result.json" >> "$GITHUB_OUTPUT"
+
- name: Record invalid Codex auth
id: auth_invalid_record
if: ${{ always() && steps.auth.outcome == 'success' &&
steps.review.outputs.auth_invalid_reason == 'refresh_token_reused' }}
@@ -1104,6 +1084,9 @@ jobs:
REPO: ${{ github.repository }}
REVIEW_CONTEXT_OUTCOME: ${{ steps.review_context.outcome }}
REVIEW_OUTCOME: ${{ steps.review.outcome }}
+ REVIEW_STATE: ${{ steps.review.outputs.review_state }}
+ REVIEW_P0: ${{ steps.review.outputs.p0 }}
+ REVIEW_P1: ${{ steps.review.outputs.p1 }}
run: |
state="pending"
summary="Automated review did not pass for PR #${PR_NUMBER} at
${BASE_SHA:0:12}."
@@ -1111,8 +1094,13 @@ jobs:
if [ "$JOB_STATUS" = "success" ] && \
[ "$REVIEW_CONTEXT_OUTCOME" = "success" ] && \
[ "$REVIEW_OUTCOME" = "success" ]; then
- state="success"
- summary="Automated review passed for PR #${PR_NUMBER} at
${BASE_SHA:0:12}."
+ case "$REVIEW_STATE" in
+ success|failure)
+ state="$REVIEW_STATE"
+ summary="Review completed for PR #${PR_NUMBER}:
P0=${REVIEW_P0}, P1=${REVIEW_P1}."
+ ;;
+ *) echo "::error::Missing verified review verdict"; exit 1 ;;
+ esac
fi
source_context="code-review/source/automated/pr-${PR_NUMBER}/base-${BASE_SHA}"
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]