diff --git a/.github/scripts/extract-actionable-comments.sh b/.github/scripts/extract-actionable-comments.sh new file mode 100755 index 00000000..729e61f9 --- /dev/null +++ b/.github/scripts/extract-actionable-comments.sh @@ -0,0 +1,560 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: + .github/scripts/extract-actionable-comments.sh [--repo OWNER/REPO] [--out ] [--full] + .github/scripts/extract-actionable-comments.sh [--repo OWNER/REPO] [--out ] [--full] [--all-sources] + .github/scripts/extract-actionable-comments.sh [--repo OWNER/REPO] [--out ] [--full] [--include-conversation] [--include-reviews] + +Purpose: + Extract actionable PR feedback (CodeRabbitAI + humans) from: + - PR review threads (inline review comments; diff-positioned, can become outdated) + - optionally PR conversation comments (issue comments) + - optionally PR review summaries (review bodies, e.g. "changes requested") + + Review threads are grouped by staleness; all sources support a lightweight ack + convention via: ✅ Addressed in commit + +Outputs: + - Writes a Markdown report to stdout by default. + - Also writes raw JSON + intermediate artifacts to /tmp. + +Options: + --repo OWNER/REPO Override repo (default: current repo via `gh repo view`) + --out Write the Markdown report to (also prints to stdout) + --full Include full comment bodies for actionable comments + --include-conversation Also include PR conversation (issue) comments + --include-reviews Also include PR review summaries (approve/request-changes bodies) + --all-sources Equivalent to: --include-conversation --include-reviews + +Examples: + .github/scripts/extract-actionable-comments.sh 176 + .github/scripts/extract-actionable-comments.sh 176 --out /tmp/pr-176-report.md + .github/scripts/extract-actionable-comments.sh 176 --full + .github/scripts/extract-actionable-comments.sh 176 --all-sources +EOF +} + +require_cmd() { + local cmd="$1" + command -v "$cmd" >/dev/null 2>&1 || { echo "Missing dependency: $cmd" >&2; exit 2; } +} + +fetch_paginated_json() { + local api_path="$1" + local out_json="$2" + local out_err="$3" + + local attempt=1 + local delay_s=1 + while true; do + if gh api "$api_path" --paginate > "$out_json" 2> "$out_err"; then + break + fi + + if [[ "$attempt" -ge 4 ]]; then + echo "Error: Failed to fetch GitHub API '${api_path}' after ${attempt} attempts." >&2 + echo "Troubleshooting:" >&2 + echo "- Run: gh auth status" >&2 + echo "- Check rate limits / token scopes (GH_TOKEN) and retry later" >&2 + echo "- Verify repo/PR access permissions" >&2 + echo "- Check network connectivity" >&2 + echo >&2 + echo "gh api stderr (last attempt):" >&2 + sed -n '1,200p' "$out_err" >&2 + exit 1 + fi + + sleep "$delay_s" + delay_s="$((delay_s * 2))" + attempt="$((attempt + 1))" + done + + if ! jq -e . "$out_json" >/dev/null 2>&1; then + echo "Error: GitHub API returned invalid JSON in: ${out_json}" >&2 + echo "gh api stderr:" >&2 + sed -n '1,200p' "$out_err" >&2 + exit 1 + fi +} + +PR_NUMBER="${1:-}" +shift || true # Prevent set -e exit when $1 is absent (no remaining args to shift). +if [[ -z "${PR_NUMBER}" ]]; then + usage >&2 + exit 2 +fi +if ! [[ "${PR_NUMBER}" =~ ^[0-9]+$ ]]; then + echo "Error: PR_NUMBER must be numeric, got: ${PR_NUMBER}" >&2 + usage >&2 + exit 2 +fi + +REPO="" +OUT="" +FULL=0 +INCLUDE_CONVERSATION=0 +INCLUDE_REVIEWS=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --repo) + if [[ -z "${2:-}" || "${2:-}" == -* ]]; then + echo "Error: --repo requires a value in the form OWNER/REPO" >&2 + usage >&2 + exit 2 + fi + REPO="$2" + shift 2 + ;; + --out) + if [[ -z "${2:-}" || "${2:-}" == -* ]]; then + echo "Error: --out requires a filesystem path" >&2 + usage >&2 + exit 2 + fi + OUT="$2" + shift 2 + ;; + --full) + FULL=1 + shift + ;; + --include-conversation) + INCLUDE_CONVERSATION=1 + shift + ;; + --include-reviews) + INCLUDE_REVIEWS=1 + shift + ;; + --all-sources) + INCLUDE_CONVERSATION=1 + INCLUDE_REVIEWS=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +require_cmd gh +require_cmd jq + +if [[ -z "$REPO" ]]; then + REPO="$(gh repo view --json nameWithOwner --jq '.nameWithOwner')" +fi + +OWNER="${REPO%/*}" +NAME="${REPO#*/}" +if [[ "$REPO" != */* || "$REPO" == */*/* || -z "$OWNER" || -z "$NAME" || "$OWNER" == "$NAME" ]]; then + echo "Error: Invalid repo format '${REPO}'. Expected OWNER/REPO." >&2 + exit 2 +fi + +HEAD_SHA="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefOid --jq '.headRefOid')" +if [[ -z "$HEAD_SHA" || ! "$HEAD_SHA" =~ ^[0-9a-f]{7,}$ ]]; then + echo "Error: Failed to determine PR head commit SHA for ${REPO}#${PR_NUMBER}" >&2 + exit 1 +fi +HEAD7="${HEAD_SHA:0:7}" + +TS="$(date +%s)-$$" +RAW_COMMITS="/tmp/pr-${PR_NUMBER}-commits-${TS}.json" +RAW_COMMITS_ERR="/tmp/pr-${PR_NUMBER}-commits-${TS}.err" +RAW_REVIEW="/tmp/pr-${PR_NUMBER}-review-comments-${TS}.json" +RAW_REVIEW_ERR="/tmp/pr-${PR_NUMBER}-review-comments-${TS}.err" +LATEST_REVIEW="/tmp/pr-${PR_NUMBER}-review-latest-${TS}.json" +RAW_CONVERSATION="/tmp/pr-${PR_NUMBER}-conversation-comments-${TS}.json" +RAW_CONVERSATION_ERR="/tmp/pr-${PR_NUMBER}-conversation-comments-${TS}.err" +LATEST_CONVERSATION="/tmp/pr-${PR_NUMBER}-conversation-latest-${TS}.json" +RAW_REVIEWS="/tmp/pr-${PR_NUMBER}-reviews-${TS}.json" +RAW_REVIEWS_ERR="/tmp/pr-${PR_NUMBER}-reviews-${TS}.err" +LATEST_REVIEWS="/tmp/pr-${PR_NUMBER}-reviews-latest-${TS}.json" +LATEST_ALL="/tmp/pr-${PR_NUMBER}-latest-${TS}.json" +REPORT="/tmp/pr-${PR_NUMBER}-report-${TS}.md" + +fetch_paginated_json "repos/${OWNER}/${NAME}/pulls/${PR_NUMBER}/commits" "$RAW_COMMITS" "$RAW_COMMITS_ERR" +VALID_COMMITS_JSON="$(jq -c '[ .[] | (.sha // "")[0:7] | select(length == 7) ] | unique' "$RAW_COMMITS")" +if [[ -z "$VALID_COMMITS_JSON" || "$VALID_COMMITS_JSON" == "[]" ]]; then + VALID_COMMITS_JSON="$(jq -nc --arg head "$HEAD7" '[ $head ]')" +fi + +fetch_paginated_json "repos/${OWNER}/${NAME}/pulls/${PR_NUMBER}/comments" "$RAW_REVIEW" "$RAW_REVIEW_ERR" +if [[ "$INCLUDE_CONVERSATION" -eq 1 ]]; then + fetch_paginated_json "repos/${OWNER}/${NAME}/issues/${PR_NUMBER}/comments" "$RAW_CONVERSATION" "$RAW_CONVERSATION_ERR" +fi +if [[ "$INCLUDE_REVIEWS" -eq 1 ]]; then + fetch_paginated_json "repos/${OWNER}/${NAME}/pulls/${PR_NUMBER}/reviews" "$RAW_REVIEWS" "$RAW_REVIEWS_ERR" +fi + +# Normalize review-thread comments (top-level only) and detect ack markers in replies. +FILTER_COMMON="/tmp/pr-${PR_NUMBER}-jq-common-${TS}.jq" +FILTER_REVIEW="/tmp/pr-${PR_NUMBER}-jq-review-thread-${TS}.jq" +FILTER_CONVERSATION="/tmp/pr-${PR_NUMBER}-jq-conversation-${TS}.jq" +FILTER_REVIEWS="/tmp/pr-${PR_NUMBER}-jq-review-summaries-${TS}.jq" +FILTER_REVIEW_FULL="/tmp/pr-${PR_NUMBER}-jq-review-thread-full-${TS}.jq" +FILTER_CONVERSATION_FULL="/tmp/pr-${PR_NUMBER}-jq-conversation-full-${TS}.jq" +FILTER_REVIEWS_FULL="/tmp/pr-${PR_NUMBER}-jq-review-summaries-full-${TS}.jq" + +cat > "$FILTER_COMMON" <<'JQ' +def is_bot_user(u): + (u | type) == "object" + and ( + (u.type // "") == "Bot" + or ((u.login // "") | endswith("[bot]")) + ); + +# An ack marker is considered valid only when: +# - authored by a non-bot user (prevents false positives from CodeRabbit templates), and +# - includes a commit SHA that is actually part of the PR (reduces accidental matches). +def ack_commit(body): + if (body | type) != "string" then null + else + (try + (body + | capture("(?m)^[\\s>]*✅ Addressed in commit (?[0-9a-f]{7,40})\\b") + | .commit + | ascii_downcase + | .[0:7] + ) + catch null) + end; + +def has_ack_marker(body; user): + (is_bot_user(user) | not) + and (ack_commit(body) as $c + | $c != null + and ($valid_commits | index($c)) != null + ); + +def normalize_title(body): + ((body + | split("\n") + | map(select(. != "")) + | .[0] // "UNTITLED" + ) + | gsub("\\*\\*"; "") + | .[0:80]); + +def priority_from_body(body): + if (body | test("\\bP0\\b|badge/P0-|🔴|Critical"; "i")) then "P0" + elif (body | test("\\bP1\\b|badge/P1-|🟠|Major"; "i")) then "P1" + elif (body | test("\\bP2\\b|badge/P2-|🟡|Minor"; "i")) then "P2" + else "P3" + end; + +def likely_actionable(body): + (body | type) == "string" + and (body | test("\\bP[0-3]\\b|\\bTODO\\b|\\bFIXME\\b|\\bnit\\b|suggest|\\bshould\\b|\\bconsider\\b|blocker|\\bbug\\b|error|fail|typo|rename|missing|clarify|doc(s|ument)?|\\btests?\\b|panic|crash|security|\\bdetermin"; "i")); +JQ + +cat > "$FILTER_REVIEW" <<'JQ' +# Replies are returned in the same list, with `in_reply_to_id` set. +def ack_by_reply: + reduce .[] as $c ({}; if ($c.in_reply_to_id != null and has_ack_marker($c.body; $c.user)) then .[($c.in_reply_to_id | tostring)] = true else . end); + +(ack_by_reply) as $replies | +[ .[] + | select(.in_reply_to_id == null) + | { + id, + author: (.user.login // "unknown"), + author_is_bot: ( + (.user.type // "") == "Bot" + or ((.user.login // "") | endswith("[bot]")) + ), + url: .html_url, + source: "review_thread", + path, + line, + position, + original_position, + head_commit: $head, + comment_commit: (.commit_id[0:7]), + original_commit: (.original_commit_id[0:7]), + is_on_head: (.commit_id[0:7] == $head), + is_visible_on_head_diff: (.position != null), + is_outdated: (.position == null), + is_moved: (.commit_id != .original_commit_id), + has_ack: ($replies[(.id | tostring)] // false), + is_actionable: true, + priority: priority_from_body(.body), + title: normalize_title(.body), + body: .body + } +] +JQ + +cat "$FILTER_COMMON" "$FILTER_REVIEW" > "$FILTER_REVIEW_FULL" +jq --arg head "$HEAD7" --argjson valid_commits "$VALID_COMMITS_JSON" -f "$FILTER_REVIEW_FULL" "$RAW_REVIEW" > "$LATEST_REVIEW" + +if [[ "$INCLUDE_CONVERSATION" -eq 1 ]]; then + cat > "$FILTER_CONVERSATION" <<'JQ' +def is_html_comment(body): + (body | type) == "string" + and (body | test("^\\s* + +# Procedure: Extract Actionable Comments from PR Review Threads (CodeRabbitAI + Humans) + +This procedure is part of the required PR workflow for this repo. + +GitHub carries forward review comments across commits, so you must extract only the **currently actionable** feedback (not already-fixed or stale comments) before starting another fix batch. + +--- + +## Expected Workflow Context (Where This Fits) + +When you finish work: + +1. Push a branch and open a PR. +2. Wait for CI + CodeRabbitAI. +3. Extract actionable comments (this doc). +4. Fix issues in small commits + push. +5. Repeat until CodeRabbitAI (and any required human reviewer) approves. + +--- + +## Prerequisites + +- `gh` installed and authenticated +- `jq` installed (minimum: `jq >= 1.6`) +- Repo access to view PRs + +--- + +## Procedure + +### Quick Recommendation + +Prefer the repo automation whenever possible: + +```bash +.github/scripts/extract-actionable-comments.sh --full +``` + +It is designed to: + +- include review comments from **all** authors (CodeRabbitAI *and* human reviewers), +- include “outdated” comments that are not visible on the current head diff, +- detect `✅ Addressed in commit ...` markers in replies (authored by a human), +- and produce a deterministic Markdown report. + +To widen the net beyond inline review threads, you can include: + +- PR conversation comments (top-level PR timeline discussion), and +- review summaries (approve / changes-requested review bodies). + +```bash +.github/scripts/extract-actionable-comments.sh --all-sources --full +``` + +Note: +- Conversation comments and review summaries are not diff-positioned like review threads, so the script applies a simple “likely actionable” heuristic and emits a separate “Unclassified” bucket for anything that doesn’t match. +- For those non-thread sources, the script treats **human-authored** items as actionable; bot-authored summaries/status comments are included for context but are not counted as “Needs attention” (they can’t be reliably acked the same way review-thread replies can). + +### Step 1: Identify the PR head commit (the current diff) + +```bash +PR_NUMBER="" +LATEST_COMMIT="$(gh pr view "$PR_NUMBER" --json headRefOid --jq '.headRefOid[0:7]')" +echo "PR head commit: $LATEST_COMMIT" +``` + +Why: comment staleness is measured relative to the current PR head. + +--- + +### Step 2: Fetch all review comments (PR review threads) + +```bash +OWNER="" +REPO="" +TMPFILE="/tmp/pr-${PR_NUMBER}-comments-$(date +%s).json" +gh api "repos/${OWNER}/${REPO}/pulls/${PR_NUMBER}/comments" --paginate > "$TMPFILE" +``` + +Note: +- This endpoint returns PR review comments authored by anyone (humans, bots, CodeRabbitAI, etc.). + +--- + +### Step 3: Extract top-level review comments (including “outdated”) + +Important: + +- GitHub’s review comments API (`/pulls/:number/comments`) keeps each comment’s `commit_id` fixed to the commit it was authored on. +- When the PR head moves, older unresolved comments usually become **outdated** rather than being re-bound to the new head. +- If you filter only to `commit_id == PR_HEAD`, you can incorrectly report “0 actionables” while older threads remain open. + +```bash +cat "$TMPFILE" | jq --arg head "$LATEST_COMMIT" ' + .[] | + select(.in_reply_to_id == null) | + { + id, + line, + path, + position, + head_commit: $head, + comment_commit: .commit_id[0:7], + original_commit: .original_commit_id[0:7], + is_visible_on_head_diff: (.position != null), + is_outdated: (.position == null), + is_moved: (.commit_id != .original_commit_id), + created_at, + body_preview: (.body[0:200]) + } +' | jq -s '.' > /tmp/comments-latest.json +``` + +--- + +### Step 4: Bucket on-head vs outdated (and verify against current code) + +```bash +cat /tmp/comments-latest.json | jq ' + group_by(.is_outdated) | + map({ + category: (if .[0].is_outdated then "OUTDATED (earlier commit)" else "ON_HEAD" end), + count: length, + comments: map({id, line, path, position, comment_commit, original_commit}) + }) +' +``` + +Key insight: +- “Outdated” means “not visible on the current head diff”, **not** “fixed”. +- Always verify against current code before acting (see Step 7). + +--- + +### Step 5: Detect “Already Addressed” markers + +The “✅ Addressed in commit …” marker is a lightweight ack convention used to prevent re-triaging the same comments. + +Important: **do not** treat a bare substring match as reliable. + +- Review bots (including CodeRabbitAI) may include the exact string as a template/example in their own review text. +- If you count those as “acknowledged”, you can incorrectly report “0 actionables” and miss real work. + +For **review threads**, prefer a human-authored reply containing a commit SHA that is actually part of the PR. + +For **PR conversation comments** and **review summaries**, you may also use the marker in your own comment body (or edit), but only treat it as acknowledged when the marker includes a real PR commit SHA. + +If you want reliable ack detection, prefer the repo script: + +```bash +.github/scripts/extract-actionable-comments.sh +``` + +```bash +cat "$TMPFILE" | jq ' + # Very rough, but safer than substring matching: + # - only count replies (in_reply_to_id != null) + # - only count markers that start a line and include a hex SHA + [ .[] + | select(.in_reply_to_id != null) + | select(.body | test("(?m)^\\s*✅ Addressed in commit [0-9a-f]{7,40}\\b")) + | { in_reply_to_id, reply_id: .id, user: (.user.login // "unknown"), body: (.body[0:80]) } + ] +' +``` + +Key insight: +- Explicit acks are only useful when they can’t be accidentally “spoofed” by templated bot text. + +--- + +### Step 6: Categorize by priority (optional) + +This is only useful if CodeRabbitAI uses explicit priority markers in comment bodies. + +```bash +cat "$TMPFILE" | jq --arg head "$LATEST_COMMIT" ' + .[] | + select( + .in_reply_to_id == null + ) | + { + id, + line, + path, + priority: ( + if (.body | test("\\bP0\\b|badge/P0-|🔴|Critical"; "i")) then "P0" + elif (.body | test("\\bP1\\b|badge/P1-|🟠|Major"; "i")) then "P1" + elif (.body | test("\\bP2\\b|badge/P2-|🟡|Minor"; "i")) then "P2" + else "P3" + end + ), + is_visible_on_head_diff: (.position != null), + is_outdated: (.position == null), + body + } +' | jq -s '.' > /tmp/prioritized-comments.json +``` + +--- + +### Step 7: Verify outdated comments against current code (critical step) + +Do not trust `is_outdated` alone. Verify by mapping fields from the comment object to concrete commands. + +Suggested mapping: + +- `path` → file path +- `line` → line number (use a small context window, e.g. ±5 lines) + +Example: + +```bash +# Suppose you have a single comment object (e.g., from /tmp/comments-latest.json): +COMMENT_PATH="docs/decision-log.md" +COMMENT_LINE=42 + +# Clamp the start line to 1 (sed doesn't like 0/negative ranges). +START=$((COMMENT_LINE - 5)) +if [[ "$START" -lt 1 ]]; then START=1; fi +END=$((COMMENT_LINE + 5)) + +# 1) Inspect current state around the line +git show "HEAD:${COMMENT_PATH}" | sed -n "${START},${END}p" + +# 2) Scan recent history for related fixes +git log --all --oneline -- "${COMMENT_PATH}" | head -20 + +# 3) If the comment mentions a specific identifier (function/struct name), search by token +git log -p --all -S"SomeIdentifierFromComment" -- "${COMMENT_PATH}" | head -80 +``` + +If the comment is outdated (not visible on head diff), it may refer to old line numbers. In that case: + +- search by keyword/token rather than trusting the line number, and +- look up the original code context via `original_commit_id` if needed. + +```bash +# Use the repo script if you want the comment bodies included: +.github/scripts/extract-actionable-comments.sh --full +``` + +--- + +### Step 8: Produce an issue report (batch) + +Create a batch checklist and work top-down: + +```bash +cat > /tmp/batch-N-issues.md << 'EOF' +# Batch N - CodeRabbitAI Issues + +## Outdated (Verify / Already Fixed) +- [ ] Line XXX - Issue description (Fixed in: COMMIT_SHA) + +## P0 Critical +- [ ] Line XXX - Issue description + +## P1 Major +- [ ] Line XXX - Issue description + +## P2 Minor +- [ ] Line XXX - Issue description + +## P3 Trivial +- [ ] Line XXX - Issue description +EOF +``` + +--- + +### Step 9: Save full bodies for needs-attention issues + +Prefer the helper script, which understands ack markers in replies and can print full bodies: + +```bash +.github/scripts/extract-actionable-comments.sh --full +``` + +--- + +## When CodeRabbitAI approval doesn’t unblock GitHub + +If CodeRabbitAI approved but GitHub still shows “changes requested”, nudge the bot: + +```text +@coderabbitai Please re-review the latest commit and submit a new approval to update the review status. +``` + +Note: CodeRabbitAI can’t “dismiss” its own prior review; it updates the PR status by submitting a new review after re-reviewing the latest commit. + +--- + +## Automation + +Use the helper script: + +- `.github/scripts/extract-actionable-comments.sh` diff --git a/docs/procedures/PR-SUBMISSION-REVIEW-LOOP.md b/docs/procedures/PR-SUBMISSION-REVIEW-LOOP.md new file mode 100644 index 00000000..09dcb352 --- /dev/null +++ b/docs/procedures/PR-SUBMISSION-REVIEW-LOOP.md @@ -0,0 +1,127 @@ + + +# Procedure: PR Submission + CodeRabbitAI Review Loop + +This document defines the required end-to-end submission workflow for this repo. + +It is deliberately operational: follow it step-by-step and avoid “interpretation drift”. + +--- + +## Rules (Non‑Negotiable) + +1. No direct-to-`main` commits. +2. No admin bypass merges to skip required reviews. +3. CI green is required but not sufficient — review approval is a separate gate. +4. Iterate in small commits to reduce review ambiguity. +5. Every PR must reference a GitHub Issue in the PR body with closing keywords (e.g., `Closes #123`). + +--- + +## Workflow (Branch → PR → Review → Fix → Merge) + +### Step 0 — Start on a branch + +Prefer a clear prefix: + +- `docs/...` for docs-only changes +- `feat/...` for features +- `fix/...` for bug fixes +- `chore/...` for tooling/maintenance + +```bash +git checkout -b +``` + +--- + +### Step 1 — Push and open a PR + +```bash +git push -u origin +gh pr create --base main --head +``` + +--- + +### Step 2 — Wait for CI and CodeRabbitAI + +Watch checks: + +```bash +gh pr checks --watch +``` + +Then wait for CodeRabbitAI to comment. Do not merge “because CI is green”. + +If CodeRabbitAI doesn’t respond within a reasonable time (or you see a failing status like “Review rate limit exceeded”): + +1. Check PR checks for rate limit/error details. +2. Post `@coderabbitai review` on the PR to re-trigger. +3. If it still fails, wait and retry (or escalate to repo admins if it persists). + +--- + +### Step 3 — Extract actionable review feedback (required) + +Use: + +- `docs/procedures/EXTRACT-PR-COMMENTS.md` + +The outcome of this step should be a bucketed list of actionable items (P0/P1/P2/P3). + +--- + +### Step 4 — Fix issues in batches (commit + push) + +Work one bucket at a time: + +- P0: correctness / determinism / security (CodeRabbitAI: 🔴 Critical) +- P1: major design/API drift (CodeRabbitAI: 🟠 Major) +- P2: minor issues / maintainability (CodeRabbitAI: 🟡 Minor) +- P3: nits (CodeRabbitAI: 🔵 Trivial / nitpicks) + +For each batch: + +```bash +git commit -m "fix: " +git push +``` + +When replying in threads, prefer: + +> ✅ Addressed in commit `abc1234` + +This reduces stale-comment confusion in later rounds. + +--- + +### Step 5 — Repeat until approved + +Repeat Steps 2–4 until: + +- CI checks are green, and +- CodeRabbitAI is satisfied (approved or no unresolved actionables), and +- any required human reviewer has approved. + +--- + +### Step 6 — Merge only when approved + +If branch protection requires it, enable auto-merge: + +```bash +gh pr merge --auto --merge +``` + +--- + +## If CodeRabbitAI approved but GitHub stays blocked + +Sometimes the “changes requested” status lingers even after an approving review. + +Post this comment on the PR: + +```text +@coderabbitai Please review the latest commit and clear the "changes requested" status since you have already approved the changes. +```