notamitgamer

It's my profile man, there is ...
Log | Files | Refs | README

commit 9bfd16aac55f0d74eaee3d6be3715a2f9ac16f4b
parent b1e32985aad6132f0f09f60562d32d931b5f0aeb
Author: Amit Dutta <mail@amit.is-a.dev>
Date:   Sat, 27 Jun 2026 09:37:15 +0530

Refactor update_maintainers.py for efficiency

Refactor update_maintainers.py to use GitHub Search API for fetching PRs. Simplify activity collection and improve performance by reducing pagination.
Diffstat:
M.github/scripts/update_maintainers.py | 255++++++++++++++++---------------------------------------------------------------
1 file changed, 51 insertions(+), 204 deletions(-)

diff --git a/.github/scripts/update_maintainers.py b/.github/scripts/update_maintainers.py @@ -3,21 +3,15 @@ Fetches PR activity for tracked maintainers from is-a-dev/register and writes the results to maintainers.json. -Activity counted (excluding PRs the maintainer themselves opened): - - PR reviews (approved, changes requested, commented) - - PR review comments - - Issue comments on PRs - - Merging a PR - - Closing a PR (without merge) - - Adding labels to a PR +Uses the GitHub Search API to find PRs each maintainer interacted with +(reviewed, commented, merged, closed, labelled) — much faster than +paginating every PR in the repo. """ import json import os -import sys import time -from datetime import datetime, timezone, timedelta -from collections import defaultdict +from datetime import datetime, timezone import requests @@ -42,8 +36,6 @@ MAINTAINERS = [ GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "") OUTPUT_FILE = "maintainers.json" -# ── Helpers ─────────────────────────────────────────────────────────────────── - HEADERS = { "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", @@ -51,11 +43,9 @@ HEADERS = { if GITHUB_TOKEN: HEADERS["Authorization"] = f"Bearer {GITHUB_TOKEN}" -MAINTAINERS_LOWER = {m.lower() for m in MAINTAINERS} - +# ── Helpers ─────────────────────────────────────────────────────────────────── -def get(url: str, params: dict = None) -> dict | list: - """GET with basic rate-limit handling.""" +def get(url: str, params: dict = None): while True: r = requests.get(url, headers=HEADERS, params=params, timeout=30) if r.status_code == 403 and "rate limit" in r.text.lower(): @@ -64,26 +54,31 @@ def get(url: str, params: dict = None) -> dict | list: print(f" Rate limited – sleeping {wait}s …", flush=True) time.sleep(wait) continue + if r.status_code == 422: + # Search index not available for this query, return empty + return {"items": [], "total_count": 0} r.raise_for_status() return r.json() -def paginate(url: str, params: dict = None) -> list: - """Fetch all pages from a GitHub list endpoint.""" - params = dict(params or {}) - params.setdefault("per_page", 100) - results = [] +def search_prs(query: str) -> set[int]: + """Return set of PR numbers matching a GitHub search query.""" + pr_numbers = set() page = 1 while True: - params["page"] = page - chunk = get(url, params) - if not chunk: - break - results.extend(chunk) - if len(chunk) < params["per_page"]: + data = get( + "https://api.github.com/search/issues", + {"q": query, "per_page": 100, "page": page}, + ) + items = data.get("items", []) + for item in items: + pr_numbers.add(item["number"]) + print(f" page {page}: {len(items)} results (total so far: {len(pr_numbers)})", flush=True) + if len(items) < 100: break page += 1 - return results + time.sleep(0.5) # be kind to search rate limits + return pr_numbers def now_utc() -> datetime: @@ -91,13 +86,8 @@ def now_utc() -> datetime: def month_bounds(offset: int = 0): - """ - Return (start, end) for a calendar month. - offset=0 → current month so far - offset=-1 → last month - """ - today = now_utc().replace(day=1, hour=0, minute=0, second=0, microsecond=0) - # shift months + """Return (start_str, end_str) in YYYY-MM-DD for use in GitHub search.""" + today = now_utc().replace(day=1) year, month = today.year, today.month month += offset while month <= 0: @@ -107,199 +97,57 @@ def month_bounds(offset: int = 0): month -= 12 year += 1 start = today.replace(year=year, month=month) - # end = first day of next month end_month = month + 1 end_year = year if end_month > 12: end_month = 1 end_year += 1 end = start.replace(year=end_year, month=end_month) - return start, end - - -# ── Core fetch logic ────────────────────────────────────────────────────────── - -def fetch_all_prs() -> list: - """Fetch every PR (open + closed) from the repo.""" - print("Fetching all PRs …", flush=True) - prs = paginate( - f"https://api.github.com/repos/{REPO}/pulls", - {"state": "all", "sort": "updated", "direction": "desc"}, - ) - print(f" → {len(prs)} PRs fetched", flush=True) - return prs - - -def is_maintainer(username: str) -> bool: - return username and username.lower() in MAINTAINERS_LOWER + return start.strftime("%Y-%m-%d"), end.strftime("%Y-%m-%d") -def canonical(username: str) -> str: - """Return the correctly-cased username from MAINTAINERS list.""" - for m in MAINTAINERS: - if m.lower() == username.lower(): - return m - return username +# ── Per-maintainer search ───────────────────────────────────────────────────── - -def collect_activities(prs: list) -> list[dict]: +def get_pr_count(username: str, date_filter: str = "") -> int: """ - For each PR not authored by a maintainer, collect all maintainer - interactions: reviews, review-comments, issue-comments, merge, close, labels. + Count distinct PRs in REPO that `username` interacted with + (reviewed, commented, merged/closed) but did NOT author. - Returns a flat list of: - { "username": str, "pr_number": int, "type": str, "created_at": datetime } + date_filter: e.g. "2025-06-01..2025-07-01" or "" for all time. """ - activities = [] - total = len(prs) - - for i, pr in enumerate(prs, 1): - pr_number = pr["number"] - pr_author = (pr.get("user") or {}).get("login", "") - print(f" [{i}/{total}] PR #{pr_number} by {pr_author}", flush=True) - - base = f"https://api.github.com/repos/{REPO}" - - # ── Reviews ────────────────────────────────────────────────────────── - reviews = paginate(f"{base}/pulls/{pr_number}/reviews") - for rev in reviews: - user = (rev.get("user") or {}).get("login", "") - if is_maintainer(user) and user.lower() != pr_author.lower(): - submitted = rev.get("submitted_at") or rev.get("created_at") - if submitted: - activities.append({ - "username": canonical(user), - "pr_number": pr_number, - "type": "review", - "created_at": datetime.fromisoformat(submitted.replace("Z", "+00:00")), - }) - - # ── Review comments (inline) ────────────────────────────────────────── - review_comments = paginate(f"{base}/pulls/{pr_number}/comments") - seen_review_comment_pr = set() - for rc in review_comments: - user = (rc.get("user") or {}).get("login", "") - if is_maintainer(user) and user.lower() != pr_author.lower(): - key = (canonical(user), pr_number, "review_comment") - if key not in seen_review_comment_pr: - seen_review_comment_pr.add(key) - created = rc.get("created_at", "") - if created: - activities.append({ - "username": canonical(user), - "pr_number": pr_number, - "type": "review_comment", - "created_at": datetime.fromisoformat(created.replace("Z", "+00:00")), - }) - - # ── Issue comments (general PR comments) ───────────────────────────── - issue_comments = paginate(f"{base}/issues/{pr_number}/comments") - seen_issue_comment_pr = set() - for ic in issue_comments: - user = (ic.get("user") or {}).get("login", "") - if is_maintainer(user) and user.lower() != pr_author.lower(): - key = (canonical(user), pr_number, "comment") - if key not in seen_issue_comment_pr: - seen_issue_comment_pr.add(key) - created = ic.get("created_at", "") - if created: - activities.append({ - "username": canonical(user), - "pr_number": pr_number, - "type": "comment", - "created_at": datetime.fromisoformat(created.replace("Z", "+00:00")), - }) - - # ── Merge ───────────────────────────────────────────────────────────── - if pr.get("merged_at"): - merged_by = (pr.get("merged_by") or {}).get("login", "") - if is_maintainer(merged_by) and merged_by.lower() != pr_author.lower(): - merged_at = pr["merged_at"] - activities.append({ - "username": canonical(merged_by), - "pr_number": pr_number, - "type": "merge", - "created_at": datetime.fromisoformat(merged_at.replace("Z", "+00:00")), - }) - - # ── Close (without merge) ───────────────────────────────────────────── - if pr.get("state") == "closed" and not pr.get("merged_at"): - closed_at = pr.get("closed_at", "") - if closed_at: - # GitHub doesn't expose who closed a PR directly on the PR object. - # We derive it from the timeline events. - events = paginate(f"{base}/issues/{pr_number}/events") - for ev in events: - if ev.get("event") == "closed": - closer = (ev.get("actor") or {}).get("login", "") - if is_maintainer(closer) and closer.lower() != pr_author.lower(): - activities.append({ - "username": canonical(closer), - "pr_number": pr_number, - "type": "close", - "created_at": datetime.fromisoformat( - ev["created_at"].replace("Z", "+00:00") - ), - }) - - # ── Labels ──────────────────────────────────────────────────────────── - events = paginate(f"{base}/issues/{pr_number}/events") - seen_label_pr = set() - for ev in events: - if ev.get("event") == "labeled": - labeler = (ev.get("actor") or {}).get("login", "") - if is_maintainer(labeler) and labeler.lower() != pr_author.lower(): - key = (canonical(labeler), pr_number, "label") - if key not in seen_label_pr: - seen_label_pr.add(key) - activities.append({ - "username": canonical(labeler), - "pr_number": pr_number, - "type": "label", - "created_at": datetime.fromisoformat( - ev["created_at"].replace("Z", "+00:00") - ), - }) - - return activities - - -# ── Dedup & count ───────────────────────────────────────────────────────────── - -def count_unique_prs(activities: list, username: str, since: datetime = None, until: datetime = None) -> int: - """Count distinct PRs a maintainer interacted with, optionally within a date range.""" + base = f"repo:{REPO} is:pr -author:{username}" + date_part = f" updated:{date_filter}" if date_filter else "" + + queries = [ + f"{base} reviewed-by:{username}{date_part}", + f"{base} commenter:{username}{date_part}", + ] + pr_set = set() - for act in activities: - if act["username"] != username: - continue - ts = act["created_at"] - if since and ts < since: - continue - if until and ts >= until: - continue - pr_set.add(act["pr_number"]) + for q in queries: + print(f" Searching: {q}", flush=True) + pr_set |= search_prs(q) + time.sleep(1) # GitHub search: 30 req/min for authenticated + return len(pr_set) # ── Main ────────────────────────────────────────────────────────────────────── def main(): - prs = fetch_all_prs() - - print("Collecting maintainer activities …", flush=True) - activities = collect_activities(prs) - print(f" → {len(activities)} activity records collected", flush=True) - - # Date bounds this_month_start, this_month_end = month_bounds(0) last_month_start, last_month_end = month_bounds(-1) updated_at = now_utc().isoformat() output = [] for username in MAINTAINERS: - all_time = count_unique_prs(activities, username) - this_month = count_unique_prs(activities, username, since=this_month_start, until=this_month_end) - last_month = count_unique_prs(activities, username, since=last_month_start, until=last_month_end) + print(f"\n── {username} ──", flush=True) + + all_time = get_pr_count(username) + this_month = get_pr_count(username, f"{this_month_start}..{this_month_end}") + last_month = get_pr_count(username, f"{last_month_start}..{last_month_end}") + + print(f" → all={all_time} this_month={this_month} last_month={last_month}", flush=True) output.append({ "username": username, @@ -310,7 +158,6 @@ def main(): }, "last_updated": updated_at, }) - print(f" {username}: all={all_time} this_month={this_month} last_month={last_month}", flush=True) with open(OUTPUT_FILE, "w") as f: json.dump(output, f, indent=2)
© notamitgamer • Site Built: 2026-07-21 13:58:23 UTC • git-mirror commit: 1037f62 [view raw info]
Originally created with stagit • modified by notamitgamer
Forked from github.com/notamitgamer/git-mirror