Created
March 30, 2026 20:45
-
-
Save PaulKinlan/3887bd2a0a533d2ff3c1ee8ed59cc230 to your computer and use it in GitHub Desktop.
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
| #!/usr/bin/env bash | |
| # gh-ai-issue-comments.sh | |
| # Fetch 6 months of AI bot issue comment counts from GitHub Search API. | |
| # Uses total_count per query (not actual results), so no 1,000-result cap. | |
| # Output: CSV to stdout, progress to stderr. | |
| # | |
| # Tracks issues commented on by known AI bot accounts using "commenter:" qualifier. | |
| # | |
| # Usage: | |
| # ./gh-ai-issue-comments.sh # weekly buckets, last 6 months | |
| # ./gh-ai-issue-comments.sh --granularity monthly | |
| # ./gh-ai-issue-comments.sh --start 2025-08-01 --end 2026-02-27 | |
| # | |
| # Requirements: gh (authenticated), jq | |
| # Rate limit: 30 search requests/min authenticated. Script sleeps 2.5s/request. | |
| set -euo pipefail | |
| # ── Config ──────────────────────────────────────────────────────────────────── | |
| GRANULARITY="weekly" | |
| END_DATE=$(date +%Y-%m-%d) | |
| START_DATE=$(date -d "6 months ago" +%Y-%m-%d) | |
| SLEEP_SECS=2.5 | |
| while [[ $# -gt 0 ]]; do | |
| case $1 in | |
| --granularity) GRANULARITY="$2"; shift 2 ;; | |
| --start) START_DATE="$2"; shift 2 ;; | |
| --end) END_DATE="$2"; shift 2 ;; | |
| --sleep) SLEEP_SECS="$2"; shift 2 ;; | |
| *) echo "Unknown arg: $1" >&2; exit 1 ;; | |
| esac | |
| done | |
| # ── Queries to track ───────────────────────────────────────────────────────── | |
| # Format: "display_name|search_query" | |
| # All queries get "is:issue" prepended and "updated:FROM..TO" appended automatically. | |
| # Uses "commenter:" qualifier to find issues where the bot has commented. | |
| declare -a TOOLS=( | |
| # ── AI Code Review Bots ── | |
| "CodeRabbit|commenter:coderabbitai[bot]" | |
| "Gemini Code Assist|commenter:gemini-code-assist[bot]" | |
| "Cursor|commenter:cursor[bot]" | |
| "Claude|commenter:claude[bot]" | |
| "Copilot|commenter:copilot[bot]" | |
| "Copilot PR Reviewer|commenter:copilot-pull-request-reviewer[bot]" | |
| "Sourcery|commenter:sourcery-ai[bot]" | |
| "Korbit|commenter:korbit-ai[bot]" | |
| "What The Diff|commenter:what-the-diff[bot]" | |
| "CodeAnt AI|commenter:codeant-ai[bot]" | |
| "Augment Code|commenter:augmentcode[bot]" | |
| "Ellipsis|commenter:ellipsis-dev[bot]" | |
| "Greptile|commenter:greptile[bot]" | |
| "Greptile Apps|commenter:greptile-apps[bot]" | |
| "PR-Agent/CodiumAI|commenter:codiumai-pr-agent-pro[bot]" | |
| "Qodo|commenter:qodo-ai[bot]" | |
| # ── AI Coding Agents ── | |
| "Copilot SWE Agent|commenter:copilot-swe-agent[bot]" | |
| "Codex Connector|commenter:chatgpt-codex-connector[bot]" | |
| "Devin|commenter:devin-ai-integration[bot]" | |
| "Jules|commenter:google-labs-jules[bot]" | |
| "Amazon Q|commenter:amazon-q-developer[bot]" | |
| "Sweep|commenter:sweep-ai[bot]" | |
| "Pixeebot|commenter:pixeebot[bot]" | |
| "Codeflash|commenter:codeflash-ai[bot]" | |
| "Codegen|commenter:codegen-sh[bot]" | |
| "Mentat|commenter:mentatbot[bot]" | |
| "GitAuto|commenter:gitauto-ai[bot]" | |
| # ── AI Docs / Triage ── | |
| "Dosu|commenter:dosubot[bot]" | |
| "Continue|commenter:continue[bot]" | |
| "Graphite|commenter:graphite-app[bot]" | |
| # ── Dependency / Automation (context) ── | |
| "Dependabot|commenter:dependabot[bot]" | |
| "Renovate|commenter:renovate[bot]" | |
| "GitHub Actions|commenter:github-actions[bot]" | |
| ) | |
| # ── Date range generation ───────────────────────────────────────────────────── | |
| generate_ranges() { | |
| local granularity="$1" | |
| local start="$2" | |
| local end="$3" | |
| case "$granularity" in | |
| daily) | |
| local current="$start" | |
| while [[ "$current" < "$end" || "$current" == "$end" ]]; do | |
| echo "${current}|${current}" | |
| current=$(date -d "$current + 1 day" +%Y-%m-%d) | |
| done | |
| ;; | |
| weekly) | |
| local current | |
| current=$(date -d "$start - $(date -d "$start" +%u) days + 1 day" +%Y-%m-%d) | |
| [[ "$current" < "$start" ]] && current=$(date -d "$current + 7 days" +%Y-%m-%d) | |
| while [[ "$current" < "$end" || "$current" == "$end" ]]; do | |
| local week_end | |
| week_end=$(date -d "$current + 6 days" +%Y-%m-%d) | |
| [[ "$week_end" > "$end" ]] && week_end="$end" | |
| echo "${current}|${week_end}" | |
| current=$(date -d "$current + 7 days" +%Y-%m-%d) | |
| done | |
| ;; | |
| monthly) | |
| local current="$start" | |
| while [[ "$current" < "$end" || "$current" == "$end" ]]; do | |
| local year month last_day | |
| year=$(date -d "$current" +%Y) | |
| month=$(date -d "$current" +%m) | |
| last_day=$(date -d "$year-$month-01 + 1 month - 1 day" +%Y-%m-%d) | |
| [[ "$last_day" > "$end" ]] && last_day="$end" | |
| local range_start | |
| range_start=$(date -d "$year-$month-01" +%Y-%m-%d) | |
| echo "${range_start}|${last_day}" | |
| current=$(date -d "$year-$month-01 + 1 month" +%Y-%m-%d) | |
| done | |
| ;; | |
| esac | |
| } | |
| # ── Query function ──────────────────────────────────────────────────────────── | |
| url_encode() { | |
| local str="$1" | |
| str="${str// /+}" | |
| str="${str//[/\%5B}" | |
| str="${str//]/\%5D}" | |
| str="${str//:/%3A}" | |
| echo "$str" | |
| } | |
| MAX_RETRIES=5 | |
| RETRY_BASE_SECS=60 | |
| query_count() { | |
| local query="$1" | |
| local from="$2" | |
| local to="$3" | |
| local full_query="is:issue ${query} updated:${from}..${to}" | |
| local attempt=0 | |
| while true; do | |
| local result | |
| local exit_code=0 | |
| result=$(gh api \ | |
| --method GET \ | |
| "search/issues" \ | |
| -f q="${full_query}" \ | |
| -f per_page=1 \ | |
| 2>&1) || exit_code=$? | |
| if echo "$result" | grep -qi "secondary rate limit"; then | |
| attempt=$(( attempt + 1 )) | |
| if [[ "$attempt" -gt "$MAX_RETRIES" ]]; then | |
| echo "ERROR: secondary rate limit — gave up after $MAX_RETRIES retries" >&2 | |
| echo "ERROR" | |
| return | |
| fi | |
| local wait_secs=$(( RETRY_BASE_SECS * attempt )) | |
| echo " RATE LIMITED: waiting ${wait_secs}s before retry $attempt/$MAX_RETRIES..." >&2 | |
| sleep "$wait_secs" | |
| continue | |
| fi | |
| if [[ "$exit_code" -ne 0 ]]; then | |
| echo "ERROR: gh api failed (exit $exit_code): $result" >&2 | |
| echo "ERROR" | |
| return | |
| fi | |
| local count | |
| count=$(echo "$result" | jq -r '.total_count // empty' 2>/dev/null) | |
| if [[ -z "$count" ]]; then | |
| local err_msg | |
| err_msg=$(echo "$result" | jq -r '.message // .errors[0].message // "unknown error"' 2>/dev/null || echo "unparseable response") | |
| echo "ERROR: API response: $err_msg" >&2 | |
| echo "ERROR" | |
| return | |
| fi | |
| echo "$count" | |
| return | |
| done | |
| } | |
| build_search_url() { | |
| local query="$1" | |
| local from="$2" | |
| local to="$3" | |
| local encoded | |
| encoded=$(url_encode "is:issue ${query} updated:${from}..${to}") | |
| echo "https://github.com/search?q=${encoded}&type=issues" | |
| } | |
| # ── Main ────────────────────────────────────────────────────────────────────── | |
| echo "tool,period_start,period_end,granularity,total_count,url" | |
| mapfile -t RANGES < <(generate_ranges "$GRANULARITY" "$START_DATE" "$END_DATE") | |
| total_requests=$(( ${#TOOLS[@]} * ${#RANGES[@]} )) | |
| done_requests=0 | |
| est_secs=$(( total_requests * 3 )) | |
| est_mins=$(( est_secs / 60 )) | |
| echo "==> ${#TOOLS[@]} tools × ${#RANGES[@]} periods = ${total_requests} API calls" >&2 | |
| echo "==> Estimated time: ~${est_mins} minutes" >&2 | |
| echo "" >&2 | |
| for tool_entry in "${TOOLS[@]}"; do | |
| display="${tool_entry%%|*}" | |
| query="${tool_entry#*|}" | |
| echo "--- ${display} ---" >&2 | |
| for range in "${RANGES[@]}"; do | |
| from="${range%%|*}" | |
| to="${range#*|}" | |
| count=$(query_count "$query" "$from" "$to") | |
| url=$(build_search_url "$query" "$from" "$to") | |
| echo "${display},${from},${to},${GRANULARITY},${count},${url}" | |
| if [[ "$count" == "ERROR" ]]; then | |
| echo " ${from}..${to}: ERROR (see above)" >&2 | |
| else | |
| echo " ${from}..${to}: ${count} ${url}" >&2 | |
| fi | |
| done_requests=$(( done_requests + 1 )) | |
| if [[ "$done_requests" -lt "$total_requests" ]]; then | |
| sleep "$SLEEP_SECS" | |
| fi | |
| done | |
| done | |
| echo "" >&2 | |
| echo "==> Done. ${done_requests} requests made." >&2 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment