Skip to content

Commit 7009901

Browse files
authored
Add Bash and PowerShell scripts for monitoring GitHub API rate limits (#25)
1 parent d039a2e commit 7009901

2 files changed

Lines changed: 473 additions & 0 deletions

File tree

_in progress/gh-ratelimit

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
#!/usr/bin/env bash
2+
# gh-ratelimit - GitHub API rate-limit monitor for Bash and POSIX-style shells.
3+
#
4+
# What it does:
5+
# Queries GitHub's /rate_limit endpoint through GitHub CLI and renders the
6+
# current API buckets sorted by pressure so the tightest buckets appear first.
7+
#
8+
# Requirements:
9+
# - gh (GitHub CLI)
10+
# - jq
11+
# - an authenticated gh session
12+
#
13+
# Setup:
14+
# macOS: brew install gh jq
15+
# Ubuntu: sudo apt-get install gh jq
16+
# Then authenticate once:
17+
# gh auth login
18+
#
19+
# Notes:
20+
# - This script uses gh for both API access and auth context.
21+
# - Bash version requires jq for parsing and pretty-printing JSON.
22+
# - GitHub's /rate_limit endpoint is exempt from rate limiting, so polling it is safe.
23+
#
24+
# Usage:
25+
# gh-ratelimit
26+
# gh-ratelimit -w
27+
# gh-ratelimit -w -i 5
28+
# gh-ratelimit -j
29+
# gh-ratelimit -q
30+
# gh-ratelimit -h
31+
#
32+
# Options:
33+
# -w, --watch Refresh continuously.
34+
# -i, --interval Watch refresh interval in seconds. Default: 10.
35+
# -j, --json Print the raw GitHub API response as formatted JSON.
36+
# -q, --quiet Only show buckets that are not full and have a non-zero limit.
37+
# -h, --help Show this help text.
38+
#
39+
# Examples:
40+
# gh-ratelimit # one-time snapshot
41+
# gh-ratelimit -q # only show buckets under pressure
42+
# gh-ratelimit -w -i 60 # refresh every 60 seconds
43+
# gh-ratelimit -j | jq . # inspect raw payload
44+
45+
set -euo pipefail
46+
47+
WATCH=0
48+
INTERVAL=10
49+
JSON=0
50+
QUIET=0
51+
52+
show_usage() {
53+
awk 'NR == 1 { next } /^#/ { sub(/^# ?/, ""); print; next } { exit }' "$0"
54+
}
55+
56+
while [[ $# -gt 0 ]]; do
57+
case "$1" in
58+
-w|--watch) WATCH=1 ;;
59+
-i|--interval) INTERVAL="${2:-10}"; shift ;;
60+
-j|--json) JSON=1 ;;
61+
-q|--quiet) QUIET=1 ;;
62+
-h|--help)
63+
show_usage
64+
exit 0
65+
;;
66+
*) echo "unknown arg: $1" >&2; exit 2 ;;
67+
esac
68+
shift
69+
done
70+
71+
command -v gh >/dev/null || { echo "gh not installed" >&2; exit 1; }
72+
command -v jq >/dev/null || { echo "jq not installed" >&2; exit 1; }
73+
74+
if ! gh auth status >/dev/null 2>&1; then
75+
echo "gh is installed but not authenticated. Run: gh auth login" >&2
76+
exit 1
77+
fi
78+
79+
# ANSI colours (skip if not a TTY)
80+
if [[ -t 1 ]]; then
81+
C_RESET=$'\033[0m'; C_DIM=$'\033[2m'; C_BOLD=$'\033[1m'
82+
C_RED=$'\033[31m'; C_YEL=$'\033[33m'; C_GRN=$'\033[32m'; C_CYN=$'\033[36m'
83+
else
84+
C_RESET=; C_DIM=; C_BOLD=; C_RED=; C_YEL=; C_GRN=; C_CYN=
85+
fi
86+
87+
bar() {
88+
# bar <remaining> <limit> <width>
89+
local rem="$1" lim="$2" width="${3:-24}"
90+
if (( lim <= 0 )); then printf '%*s' "$width" ''; return; fi
91+
local filled=$(( rem * width / lim ))
92+
(( filled < 0 )) && filled=0
93+
(( filled > width )) && filled=$width
94+
local empty=$(( width - filled ))
95+
local colour="$C_GRN"
96+
local pct=$(( rem * 100 / lim ))
97+
if (( pct < 10 )); then colour="$C_RED"
98+
elif (( pct < 33 )); then colour="$C_YEL"
99+
fi
100+
printf '%s' "$colour"
101+
if (( filled > 0 )); then
102+
printf '█%.0s' $(seq 1 "$filled")
103+
fi
104+
printf '%s' "$C_DIM"
105+
if (( empty > 0 )); then
106+
printf '░%.0s' $(seq 1 "$empty")
107+
fi
108+
printf '%s' "$C_RESET"
109+
}
110+
111+
human_reset() {
112+
# Seconds-from-now → e.g. "in 31m12s" or "passed"
113+
local target="$1" now diff m s
114+
now=$(date +%s)
115+
diff=$(( target - now ))
116+
if (( diff <= 0 )); then echo "now"; return; fi
117+
m=$(( diff / 60 )); s=$(( diff % 60 ))
118+
if (( m > 0 )); then printf 'in %dm%02ds' "$m" "$s"
119+
else printf 'in %ds' "$s"
120+
fi
121+
}
122+
123+
render() {
124+
local payload
125+
if ! payload=$(gh api rate_limit 2>/dev/null); then
126+
echo "${C_RED}gh api failed - are you authenticated? (gh auth status)${C_RESET}" >&2
127+
return 1
128+
fi
129+
130+
if (( JSON )); then
131+
echo "$payload" | jq .
132+
return 0
133+
fi
134+
135+
# Header
136+
local user host now
137+
user=$(gh api user --jq .login 2>/dev/null || echo '?')
138+
host=$(gh auth status 2>&1 | awk -F'[ ()]+' '/Logged in to/{print $5; exit}' || echo 'github.com')
139+
now=$(date '+%H:%M:%S')
140+
printf '%s%sGitHub rate limits%s user=%s%s%s host=%s %s%s%s\n' \
141+
"$C_BOLD" "$C_CYN" "$C_RESET" "$C_BOLD" "$user" "$C_RESET" "$host" "$C_DIM" "$now" "$C_RESET"
142+
echo
143+
144+
# Sort: most-pressured (lowest %) first
145+
local jq_filter='
146+
.resources
147+
| to_entries
148+
| map({
149+
key: .key,
150+
rem: .value.remaining,
151+
lim: .value.limit,
152+
used: .value.used,
153+
reset: .value.reset,
154+
pct: (if .value.limit > 0 then (.value.remaining * 100 / .value.limit) else 100 end)
155+
})
156+
| sort_by(.pct)
157+
| .[]
158+
| [.key, (.rem|tostring), (.lim|tostring), (.used|tostring), (.reset|tostring), ((.pct|floor)|tostring)]
159+
| @tsv'
160+
161+
local key rem lim used reset pct
162+
while IFS=$'\t' read -r key rem lim used reset pct; do
163+
if (( QUIET )) && (( rem == lim || lim == 0 )); then continue; fi
164+
printf '%s%-26s%s %5d/%-5d %s %3d%% resets %s\n' \
165+
"$C_BOLD" "$key" "$C_RESET" \
166+
"$rem" "$lim" \
167+
"$(bar "$rem" "$lim" 24)" \
168+
"$pct" \
169+
"$(human_reset "$reset")"
170+
done < <(echo "$payload" | jq -r "$jq_filter")
171+
}
172+
173+
if (( WATCH )); then
174+
trap 'tput cnorm 2>/dev/null; exit 0' INT TERM
175+
tput civis 2>/dev/null || true
176+
while true; do
177+
clear
178+
render || true
179+
printf '\n%srefresh every %ss - ctrl-c to exit%s\n' "$C_DIM" "$INTERVAL" "$C_RESET"
180+
sleep "$INTERVAL"
181+
done
182+
else
183+
render
184+
fi

0 commit comments

Comments
 (0)