diff --git a/.github/tests/merge-gate-fixtures/cr-approval-edited-after-changes-requested/comments.json b/.github/tests/merge-gate-fixtures/cr-approval-edited-after-changes-requested/comments.json
new file mode 100644
index 0000000..11e1a32
--- /dev/null
+++ b/.github/tests/merge-gate-fixtures/cr-approval-edited-after-changes-requested/comments.json
@@ -0,0 +1,9 @@
+[
+ {
+ "user": {
+ "login": "coderabbitai[bot]"
+ },
+ "body": "\n\n🚥 Pre-merge checks | ✅ 5\n",
+ "created_at": "2026-07-11T11:30:00Z"
+ }
+]
diff --git a/.github/tests/merge-gate-fixtures/cr-approval-edited-after-changes-requested/reviews.json b/.github/tests/merge-gate-fixtures/cr-approval-edited-after-changes-requested/reviews.json
new file mode 100644
index 0000000..9e80a62
--- /dev/null
+++ b/.github/tests/merge-gate-fixtures/cr-approval-edited-after-changes-requested/reviews.json
@@ -0,0 +1,22 @@
+[
+ {
+ "user": {
+ "login": "coderabbitai[bot]"
+ },
+ "state": "APPROVED",
+ "commit_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "submitted_at": "2026-07-11T10:00:00Z",
+ "last_edited_at": "2026-07-11T12:00:00Z",
+ "body": "Approval body edited after the later verdict."
+ },
+ {
+ "user": {
+ "login": "coderabbitai[bot]"
+ },
+ "state": "CHANGES_REQUESTED",
+ "commit_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "submitted_at": "2026-07-11T11:00:00Z",
+ "last_edited_at": null,
+ "body": "A later red verdict must remain authoritative."
+ }
+]
diff --git a/.github/tests/merge-gate-fixtures/cr-approval-edited-after-dismissal/comments.json b/.github/tests/merge-gate-fixtures/cr-approval-edited-after-dismissal/comments.json
new file mode 100644
index 0000000..11e1a32
--- /dev/null
+++ b/.github/tests/merge-gate-fixtures/cr-approval-edited-after-dismissal/comments.json
@@ -0,0 +1,9 @@
+[
+ {
+ "user": {
+ "login": "coderabbitai[bot]"
+ },
+ "body": "\n\n🚥 Pre-merge checks | ✅ 5\n",
+ "created_at": "2026-07-11T11:30:00Z"
+ }
+]
diff --git a/.github/tests/merge-gate-fixtures/cr-approval-edited-after-dismissal/reviews.json b/.github/tests/merge-gate-fixtures/cr-approval-edited-after-dismissal/reviews.json
new file mode 100644
index 0000000..360aef5
--- /dev/null
+++ b/.github/tests/merge-gate-fixtures/cr-approval-edited-after-dismissal/reviews.json
@@ -0,0 +1,22 @@
+[
+ {
+ "user": {
+ "login": "coderabbitai[bot]"
+ },
+ "state": "APPROVED",
+ "commit_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "submitted_at": "2026-07-11T10:00:00Z",
+ "last_edited_at": "2026-07-11T12:00:00Z",
+ "body": "Approval body edited after the later dismissal."
+ },
+ {
+ "user": {
+ "login": "coderabbitai[bot]"
+ },
+ "state": "DISMISSED",
+ "commit_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "submitted_at": "2026-07-11T11:00:00Z",
+ "last_edited_at": null,
+ "body": "A later dismissal must remain authoritative."
+ }
+]
diff --git a/.github/tests/merge-gate-fixtures/cr-changes-requested-then-approval/comments.json b/.github/tests/merge-gate-fixtures/cr-changes-requested-then-approval/comments.json
new file mode 100644
index 0000000..11e1a32
--- /dev/null
+++ b/.github/tests/merge-gate-fixtures/cr-changes-requested-then-approval/comments.json
@@ -0,0 +1,9 @@
+[
+ {
+ "user": {
+ "login": "coderabbitai[bot]"
+ },
+ "body": "\n\n🚥 Pre-merge checks | ✅ 5\n",
+ "created_at": "2026-07-11T11:30:00Z"
+ }
+]
diff --git a/.github/tests/merge-gate-fixtures/cr-changes-requested-then-approval/reviews.json b/.github/tests/merge-gate-fixtures/cr-changes-requested-then-approval/reviews.json
new file mode 100644
index 0000000..240d9e8
--- /dev/null
+++ b/.github/tests/merge-gate-fixtures/cr-changes-requested-then-approval/reviews.json
@@ -0,0 +1,22 @@
+[
+ {
+ "user": {
+ "login": "coderabbitai[bot]"
+ },
+ "state": "CHANGES_REQUESTED",
+ "commit_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "submitted_at": "2026-07-11T10:00:00Z",
+ "last_edited_at": "2026-07-11T12:00:00Z",
+ "body": "The older red verdict body was edited later."
+ },
+ {
+ "user": {
+ "login": "coderabbitai[bot]"
+ },
+ "state": "APPROVED",
+ "commit_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "submitted_at": "2026-07-11T11:00:00Z",
+ "last_edited_at": null,
+ "body": "A later approval supersedes the earlier red verdict."
+ }
+]
diff --git a/.github/tests/merge-gate-fixtures/index.json b/.github/tests/merge-gate-fixtures/index.json
index 71d5254..81be439 100644
--- a/.github/tests/merge-gate-fixtures/index.json
+++ b/.github/tests/merge-gate-fixtures/index.json
@@ -95,6 +95,18 @@
"name": "cr-approval-superseded-at-head",
"expect_green": false
},
+ {
+ "name": "cr-approval-edited-after-changes-requested",
+ "expect_green": false
+ },
+ {
+ "name": "cr-approval-edited-after-dismissal",
+ "expect_green": false
+ },
+ {
+ "name": "cr-changes-requested-then-approval",
+ "expect_green": true
+ },
{
"name": "cr-approval-dismissed-at-head",
"expect_green": false
@@ -135,6 +147,10 @@
"name": "premerge-warning-full",
"expect_green": false
},
+ {
+ "name": "premerge-full-pass-only-in-explanation",
+ "expect_green": false
+ },
{
"name": "premerge-stale-before-head",
"expect_green": false
diff --git a/.github/tests/merge-gate-fixtures/premerge-full-pass-only-in-explanation/comments.json b/.github/tests/merge-gate-fixtures/premerge-full-pass-only-in-explanation/comments.json
new file mode 100644
index 0000000..28fdde1
--- /dev/null
+++ b/.github/tests/merge-gate-fixtures/premerge-full-pass-only-in-explanation/comments.json
@@ -0,0 +1,9 @@
+[
+ {
+ "user": {
+ "login": "coderabbitai[bot]"
+ },
+ "body": "\n\n## Pre-merge checks\n| Check | Status | Details |\n| --- | --- | --- |\n| Title check | ✅ Passed | Current result |\n| Linked Issues | Pending | Previous result: ✅ Passed |\n",
+ "created_at": "2026-07-11T10:00:00Z"
+ }
+]
diff --git a/.github/tests/merge-gate-fixtures/premerge-full-pass-only-in-explanation/reviews.json b/.github/tests/merge-gate-fixtures/premerge-full-pass-only-in-explanation/reviews.json
new file mode 100644
index 0000000..f47d3a3
--- /dev/null
+++ b/.github/tests/merge-gate-fixtures/premerge-full-pass-only-in-explanation/reviews.json
@@ -0,0 +1,10 @@
+[
+ {
+ "user": {
+ "login": "coderabbitai[bot]"
+ },
+ "state": "APPROVED",
+ "commit_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "submitted_at": "2026-07-11T10:00:00Z"
+ }
+]
diff --git a/.scripts/check-merge-gates.sh b/.scripts/check-merge-gates.sh
index 57ca8c3..98df533 100755
--- a/.scripts/check-merge-gates.sh
+++ b/.scripts/check-merge-gates.sh
@@ -62,15 +62,15 @@ premerge_state="not-posted"
# CodeRabbit's verdict at the head is its LATEST verdict-bearing review there:
# an APPROVED superseded by CHANGES_REQUESTED (or dismissed) must not count.
+# Verdict chronology is submission chronology; editing an older review body
+# must not make its verdict supersede a later state transition.
cr_latest_verdict_at_head="$(jq -r --arg sha "$head_sha" '
- def effective_at:
- [.submitted_at, .last_edited_at] | map(select(. != null)) | max // "";
[.[]
| select(.user.login == "coderabbitai[bot]")
| select(.commit_id == $sha)
| select(.state == "APPROVED" or .state == "CHANGES_REQUESTED"
or .state == "DISMISSED")]
- | sort_by(effective_at) | last | .state // empty' "$reviews_json")"
+ | sort_by(.submitted_at // "") | last | .state // empty' "$reviews_json")"
cr_approved_anywhere="$(jq -r '
[.[] | select(.user.login == "coderabbitai[bot]" and .state == "APPROVED")]
@@ -100,11 +100,11 @@ cr_commented_probe="$(jq -r --arg sha "$head_sha" '
def effective_at:
[.submitted_at, .last_edited_at] | map(select(. != null)) | max // "";
([.[]
- | select(.user.login == "coderabbitai[bot]")
- | select(.commit_id == $sha)
- | select(.state == "APPROVED" or .state == "CHANGES_REQUESTED"
+ | select(.user.login == "coderabbitai[bot]")
+ | select(.commit_id == $sha)
+ | select(.state == "APPROVED" or .state == "CHANGES_REQUESTED"
or .state == "DISMISSED")]
- | sort_by(effective_at) | last | effective_at) as $verdict_at
+ | sort_by(.submitted_at // "") | last | (.submitted_at // "")) as $verdict_at
| [.[]
| select(.user.login == "coderabbitai[bot]")
| select(.commit_id == $sha)
@@ -230,18 +230,27 @@ if [[ -n "$premerge_body" ]]; then
# Header/separator rows are ignored; an unknown/pending data row is red
# even when another row passed. Requiring at least one data row keeps an
# unrecognized future shape fail-closed.
- full_check_rows="$(awk '
+ full_check_rows="$(awk -F'|' '
/^## Pre-merge checks[[:space:]]*$/ { in_section = 1; next }
in_section && /^##[[:space:]]/ { exit }
in_section && /^\|/ {
if ($0 ~ /^\|[-[:space:]:|]+\|[[:space:]]*$/) next
- if ($0 ~ /\|[[:space:]]*(Status|Result)[[:space:]]*\|/) next
+ status = $3
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", status)
+ if (status == "Status" || status == "Result") next
print
}
' <<<"$region")"
if [[ -n "$full_check_rows" && "$region" != *"❌"* &&
"$region" != *"❓"* && "$region" != *"⚠️"* ]] &&
- ! grep -qvF '✅ Passed' <<<"$full_check_rows"; then
+ awk -F'|' '
+ {
+ status = $3
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", status)
+ if (status != "✅ Passed") bad = 1
+ }
+ END { exit bad }
+ ' <<<"$full_check_rows"; then
premerge_state="green"
else
premerge_state="failed"