commit 4352a1bae1d99311c813070b7b8b8a299b1904eb
parent 637f5c6d17a09684dabbaf4fa318dcab1c7651a9
Author: Amit Dutta <mail@amit.is-a.dev>
Date: Sat, 27 Jun 2026 10:12:56 +0530
Refactor update_maintainers.py for improved clarity
Diffstat:
1 file changed, 60 insertions(+), 86 deletions(-)
diff --git a/.github/scripts/update_maintainers.py b/.github/scripts/update_maintainers.py
@@ -3,14 +3,13 @@ import json
import os
import time
from datetime import datetime, timezone
-from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
# ── Config ────────────────────────────────────────────────────────────────────
REPO = "is-a-dev/register"
-REPO_START = (2020, 10) # October 2020
+REPO_START = (2020, 10)
CACHE_FILE = "pr_cache.json"
OUTPUT_FILE = "maintainers.json"
@@ -42,11 +41,11 @@ def month_key(y: int, m: int) -> str:
return f"{y}-{m:02d}"
def month_range_str(y: int, m: int) -> tuple[str, str]:
+ """Returns ISO timestamps for start/end of a month."""
ny, nm = next_month(y, m)
- return f"{y}-{m:02d}-01", f"{ny}-{nm:02d}-01"
+ return f"{y}-{m:02d}-01T00:00:00Z", f"{ny}-{nm:02d}-01T00:00:00Z"
def all_months() -> list[tuple[int, int]]:
- """All months from REPO_START up to and including current month."""
now = now_utc()
months = []
y, m = REPO_START
@@ -55,20 +54,9 @@ def all_months() -> list[tuple[int, int]]:
y, m = next_month(y, m)
return months
-# ── Cache helpers ─────────────────────────────────────────────────────────────
+# ── Cache ─────────────────────────────────────────────────────────────────────
def load_cache() -> dict:
- """
- Cache structure:
- {
- "USERNAME": {
- "2020-10": 5,
- "2020-11": 12,
- ...
- },
- ...
- }
- """
if os.path.exists(CACHE_FILE):
with open(CACHE_FILE) as f:
return json.load(f)
@@ -78,54 +66,48 @@ def save_cache(cache: dict):
with open(CACHE_FILE, "w") as f:
json.dump(cache, f, indent=2)
-# ── GitHub Search ─────────────────────────────────────────────────────────────
+# ── GitHub Commits API ────────────────────────────────────────────────────────
-def fetch_ids(query: str) -> set[int]:
- """Fetch all PR numbers matching a search query (handles pagination)."""
- ids = set()
- page = 1
+def count_commits(username: str, since: str, until: str) -> int:
+ """Count commits by a user in a date range using pagination."""
+ url = f"https://api.github.com/repos/{REPO}/commits"
+ count = 0
+ page = 1
while True:
while True:
- r = requests.get(
- "https://api.github.com/search/issues",
- headers=HEADERS,
- params={"q": query, "per_page": 100, "page": page},
- timeout=30,
- )
- if r.status_code in (403, 429):
- reset = int(r.headers.get("X-RateLimit-Reset", time.time() + 30))
- wait = max(reset - int(time.time()), 10)
+ r = requests.get(url, headers=HEADERS, params={
+ "author": username,
+ "since": since,
+ "until": until,
+ "per_page": 100,
+ "page": page,
+ }, timeout=30)
+
+ remaining = int(r.headers.get("X-RateLimit-Remaining", 10))
+ reset_at = int(r.headers.get("X-RateLimit-Reset", time.time() + 60))
+
+ if r.status_code in (403, 429) or remaining == 0:
+ wait = max(reset_at - int(time.time()), 5)
print(f" Rate limited – sleeping {wait}s …", flush=True)
- time.sleep(wait)
+ time.sleep(wait + 2)
continue
- if r.status_code == 422:
- return ids
+
r.raise_for_status()
+
+ if remaining < 10:
+ wait = max(reset_at - int(time.time()), 5)
+ print(f" Low requests ({remaining} left) – sleeping {wait}s …", flush=True)
+ time.sleep(wait + 2)
break
- data = r.json()
- items = data.get("items", [])
- for item in items:
- ids.add(item["number"])
+ items = r.json()
+ count += len(items)
if len(items) < 100:
break
page += 1
- time.sleep(0.5)
- return ids
-
-
-def count_for_month(username: str, y: int, m: int) -> int:
- """Count distinct closed PRs a maintainer interacted with in a given month."""
- start, end = month_range_str(y, m)
- base = f"repo:{REPO} is:pr is:closed -author:{username}"
- date = f"closed:{start}..{end}"
+ time.sleep(0.3)
- rev_ids = fetch_ids(f"{base} reviewed-by:{username} {date}")
- time.sleep(0.5)
- com_ids = fetch_ids(f"{base} commenter:{username} {date}")
- time.sleep(0.5)
-
- return len(rev_ids | com_ids)
+ return count
# ── Per-maintainer update ─────────────────────────────────────────────────────
@@ -138,22 +120,22 @@ def update_maintainer(username: str, cache: dict) -> dict:
for (y, m) in all_months():
key = month_key(y, m)
- # Always re-fetch current month (still growing). Skip all others if cached.
if key in user_cache and key != cur_key:
- print(f" {key}: cached={user_cache[key]} (skip)", flush=True)
+ print(f" {key}: {user_cache[key]} (cached)", flush=True)
continue
- count = count_for_month(username, y, m)
+ since, until = month_range_str(y, m)
+ count = count_commits(username, since, until)
user_cache[key] = count
- print(f" {key}: fetched={count}", flush=True)
-
- cache[username] = user_cache
+ print(f" {key}: {count} (fetched)", flush=True)
- # Compute stats from cache
- all_time = sum(user_cache.values())
+ # Save after every month so progress isn't lost on interruption
+ cache[username] = user_cache
+ save_cache(cache)
- py, pm = prev_month(now.year, now.month)
- this_month = user_cache.get(month_key(now.year, now.month), 0)
+ py, pm = prev_month(now.year, now.month)
+ all_time = sum(user_cache.values())
+ this_month = user_cache.get(cur_key, 0)
last_month = user_cache.get(month_key(py, pm), 0)
print(f" ✓ all={all_time} this_month={this_month} last_month={last_month}", flush=True)
@@ -175,33 +157,25 @@ def main():
raise RuntimeError("GITHUB_TOKEN not set")
cache = load_cache()
- print(f"Loaded cache for {len(cache)} maintainers.\n", flush=True)
-
- results = {}
- # max_workers=2 to stay within Search API rate limits (30 req/min)
- with ThreadPoolExecutor(max_workers=2) as ex:
- futures = {ex.submit(update_maintainer, u, cache): u for u in MAINTAINERS}
- for future in as_completed(futures):
- u = futures[future]
- try:
- results[u] = future.result()
- except Exception as e:
- print(f" ✗ {u} failed: {e}", flush=True)
- results[u] = {
- "username": u,
- "stats": {"all_time": 0, "this_month": 0, "last_month": 0},
- "last_updated": now_utc().isoformat(),
- }
-
- # Save updated cache
- save_cache(cache)
- print(f"\n✓ Cache saved to {CACHE_FILE}", flush=True)
-
- # Write output
+ print(f"Loaded cache for {len(cache)} maintainer(s).\n", flush=True)
+
+ results = {}
+ for username in MAINTAINERS:
+ try:
+ results[username] = update_maintainer(username, cache)
+ except Exception as e:
+ print(f" ✗ {username} failed: {e}", flush=True)
+ results[username] = {
+ "username": username,
+ "stats": {"all_time": 0, "this_month": 0, "last_month": 0},
+ "last_updated": now_utc().isoformat(),
+ }
+
output = [results[u] for u in MAINTAINERS if u in results]
with open(OUTPUT_FILE, "w") as f:
json.dump(output, f, indent=2)
- print(f"✓ Written to {OUTPUT_FILE}", flush=True)
+
+ print(f"\n✓ Written to {OUTPUT_FILE}", flush=True)
if __name__ == "__main__":