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"