Copilot Review Poller #1147
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Copilot Review Poller | |
| # Polls open [Agent] PRs for Copilot reviews and dispatches Claude to handle them. | |
| # This exists because GitHub blocks event-triggered workflows from bot actors | |
| # (copilot[bot]) with "action_required" approval gates. Scheduled workflows run | |
| # as a trusted actor, bypassing this limitation entirely. | |
| # | |
| # Also cleans up stale "action_required" workflow runs that accumulate from | |
| # pull_request_review events fired by Copilot. | |
| on: | |
| schedule: | |
| - cron: '*/10 * * * *' # Every 10 minutes | |
| workflow_dispatch: # Manual trigger for testing/forcing a poll | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| actions: write # Needed to dispatch claude-code.yml and delete stale runs | |
| jobs: | |
| cleanup: | |
| name: Clean up stale action_required runs | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Delete stale action_required workflow runs | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| stale_runs=$(gh api "repos/${{ github.repository }}/actions/runs?status=action_required&per_page=100" \ | |
| --jq '.workflow_runs[].id' 2>/dev/null || true) | |
| [ -z "$stale_runs" ] && echo "No stale action_required runs." && exit 0 | |
| deleted=0 | |
| for run_id in $stale_runs; do | |
| gh api "repos/${{ github.repository }}/actions/runs/$run_id" \ | |
| --method DELETE 2>/dev/null && deleted=$((deleted + 1)) || true | |
| done | |
| echo "Deleted $deleted stale action_required run(s)." | |
| poll: | |
| name: Poll Copilot reviews on Agent PRs | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check Copilot reviews and dispatch Claude | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| echo "Checking open [Agent] PRs for Copilot reviews..." | |
| prs=$(gh api "repos/${{ github.repository }}/pulls?state=open&per_page=100" \ | |
| --jq '.[] | select(.title | startswith("[Agent]")) | .number' 2>/dev/null || true) | |
| [ -z "$prs" ] && echo "No open [Agent] PRs found." && exit 0 | |
| dispatched=0 | |
| for pr_number in $prs; do | |
| echo "--- Checking PR #$pr_number ---" | |
| # Get the latest review from any Copilot bot account | |
| latest_copilot_review=$(gh api "repos/${{ github.repository }}/pulls/$pr_number/reviews" \ | |
| --jq '[.[] | select( | |
| .user.login == "copilot-pull-request-reviewer[bot]" or | |
| .user.login == "copilot-pull-request-reviewer" or | |
| .user.login == "copilot[bot]" | |
| )] | sort_by(.submitted_at) | last' 2>/dev/null || true) | |
| if [ -z "$latest_copilot_review" ] || [ "$latest_copilot_review" = "null" ]; then | |
| echo " No Copilot review found." | |
| continue | |
| fi | |
| review_state=$(echo "$latest_copilot_review" | jq -r '.state') | |
| review_id=$(echo "$latest_copilot_review" | jq -r '.id') | |
| review_time=$(echo "$latest_copilot_review" | jq -r '.submitted_at') | |
| echo " Latest Copilot review: state=$review_state id=$review_id time=$review_time" | |
| # Skip if Claude already posted a marker for this review ID | |
| already_processed=$(gh api "repos/${{ github.repository }}/issues/$pr_number/comments?per_page=100" \ | |
| --jq "[.[] | select(.body | contains(\"copilot-review-processed:$review_id\"))] | length" \ | |
| 2>/dev/null || echo "0") | |
| if [ "$already_processed" -gt 0 ]; then | |
| echo " Already processed (marker comment found). Skipping." | |
| continue | |
| fi | |
| # Skip if commits were pushed after the review (likely already addressed) | |
| latest_commit_time=$(gh api "repos/${{ github.repository }}/pulls/$pr_number/commits?per_page=100" \ | |
| --jq '[.[].commit.committer.date] | sort | last' 2>/dev/null || true) | |
| if [ -n "$latest_commit_time" ] && [ "$latest_commit_time" \> "$review_time" ]; then | |
| echo " Commits exist after review ($latest_commit_time > $review_time). Skipping." | |
| continue | |
| fi | |
| # Determine what action to take based on review state | |
| action="" | |
| case "$review_state" in | |
| CHANGES_REQUESTED) | |
| action="fix_copilot_review" | |
| echo " -> Dispatching: fix_copilot_review" | |
| ;; | |
| COMMENTED) | |
| # Check the REVIEW-ATTACHED comments (not top-level PR comments) | |
| # This is the correct endpoint for inline review suggestions | |
| inline_count=$(gh api "repos/${{ github.repository }}/pulls/$pr_number/reviews/$review_id/comments" \ | |
| --jq 'length' 2>/dev/null || echo "0") | |
| if [ "$inline_count" -gt 0 ]; then | |
| action="fix_copilot_review" | |
| echo " -> COMMENTED with $inline_count review comments. Dispatching: fix_copilot_review" | |
| else | |
| # Also check review body for "generated N comments" | |
| generated=$(echo "$latest_copilot_review" | jq -r '.body' | grep -oP 'generated \K\d+' || echo "0") | |
| if [ "$generated" -gt 0 ]; then | |
| action="fix_copilot_review" | |
| echo " -> Review body says $generated comments generated. Dispatching: fix_copilot_review" | |
| else | |
| echo " COMMENTED with no inline comments — clean review." | |
| continue | |
| fi | |
| fi | |
| ;; | |
| APPROVED) | |
| action="copilot_approved" | |
| echo " -> Dispatching: copilot_approved" | |
| ;; | |
| *) | |
| echo " Review state '$review_state' — no action needed." | |
| continue | |
| ;; | |
| esac | |
| # Loop protection: after 4 Copilot fix cycles, escalate to human review | |
| if [ "$action" = "fix_copilot_review" ]; then | |
| cycle_count=$(gh api "repos/${{ github.repository }}/pulls/$pr_number/reviews" \ | |
| --jq '[.[] | select( | |
| (.user.login == "copilot-pull-request-reviewer[bot]" or | |
| .user.login == "copilot-pull-request-reviewer" or | |
| .user.login == "copilot[bot]") and | |
| (.state == "CHANGES_REQUESTED" or .state == "COMMENTED") | |
| )] | length' 2>/dev/null || echo "0") | |
| if [ "$cycle_count" -ge 4 ]; then | |
| echo " Loop protection: $cycle_count Copilot review cycles. Escalating to human review." | |
| action="copilot_approved" | |
| fi | |
| fi | |
| # Dispatch the Claude Code Agent workflow | |
| gh workflow run claude-code.yml \ | |
| -f pr_number="$pr_number" \ | |
| -f action="$action" \ | |
| --repo "${{ github.repository }}" | |
| dispatched=$((dispatched + 1)) | |
| echo " Dispatched successfully (action=$action)." | |
| done | |
| echo "---" | |
| echo "Done. Dispatched $dispatched workflow(s)." |