#!/usr/bin/env python3 import argparse import json import re import subprocess import sys import time from collections import Counter from datetime import datetime, timedelta from pathlib import Path REPO = "pingcap/tidb" DEFAULT_QUERY = 'repo:pingcap/tidb is:issue label:"report/customer" label:"sig/planner" created:>=2024-01-01' DEFAULT_OUT_DIR = Path("outputs/tidb-customer-planner-issues") MAX_RETRIES = 3 SLEEP_SECS = 0.15 def parse_args(): parser = argparse.ArgumentParser( description="Generate a local markdown corpus from GitHub issue and PR history." ) parser.add_argument( "--repo", default=REPO, help="GitHub repository in owner/name form. Default: pingcap/tidb", ) parser.add_argument( "--query", default=DEFAULT_QUERY, help="GitHub issue search query used by gh api search/issues.", ) parser.add_argument( "--out-dir", default=str(DEFAULT_OUT_DIR), help="Directory where issue markdown files and README.md are written.", ) return parser.parse_args() def run(cmd): proc = subprocess.run(cmd, capture_output=True, text=True) if proc.returncode != 0: raise RuntimeError( f"command failed: {' '.join(cmd)}\nstdout:\n{proc.stdout}\nstderr:\n{proc.stderr}" ) return proc.stdout def gh_json(args): last_err = None for _ in range(MAX_RETRIES): try: out = run(["gh", "api", *args]) return json.loads(out) except Exception as exc: last_err = exc time.sleep(0.5) raise last_err def fetch_search_issues(query): issues = [] page = 1 while True: data = gh_json( [ "search/issues", "-X", "GET", "-f", f"q={query}", "-f", "per_page=100", "-f", f"page={page}", ] ) items = data.get("items", []) if not items: break issues.extend(items) if len(items) < 100: break page += 1 time.sleep(SLEEP_SECS) return issues def fetch_timeline(repo, issue_number): page = 1 items = [] while True: batch = gh_json( [ f"repos/{repo}/issues/{issue_number}/timeline", "-X", "GET", "-H", "Accept: application/vnd.github+json", "-f", "per_page=100", "-f", f"page={page}", ] ) if not batch: break items.extend(batch) if len(batch) < 100: break page += 1 time.sleep(SLEEP_SECS) return items def fetch_pr(repo, pr_number): return gh_json([f"repos/{repo}/pulls/{pr_number}"]) def fetch_pr_files(repo, pr_number): files = [] page = 1 while True: batch = gh_json( [ f"repos/{repo}/pulls/{pr_number}/files", "-X", "GET", "-f", "per_page=100", "-f", f"page={page}", ] ) if not batch: break files.extend(batch) if len(batch) < 100: break page += 1 time.sleep(SLEEP_SECS) return files def slugify(text): text = text.lower() text = re.sub(r"[^a-z0-9]+", "-", text) text = text.strip("-") return text[:80] or "issue" def issue_type(labels): for label in labels: name = label["name"] if name.startswith("type/"): return name return "type/unknown" def label_names(labels): return sorted(label["name"] for label in labels) def sanitize_text(text): text = text.replace("\r\n", "\n") text = re.sub(r"", "", text) text = re.sub(r"https://github\.com/[^\s)]+", "", text) text = re.sub(r"https?://[^\s)]+", "", text) text = re.sub(r"!\[[^\]]*\]\([^)]+\)", "", text) text = re.sub(r"]*>", "", text) text = re.sub(r"`{3}[\s\S]*?`{3}", "", text) text = re.sub(r"`[^`]*`", "", text) text = re.sub(r"^#+\s*", "", text, flags=re.M) text = re.sub(r"\n{3,}", "\n\n", text) return text.strip() def parse_iso8601(ts): if not ts: return None return datetime.fromisoformat(ts.replace("Z", "+00:00")) def extract_section(body, heading): pattern = re.compile(rf"{re.escape(heading)}\s*(.*?)(?=\n### |\n## |\Z)", re.S) match = pattern.search(body) if match: return sanitize_text(match.group(1)) return "" def extract_phenomenon(body): body = body or "" for heading in [ "### 3. What did you see instead (Required)", "### 1. Minimal reproduce step (Required)", "### What did you see instead (Required)", "### Problem Summary", "## Problem Summary", ]: section = extract_section(body, heading) if section: lines = [ln.strip("- ").strip() for ln in section.splitlines() if ln.strip()] lines = [ln for ln in lines if not ln.startswith("