Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions scripts/merge-gate/check_bot_anchoring.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -uo pipefail

if [[ $# -lt 1 ]]; then
echo "FAILED: usage: check_bot_anchoring.sh <pr-number>"
exit 2
fi

PR_NUMBER="$1"

PR_JSON=$(gh pr view "$PR_NUMBER" --json number,headRefOid,reviews 2>/dev/null) || {
echo "FAILED: gh/network error"
exit 3
}

python3 - "$PR_JSON" << 'PYEOF'
import json, sys

try:
data = json.loads(sys.argv[1])
except json.JSONDecodeError:
print('FAILED: could not parse PR data')
sys.exit(3)

head_oid = data.get('headRefOid', '')
reviews = data.get('reviews', [])
bot_authors = {'coderabbitai', 'qodo-code-review', 'kilo-code-bot'}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Hardcoded bot author names reduce flexibility

bot_authors is a hardcoded set. If bot names change or new bots are added, this script must be edited. Consider reading from an environment variable (e.g., BOT_AUTHORS) to make it configurable without code changes.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


for r in reviews:
author = (r.get('author') or {}).get('login', '')
if author not in bot_authors:
continue
state = r.get('state', '')
if state not in ('COMMENTED', 'APPROVED', 'CHANGES_REQUESTED'):
continue
commit_oid = (r.get('commit') or {}).get('oid', '')
if commit_oid != head_oid:
continue
body = r.get('body') or ''
has_inline = r.get('includesCreatedEdit', False)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: includesCreatedEdit may not be present in GitHub REST API response

The script relies on r.get('includesCreatedEdit', False) to detect inline-only reviews. This field is not part of the standard GitHub REST API review object returned by gh pr view --json reviews. If the field is absent, inline-only bot reviews (empty body, no inline comments visible in the body field) will be missed, causing false negatives.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

if body.strip() or has_inline:
print('SUCCESS: anchored substantive bot review found')
sys.exit(0)

print('FAILED: no anchored substantive bot review found')
sys.exit(10)
PYEOF
exit $?
109 changes: 109 additions & 0 deletions scripts/merge-gate/check_fake_green.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/usr/bin/env bash
set -uo pipefail

if [[ $# -lt 1 ]]; then
echo "FAILED: usage: check_fake_green.sh <pr-number>"
exit 2
fi

PR_NUMBER="$1"

PR_JSON=$(gh pr view "$PR_NUMBER" --json number,headRefOid,reviews,comments 2>/dev/null) || {
echo "FAILED: gh/network error"
exit 3
}

REPO_JSON=$(gh repo view --json owner,name 2>/dev/null) || {
echo "FAILED: gh/network error"
exit 3
}

HEAD_OID=$(echo "$PR_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['headRefOid'])") || {
echo "FAILED: could not parse PR data"
exit 3
}

OWNER=$(echo "$REPO_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['owner']['login'])")
REPO_NAME=$(echo "$REPO_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['name'])")

STATUS_JSON=$(gh api "repos/$OWNER/$REPO_NAME/commits/$HEAD_OID/status" 2>/dev/null) || {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Deprecated /status endpoint may return empty response

The combined status endpoint (/commits/{sha}/status) is deprecated by GitHub in favor of check-runs. On newer GitHub setups or GitHub Enterprise, this endpoint may return an empty statuses array, causing the fake-green detection to miss rate-limit signals entirely. The check-runs loop below is correct, but this status check may be dead code in practice.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

echo "FAILED: gh/network error"
exit 3
}

CHECKRUNS_JSON=$(gh api "repos/$OWNER/$REPO_NAME/commits/$HEAD_OID/check-runs" 2>/dev/null) || {
echo "FAILED: gh/network error"
exit 3
}

python3 - "$PR_JSON" "$STATUS_JSON" "$CHECKRUNS_JSON" << 'PYEOF'
import json, sys

try:
pr_data = json.loads(sys.argv[1])
status_data = json.loads(sys.argv[2])
checkruns_data = json.loads(sys.argv[3])
except json.JSONDecodeError:
print('FAILED: could not parse gh response')
sys.exit(3)

reviews = pr_data.get('reviews', [])
comments = pr_data.get('comments', [])

# Check (a): success status/check with "Review rate limited"
for item in status_data.get('statuses', []):
if item.get('context') != 'CodeRabbit':
continue
if (item.get('state') or '').upper() != 'SUCCESS':
continue
desc = item.get('description') or ''
if 'Review rate limited' in desc:
print('FAILED: CodeRabbit commit status is SUCCESS but description contains "Review rate limited"')
print('Remediation: the plain "@coderabbitai review" command no-ops on a PR that was reviewed and then pushed to; only "@coderabbitai full review" forces a real pass.')
sys.exit(11)

for item in checkruns_data.get('check_runs', []):
if 'CodeRabbit' not in item.get('name', ''):
continue
conclusion = (item.get('conclusion') or '').upper()
if conclusion != 'SUCCESS':
continue
output = item.get('output') or {}
output_text = output.get('text') or output.get('title') or output.get('summary') or ''
if 'Review rate limited' in output_text:
print('FAILED: CodeRabbit check run is SUCCESS but output contains "Review rate limited"')
print('Remediation: the plain "@coderabbitai review" command no-ops on a PR that was reviewed and then pushed to; only "@coderabbitai full review" forces a real pass.')
sys.exit(11)

# Check (b): bare "Review finished" comment with no review object
cr_reviews = [
r for r in reviews
if (r.get('author') or {}).get('login') == 'coderabbitai'
and r.get('state') in ('COMMENTED', 'APPROVED', 'CHANGES_REQUESTED')
]

cr_comments = [
c for c in comments
if (c.get('author') or {}).get('login') == 'coderabbitai'
]

BARE_ACK_PATTERNS = ['Review finished', 'review finished']

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Redundant entries in BARE_ACK_PATTERNS

BARE_ACK_PATTERNS contains both 'Review finished' and 'review finished'. Since is_bare_ack already lowercases both the body and each pattern, the second entry is redundant and will never match anything the first doesn't already match.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


def is_bare_ack(body):
body_lower = body.lower()
for pat in BARE_ACK_PATTERNS:
if pat.lower() in body_lower:
return True
return False

bare_acks = [c for c in cr_comments if is_bare_ack(c.get('body') or '')]

if bare_acks and not cr_reviews:
print('FAILED: only CodeRabbit artifact is a bare acknowledgement comment with no review object attached')
print('Remediation: the plain "@coderabbitai review" command no-ops on a PR that was reviewed and then pushed to; only "@coderabbitai full review" forces a real pass.')
sys.exit(11)

print('SUCCESS: no fake-green signals detected')
sys.exit(0)
PYEOF
exit $?
165 changes: 165 additions & 0 deletions scripts/merge-gate/red_first.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#!/usr/bin/env bash
set -uo pipefail

if [[ $# -lt 1 ]]; then
echo "FAILED: usage: red_first.sh <pr-number> --paths <src paths> --tests <pytest node ids>"
exit 2
fi

PR_NUMBER="$1"
shift

SRC_PATHS=()
TEST_IDS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--paths)
shift
while [[ $# -gt 0 && "$1" != --* ]]; do
SRC_PATHS+=("$1")
shift
done
;;
--tests)
shift
while [[ $# -gt 0 && "$1" != --* ]]; do
TEST_IDS+=("$1")
shift
done
;;
*)
echo "FAILED: unknown argument: $1"
exit 2
;;
esac
done

if [[ ${#SRC_PATHS[@]} -eq 0 || ${#TEST_IDS[@]} -eq 0 ]]; then
echo "FAILED: both --paths and --tests are required"
exit 2
fi

PR_JSON=$(gh pr view "$PR_NUMBER" --json headRefOid,headRefName,baseRefName 2>/dev/null) || {
echo "FAILED: gh/network error"
exit 3
}

python3 - "$PR_JSON" -- "${SRC_PATHS[@]}" -- "${TEST_IDS[@]}" << 'PYEOF'
import json, sys, subprocess, os, tempfile

args = sys.argv[1:]
sep1 = args.index('--')
sep2 = args.index('--', sep1 + 1)

try:
data = json.loads(args[0])
except json.JSONDecodeError:
print('FAILED: could not parse PR data')
sys.exit(3)

src_paths = args[sep1 + 1:sep2]
test_ids = args[sep2 + 1:]

head_ref = data.get('headRefName', '')
base_ref = data.get('baseRefName', '')

def run(cmd, **kwargs):
return subprocess.run(cmd, capture_output=True, text=True, **kwargs)

def eprint(*a, **kw):
print(*a, file=sys.stderr, **kw)

repo_root = run(['git', 'rev-parse', '--show-toplevel'], check=True).stdout.strip()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Missing error handling for git rev-parse failure

run(['git', 'rev-parse', '--show-toplevel'], check=True) will raise subprocess.CalledProcessError if the script is not invoked inside a git repository. There is no surrounding try/except, so the script crashes with a raw Python traceback instead of printing a clean FAILED message and exiting with a controlled code.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

os.chdir(repo_root)

tmpdir = tempfile.mkdtemp(prefix='red-first-')
try:
result = run(['git', 'worktree', 'add', '-f', tmpdir, head_ref])
if result.returncode != 0:
eprint('FAILED: could not check out PR branch into worktree')
eprint(result.stderr, end='')
print('FAILED: could not check out PR branch into worktree')
sys.exit(3)
except Exception:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Dead except Exception block

The except Exception: on line 83 is unreachable because the run() helper calls subprocess.run without check=True, so it never raises exceptions for non-zero exit codes. This except clause is dead code and could mask bugs if check=True is added to the run() call inside the try block later without revisiting this error-handling strategy.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

eprint('FAILED: could not check out PR branch into worktree')
print('FAILED: could not check out PR branch into worktree')
sys.exit(3)

os.chdir(tmpdir)

# Stage 1: tests must PASS
eprint('Stage 1: running tests on PR branch...')
result = run(['uv', 'run', 'pytest'] + test_ids + ['-q'])
if result.returncode != 0:
eprint(result.stdout, end='')
eprint(result.stderr, end='')
print('FAILED: tests did not pass on PR branch')
os.chdir(repo_root)
run(['git', 'worktree', 'remove', tmpdir], capture_output=True)
sys.exit(13)

# Get merge base
try:
merge_base = run(['git', 'merge-base', head_ref, 'origin/' + base_ref], check=True).stdout.strip()
except subprocess.CalledProcessError:
try:
merge_base = run(['git', 'merge-base', head_ref, base_ref], check=True).stdout.strip()
except subprocess.CalledProcessError:
eprint('FAILED: could not find merge base')
print('FAILED: could not find merge base')
os.chdir(repo_root)
run(['git', 'worktree', 'remove', tmpdir], capture_output=True)
sys.exit(3)

# Stage 2: revert source paths to merge-base version
eprint('Stage 2: reverting source paths to merge-base...')
result = run(['git', 'checkout', merge_base, '--'] + src_paths)
if result.returncode != 0:
eprint('FAILED: could not revert source paths')
eprint(result.stderr, end='')
print('FAILED: could not revert source paths')
os.chdir(repo_root)
run(['git', 'worktree', 'remove', tmpdir], capture_output=True)
sys.exit(3)

# Stage 2 re-run: tests must FAIL
eprint('Stage 2: running tests after revert...')
result = run(['uv', 'run', 'pytest'] + test_ids + ['-q'])
if result.returncode == 0:
eprint(result.stdout, end='')
eprint(result.stderr, end='')
print('FAILED: tests passed without the fix in place')
os.chdir(repo_root)
run(['git', 'worktree', 'remove', tmpdir], capture_output=True)
sys.exit(12)

# Stage 3: restore
eprint('Stage 3: restoring source paths...')
result = run(['git', 'checkout', 'HEAD', '--'] + src_paths)
if result.returncode != 0:
eprint('FAILED: could not restore source paths')
eprint(result.stderr, end='')
print('FAILED: could not restore source paths')
os.chdir(repo_root)
run(['git', 'worktree', 'remove', tmpdir], capture_output=True)
sys.exit(3)

# Stage 3 re-run: tests must PASS
eprint('Stage 3: running tests after restore...')
result = run(['uv', 'run', 'pytest'] + test_ids + ['-q'])
if result.returncode != 0:
eprint(result.stdout, end='')
eprint(result.stderr, end='')
print('FAILED: tests did not pass after restore')
os.chdir(repo_root)
run(['git', 'worktree', 'remove', tmpdir], capture_output=True)
sys.exit(13)

# Cleanup
os.chdir(repo_root)
run(['git', 'worktree', 'remove', tmpdir], capture_output=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: No return-code check on final git worktree remove

On the success path, run(['git', 'worktree', 'remove', tmpdir], capture_output=True) on line 160 ignores the return code. If removal fails (e.g., worktree has uncommitted changes or is locked), the script still prints SUCCESS and exits 0, leaving a registered worktree behind. Check the return code and treat removal failure as an error.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


print('SUCCESS: red-first cycle completed')
sys.exit(0)
PYEOF
exit $?
3 changes: 3 additions & 0 deletions tests/fixtures/merge_gate/checkruns_empty.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"check_runs": []
}
22 changes: 22 additions & 0 deletions tests/fixtures/merge_gate/pr_anchored_head.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"number": 218,
"headRefOid": "abc123def456",
"reviews": [
{
"author": {"login": "coderabbitai"},
"state": "COMMENTED",
"body": "<details><summary>Nitpick comments (1)</summary><blockquote>\n\n**Add coverage for the new import-tracking table.**\n</blockquote></details>",
"commit": {"oid": "abc123def456"},
"includesCreatedEdit": false,
"submittedAt": "2026-07-28T12:21:45Z"
},
{
"author": {"login": "qodo-code-review"},
"state": "COMMENTED",
"body": "",
"commit": {"oid": "abc123def456"},
"includesCreatedEdit": false,
"submittedAt": "2026-07-28T12:22:07Z"
}
]
}
14 changes: 14 additions & 0 deletions tests/fixtures/merge_gate/pr_anchored_old_sha.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"number": 218,
"headRefOid": "abc123def456",
"reviews": [
{
"author": {"login": "coderabbitai"},
"state": "COMMENTED",
"body": "Review content here",
"commit": {"oid": "oldsha123456"},
"includesCreatedEdit": false,
"submittedAt": "2026-07-28T12:21:45Z"
}
]
}
12 changes: 12 additions & 0 deletions tests/fixtures/merge_gate/pr_bare_ack.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"number": 218,
"headRefOid": "abc123def456",
"reviews": [],
"comments": [
{
"author": {"login": "coderabbitai"},
"body": "Review finished",
"createdAt": "2026-07-28T12:21:48Z"
}
]
}
14 changes: 14 additions & 0 deletions tests/fixtures/merge_gate/pr_human_only.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"number": 218,
"headRefOid": "abc123def456",
"reviews": [
{
"author": {"login": "jaylfc"},
"state": "APPROVED",
"body": "LGTM",
"commit": {"oid": "abc123def456"},
"includesCreatedEdit": false,
"submittedAt": "2026-07-28T12:30:00Z"
}
]
}
Loading