Skip to content

GH#17503: protect executable template code blocks from simplification #58531

GH#17503: protect executable template code blocks from simplification

GH#17503: protect executable template code blocks from simplification #58531

name: Review Bot Gate
# Blocks merge until AI code review bots have posted their reviews.
# Process gap: PRs were merged before CodeRabbit/Gemini posted findings,
# losing security-relevant feedback. This workflow polls for bot reviews
# and only passes when at least one configured bot has reviewed.
#
# To enforce: add "review-bot-gate" as a required status check in
# GitHub branch protection settings for each repo.
#
# t1382: https://github.com/marcusquinn/aidevops/issues/2735
on:
pull_request:
types: [opened, synchronize, reopened]
pull_request_review:
types: [submitted]
issue_comment:
types: [created]
# Do NOT cancel in-progress runs. When cancel-in-progress was true, the initial
# pull_request:opened run was cancelled by the issue_comment:created event (fired
# when a bot posted). The cancelled run left a permanent stale CANCELLED check on
# the PR, making mergeStateStatus UNSTABLE even though the re-triggered run passed.
# With cancel-in-progress: false, both runs complete — the first passes early
# (PR too young) and the second confirms the bot posted. GH#2973.
concurrency:
group: review-bot-gate-${{ github.event.pull_request.number || github.event.issue.number }}
cancel-in-progress: false
jobs:
review-bot-gate:
runs-on: ubuntu-latest
# Only run on PRs (issue_comment fires for PR comments too).
# GH#4002: Skip pull_request_review events from known review bots.
# GitHub requires manual approval for workflow runs triggered by bot
# accounts on pull_request_review and pull_request_review_comment events,
# causing permanent action_required status on PRs. The issue_comment
# event from bots does NOT require approval, so we allow it — this is
# the primary re-trigger path when a bot posts its review as a comment.
if: >
(
github.event_name == 'pull_request' ||
github.event_name == 'pull_request_review' ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request)
) && !(
github.event_name == 'pull_request_review' && (
github.actor == 'coderabbitai' ||
github.actor == 'gemini-code-assist[bot]' ||
github.actor == 'augment-code[bot]' ||
github.actor == 'augmentcode[bot]' ||
github.actor == 'copilot[bot]' ||
github.actor == 'github-actions[bot]' ||
github.actor == 'dependabot[bot]'
)
)
permissions:
pull-requests: read
contents: read
statuses: read
steps:
- name: Check for AI review bot activity
id: check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
REPO: ${{ github.repository }}
run: |
echo "Checking PR #${PR_NUMBER} for AI review bot activity..."
# Known review bot patterns (case-insensitive matching on login).
# Add new bots here as they are configured.
# NOTE: This list differs from the job-level if-condition which also
# excludes github-actions[bot] and dependabot[bot]. Those bots are
# excluded from triggering this workflow but are NOT code review bots
# whose reviews we wait for.
KNOWN_BOTS=(
"coderabbitai"
"gemini-code-assist[bot]"
"augment-code[bot]"
"augmentcode[bot]"
"copilot[bot]"
)
# Patterns indicating rate-limit/quota notices (not real reviews).
# GH#2980: bots post these when rate-limited — must not count as reviews.
RATE_LIMIT_PATTERNS=(
"rate limit exceeded"
"rate limited by coderabbit"
"daily quota limit"
"reached your daily quota"
"Please wait up to 24 hours"
"has exceeded the limit for the number of"
)
# Minimum wait time (seconds) after PR creation before we check
# Gives bots time to start their analysis
MIN_WAIT_SECONDS=120
# GH#3827: Grace period for rate-limited bots. If bots posted
# rate-limit notices but the PR has been open longer than this,
# pass the gate with a warning instead of blocking indefinitely.
# Default: 4 hours (14400s). Prevents systemic rate-limiting from
# blocking all PRs when many are opened in a short window.
RATE_LIMIT_GRACE_SECONDS=14400
# Get PR creation time
PR_CREATED=$(gh pr view "$PR_NUMBER" --repo "$REPO" \
--json createdAt -q '.createdAt')
PR_CREATED_EPOCH=$(date -d "$PR_CREATED" +%s 2>/dev/null || \
date -j -f "%Y-%m-%dT%H:%M:%SZ" "$PR_CREATED" +%s 2>/dev/null || \
echo "0")
NOW_EPOCH=$(date +%s)
ELAPSED=$((NOW_EPOCH - PR_CREATED_EPOCH))
echo "PR created: ${PR_CREATED} (${ELAPSED}s ago)"
if [[ "$ELAPSED" -lt "$MIN_WAIT_SECONDS" ]]; then
echo "::warning::PR is only ${ELAPSED}s old (minimum ${MIN_WAIT_SECONDS}s). Review bots may not have posted yet."
fi
# Helper: check if a comment body is a rate-limit notice
is_rate_limit_comment() {
local body="$1"
for pattern in "${RATE_LIMIT_PATTERNS[@]}"; do
if echo "$body" | grep -qi "$pattern"; then
return 0
fi
done
return 1
}
# Helper: check if a bot has at least one real review (not rate-limited)
# Uses jq to select comments by bot login and extract bodies.
# Base64-encodes each body so multi-line content stays on one line.
bot_has_real_review() {
local bot_base="$1"
local jq_filter
jq_filter=".[] | select(.user.login | ascii_downcase | test(\"${bot_base}\")) | .body | @base64"
local source encoded_bodies encoded body
for source in \
"repos/${REPO}/pulls/${PR_NUMBER}/reviews" \
"repos/${REPO}/issues/${PR_NUMBER}/comments" \
"repos/${REPO}/pulls/${PR_NUMBER}/comments"; do
encoded_bodies=$(gh api "$source" --paginate --jq "$jq_filter" 2>/dev/null || echo "")
if [[ -n "$encoded_bodies" ]]; then
while IFS= read -r encoded; do
[[ -z "$encoded" ]] && continue
body=$(echo "$encoded" | base64 -d 2>/dev/null || echo "")
[[ -z "$body" ]] && continue
if ! is_rate_limit_comment "$body"; then
return 0
fi
done <<< "$encoded_bodies"
fi
done
return 1
}
# Helper: check if any bot posted a SUCCESS commit status check.
# GH#3005: When bots are rate-limited in comments but still post a
# formal GitHub status check, treat the PR as reviewed.
# GH#3007: The status context name may differ from the bot login.
# E.g., bot login "coderabbitai" but status context "CodeRabbit".
# Match bidirectionally: bot_base starts with context OR context
# starts with bot_base (case-insensitive). This handles both
# "coderabbitai" matching "coderabbit" and vice versa.
# Uses the PR head SHA to query commit statuses.
any_bot_has_success_status() {
local head_sha
head_sha=$(gh pr view "$PR_NUMBER" --repo "$REPO" \
--json headRefOid -q '.headRefOid' 2>/dev/null || echo "")
if [[ -z "$head_sha" ]]; then
return 1
fi
# Get the combined status (singular endpoint = latest per-context state)
# and check-runs unconditionally, then merge both streams.
# GH#3007: /status (singular) returns the latest state per context,
# avoiding stale-success matches from /statuses (plural, full history).
# Pagination ensures we don't miss contexts when >30 statuses exist.
local statuses check_runs
statuses=$(gh api "repos/${REPO}/commits/${head_sha}/status?per_page=100" \
--paginate --jq '.statuses[] | select(.state == "success") | .context' \
2>/dev/null || echo "")
check_runs=$(gh api "repos/${REPO}/commits/${head_sha}/check-runs?per_page=100" \
--paginate --jq '.check_runs[] | select(.conclusion == "success") | .name' \
2>/dev/null || echo "")
statuses=$(printf '%s\n%s\n' "$statuses" "$check_runs" | grep -v '^$' || true)
if [[ -z "$statuses" ]]; then
return 1
fi
local statuses_lower
statuses_lower=$(echo "$statuses" | tr '[:upper:]' '[:lower:]')
# Check if any known bot has a success status.
# GH#3007: Match bidirectionally — the status context may be a
# prefix of the bot login (e.g., "coderabbit" vs "coderabbitai")
# or the bot login may be a prefix of the context.
local bot bot_base ctx
for bot in "${KNOWN_BOTS[@]}"; do
bot_base=$(echo "$bot" | tr '[:upper:]' '[:lower:]' | sed 's/\[bot\]$//')
while IFS= read -r ctx; do
[[ -z "$ctx" ]] && continue
# Bidirectional prefix match: either string starts with the other
if [[ "$bot_base" == "$ctx"* ]] || [[ "$ctx" == "$bot_base"* ]]; then
echo "Bot '${bot}' has SUCCESS status check on commit ${head_sha:0:8} (context: '${ctx}')"
return 0
fi
done <<< "$statuses_lower"
done
return 1
}
# Check PR reviews (formal GitHub reviews from bots)
echo ""
echo "=== Checking PR reviews ==="
REVIEWS=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/reviews" \
--paginate --jq '.[].user.login' 2>/dev/null || echo "")
# Check PR comments (some bots post as issue comments, not reviews)
echo "=== Checking PR comments ==="
COMMENTS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
--paginate --jq '.[].user.login' 2>/dev/null || echo "")
# Check review comments (inline code comments)
echo "=== Checking review comments ==="
REVIEW_COMMENTS=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/comments" \
--paginate --jq '.[].user.login' 2>/dev/null || echo "")
# Combine all commenters
ALL_COMMENTERS=$(echo -e "${REVIEWS}\n${COMMENTS}\n${REVIEW_COMMENTS}" | \
sort -u | tr '[:upper:]' '[:lower:]')
echo ""
echo "All commenters found:"
echo "$ALL_COMMENTERS" | grep -v '^$' | sed 's/^/ - /'
# Check which known bots have posted REAL reviews (not rate-limit notices)
# GH#2980: bots posting rate-limit notices were incorrectly counted as reviews
FOUND_BOTS=""
MISSING_BOTS=""
RATE_LIMITED_BOTS=""
for bot in "${KNOWN_BOTS[@]}"; do
# Use grep pattern matching (bot patterns may contain [bot] suffix)
bot_lower=$(echo "$bot" | tr '[:upper:]' '[:lower:]')
# Strip [bot] for matching since GitHub may or may not include it
bot_base=$(echo "$bot_lower" | sed 's/\[bot\]$//')
if echo "$ALL_COMMENTERS" | grep -qi "$bot_base"; then
# Bot commented — verify it's a real review, not a rate-limit notice
if bot_has_real_review "$bot_base"; then
FOUND_BOTS="${FOUND_BOTS}${bot} "
echo "FOUND (real review): ${bot}"
else
RATE_LIMITED_BOTS="${RATE_LIMITED_BOTS}${bot} "
echo "FOUND (rate-limited, not a real review): ${bot}"
fi
else
MISSING_BOTS="${MISSING_BOTS}${bot} "
echo "MISSING: ${bot}"
fi
done
echo ""
echo "Found bots (real reviews): ${FOUND_BOTS:-none}"
echo "Rate-limited bots: ${RATE_LIMITED_BOTS:-none}"
echo "Missing bots: ${MISSING_BOTS:-none}"
# Gate logic: require at least ONE known bot to have posted a REAL review
# This is intentionally lenient — not all repos have all bots.
# The goal is to catch the case where NO bots have actually reviewed.
#
# Race condition fix: on pull_request events (opened/synchronize/reopened),
# the workflow fires before bots have had time to analyze. If no bots are
# found and the PR is young, pass with a "pending" note — the check will
# re-run when a bot posts via pull_request_review or issue_comment triggers.
# Only hard-fail when the PR is old enough that bots should have posted.
if [[ -n "$FOUND_BOTS" ]]; then
echo ""
echo "PASS: At least one AI review bot has posted a real review."
echo "gate_passed=true" >> "$GITHUB_OUTPUT"
echo "found_bots=${FOUND_BOTS}" >> "$GITHUB_OUTPUT"
echo "missing_bots=${MISSING_BOTS}" >> "$GITHUB_OUTPUT"
echo "rate_limited_bots=${RATE_LIMITED_BOTS}" >> "$GITHUB_OUTPUT"
echo "status_fallback=false" >> "$GITHUB_OUTPUT"
elif [[ -n "$RATE_LIMITED_BOTS" ]] && any_bot_has_success_status; then
# GH#3005: All bots are rate-limited in comments, but at least one
# posted a SUCCESS commit status check. The bot completed its analysis
# even though the comment was a rate-limit notice. Treat as reviewed.
echo ""
echo "PASS (status check fallback): Bots are rate-limited but posted SUCCESS status checks."
echo "gate_passed=true" >> "$GITHUB_OUTPUT"
echo "found_bots=${RATE_LIMITED_BOTS}" >> "$GITHUB_OUTPUT"
echo "missing_bots=${MISSING_BOTS}" >> "$GITHUB_OUTPUT"
echo "rate_limited_bots=${RATE_LIMITED_BOTS}" >> "$GITHUB_OUTPUT"
echo "status_fallback=true" >> "$GITHUB_OUTPUT"
elif [[ -n "$RATE_LIMITED_BOTS" ]] && [[ "$ELAPSED" -ge "$RATE_LIMIT_GRACE_SECONDS" ]]; then
# GH#3827: Rate-limit grace period exceeded. Bots posted rate-limit
# notices (proving they're configured) but the PR has been open long
# enough that blocking further provides no value. Pass with a warning.
GRACE_HOURS=$((RATE_LIMIT_GRACE_SECONDS / 3600))
AGE_HOURS=$((ELAPSED / 3600))
echo ""
echo "PASS (rate-limit grace): PR is ${AGE_HOURS}h old (threshold: ${GRACE_HOURS}h)."
echo "Bots are rate-limited but grace period exceeded. Passing gate."
echo "Bot reviews will be addressed post-merge if needed."
echo "gate_passed=true" >> "$GITHUB_OUTPUT"
echo "found_bots=" >> "$GITHUB_OUTPUT"
echo "missing_bots=${MISSING_BOTS}" >> "$GITHUB_OUTPUT"
echo "rate_limited_bots=${RATE_LIMITED_BOTS}" >> "$GITHUB_OUTPUT"
echo "status_fallback=false" >> "$GITHUB_OUTPUT"
echo "rate_limit_grace=true" >> "$GITHUB_OUTPUT"
elif [[ "$ELAPSED" -lt "$MIN_WAIT_SECONDS" ]]; then
echo ""
echo "PASS (pending): PR is ${ELAPSED}s old (< ${MIN_WAIT_SECONDS}s minimum)."
echo "Bots have not posted yet but the PR is too young to enforce the gate."
echo "This check will re-run when a bot posts a review or comment."
echo "gate_passed=true" >> "$GITHUB_OUTPUT"
echo "found_bots=" >> "$GITHUB_OUTPUT"
echo "missing_bots=${MISSING_BOTS}" >> "$GITHUB_OUTPUT"
echo "rate_limited_bots=${RATE_LIMITED_BOTS}" >> "$GITHUB_OUTPUT"
echo "status_fallback=false" >> "$GITHUB_OUTPUT"
else
echo ""
if [[ -n "$RATE_LIMITED_BOTS" ]]; then
echo "WAITING: Bots posted rate-limit notices only (not real reviews): ${RATE_LIMITED_BOTS}"
echo "No SUCCESS status checks found as fallback."
else
echo "WAITING: No AI review bots have posted yet."
fi
echo "This check will re-run when a bot posts a review or comment."
echo ""
echo "Expected bots: ${KNOWN_BOTS[*]}"
echo ""
echo "If no bots are configured, add 'skip-review-gate' label"
echo "to the PR to bypass this check."
echo "gate_passed=false" >> "$GITHUB_OUTPUT"
echo "found_bots=" >> "$GITHUB_OUTPUT"
echo "missing_bots=${MISSING_BOTS}" >> "$GITHUB_OUTPUT"
echo "rate_limited_bots=${RATE_LIMITED_BOTS}" >> "$GITHUB_OUTPUT"
echo "status_fallback=false" >> "$GITHUB_OUTPUT"
fi
- name: Check for skip label
id: skip
if: steps.check.outputs.gate_passed != 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
REPO: ${{ github.repository }}
run: |
LABELS=$(gh pr view "$PR_NUMBER" --repo "$REPO" \
--json labels -q '.labels[].name' 2>/dev/null || echo "")
if echo "$LABELS" | grep -q "skip-review-gate"; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "SKIP: 'skip-review-gate' label found — bypassing review bot gate."
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Gate result
if: steps.check.outputs.gate_passed != 'true' && steps.skip.outputs.skip != 'true'
env:
RATE_LIMITED_BOTS: ${{ steps.check.outputs.rate_limited_bots }}
MISSING_BOTS: ${{ steps.check.outputs.missing_bots }}
run: |
if [[ -n "$RATE_LIMITED_BOTS" ]]; then
echo "::error::Review bots posted rate-limit notices only — no real reviews or SUCCESS status checks."
echo ""
echo "Rate-limited bots: ${RATE_LIMITED_BOTS}"
echo ""
echo "The bots are rate-limited and did not perform actual code review."
echo "No SUCCESS commit status checks were found as fallback."
else
echo "::error::No AI review bots have posted on this PR yet."
fi
echo ""
echo "This PR cannot be merged until at least one AI code review bot"
echo "(CodeRabbit, Gemini Code Assist, etc.) has posted a real review"
echo "or a SUCCESS commit status check."
echo ""
echo "What to do:"
echo " 1. Wait a few minutes for bots to complete their analysis"
echo " 2. This check re-runs automatically when a bot posts"
echo " 3. If no bots are configured, add 'skip-review-gate' label"
echo ""
echo "Missing bots: ${MISSING_BOTS}"
exit 1
- name: Summary
if: always()
env:
GATE_PASSED: ${{ steps.check.outputs.gate_passed }}
STATUS_FALLBACK: ${{ steps.check.outputs.status_fallback }}
RATE_LIMIT_GRACE: ${{ steps.check.outputs.rate_limit_grace }}
FOUND_BOTS: ${{ steps.check.outputs.found_bots }}
MISSING_BOTS: ${{ steps.check.outputs.missing_bots }}
RATE_LIMITED_BOTS: ${{ steps.check.outputs.rate_limited_bots }}
SKIP_GATE: ${{ steps.skip.outputs.skip }}
run: |
{
echo "## Review Bot Gate"
echo ""
if [[ "$GATE_PASSED" == "true" && "$STATUS_FALLBACK" == "true" ]]; then
echo "**Status**: PASSED (status check fallback)"
echo ""
echo "Bots posted rate-limit notices but have SUCCESS commit status checks."
echo "The bots completed their analysis — treating as reviewed. (GH#3005)"
echo ""
echo "**Bots with SUCCESS status**: ${FOUND_BOTS}"
if [[ -n "$MISSING_BOTS" ]]; then
echo ""
echo "**Bots not yet reviewed**: ${MISSING_BOTS}"
fi
elif [[ "$GATE_PASSED" == "true" && "$RATE_LIMIT_GRACE" == "true" ]]; then
echo "**Status**: PASSED (rate-limit grace period)"
echo ""
echo "Bots are rate-limited and the PR has been open longer than the"
echo "grace period threshold. Passing to prevent indefinite blockage. (GH#3827)"
echo ""
echo "**Rate-limited bots**: ${RATE_LIMITED_BOTS}"
echo ""
echo "Bot reviews will be addressed post-merge if needed."
elif [[ "$GATE_PASSED" == "true" && -n "$FOUND_BOTS" ]]; then
echo "**Status**: PASSED"
echo ""
echo "**Bots that reviewed**: ${FOUND_BOTS}"
if [[ -n "$RATE_LIMITED_BOTS" ]]; then
echo ""
echo "**Bots rate-limited (not counted)**: ${RATE_LIMITED_BOTS}"
fi
if [[ -n "$MISSING_BOTS" ]]; then
echo ""
echo "**Bots not yet reviewed**: ${MISSING_BOTS}"
fi
elif [[ "$GATE_PASSED" == "true" ]]; then
echo "**Status**: PASSED (pending bot reviews)"
echo ""
echo "PR is too young to enforce the gate. Bots will re-trigger this"
echo "check when they post their reviews."
elif [[ "$SKIP_GATE" == "true" ]]; then
echo "**Status**: SKIPPED (skip-review-gate label)"
else
echo "**Status**: WAITING"
echo ""
if [[ -n "$RATE_LIMITED_BOTS" ]]; then
echo "Bots posted rate-limit notices only (not real reviews):"
echo "${RATE_LIMITED_BOTS}"
echo ""
fi
echo "No real AI review bot feedback yet. This check will re-run"
echo "automatically when a bot posts a review or comment."
fi
} >> "$GITHUB_STEP_SUMMARY"