From 797c8e70964c6ba34161963ac120663bd5132689 Mon Sep 17 00:00:00 2001 From: vrtnis <123119434+vrtnis@users.noreply.github.com> Date: Mon, 2 Jun 2025 00:18:58 +0000 Subject: [PATCH] Add GitHub Action to triage issues and publish to wiki --- .github/scripts/issue_triage.py | 164 ++++++++++++++++++++++++++++++++ .github/workflows/triage.yml | 60 ++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 .github/scripts/issue_triage.py create mode 100644 .github/workflows/triage.yml diff --git a/.github/scripts/issue_triage.py b/.github/scripts/issue_triage.py new file mode 100644 index 00000000..ade435b2 --- /dev/null +++ b/.github/scripts/issue_triage.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python +from __future__ import annotations +import os, sys, json, datetime, pathlib, textwrap, requests +from openai import OpenAI + +REPO = "voideditor/void" +CACHE_FILE = pathlib.Path(".github/triage_cache.json") +STAMP_FILE = pathlib.Path(".github/last_triage.txt") + +THEMES_MD = textwrap.dedent("""\ +1. πŸ”— LLM Integration & Provider Support +2. πŸ–₯ App Build & Platform Compatibility +3. 🎯 Prompt, Token, and Cost Management +4. 🧩 Editor UX & Interaction Design +5. πŸ€– Agent & Automation Features +6. βš™οΈ System Config & Environment Setup +7. πŸ—ƒ Meta: Feature Comparison, Structure, and Naming +""").strip() + +client = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) +headers = {"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}"} + + +# ───────── helpers ──────────────────────────────────────────────────────── +def utc_iso_now() -> str: + return datetime.datetime.utcnow().replace(microsecond=0, tzinfo=datetime.timezone.utc).isoformat() + +def read_stamp() -> str: + return STAMP_FILE.read_text().strip() if STAMP_FILE.exists() else "1970-01-01T00:00:00Z" + +def save_stamp(): + STAMP_FILE.parent.mkdir(parents=True, exist_ok=True) + STAMP_FILE.write_text(utc_iso_now()) + +def load_cache() -> dict[int, str]: + return json.loads(CACHE_FILE.read_text()) if CACHE_FILE.exists() else {} + +def save_cache(d: dict[int, str]): + CACHE_FILE.parent.mkdir(parents=True, exist_ok=True) + CACHE_FILE.write_text(json.dumps(d, indent=2)) + +def fetch_open_issues(since_iso: str | None = None) -> list[dict]: + issues, page = [], 1 + while True: + url = ( + f"https://api.github.com/repos/{REPO}/issues" + f"?state=open&per_page=100&page={page}" + + (f"&since={since_iso}" if since_iso else "") + ) + chunk = requests.get(url, headers=headers).json() + if not chunk or (isinstance(chunk, dict) and chunk.get("message")): + break + issues.extend(i for i in chunk if "pull_request" not in i) + page += 1 + return issues + + +# ───────── main ─────────────────────────────────────────────────────────── +last_stamp = read_stamp() +changed = fetch_open_issues(since_iso=last_stamp) + +# Fallback if **nothing** changed AND we have *no* existing output +if not changed: + cache_exists = CACHE_FILE.exists() + wiki_exists = pathlib.Path("wiki/Issue-Categories.md").exists() + if not cache_exists or not wiki_exists: + # first run or someone wiped the wiki β†’ build from scratch + print("⏩ First run or empty wiki β€” fetching ALL open issues.", file=sys.stderr) + changed = fetch_open_issues() # full list + else: + print(f"βœ… No issues updated since {last_stamp}. Nothing to classify.", file=sys.stderr) + save_stamp() + sys.exit(0) + +# ---------------------------------------------------------------- prompt +issue_lines = "\n".join(f"- {i['title']} ({i['html_url']})" for i in changed) +prompt = textwrap.dedent(f"""\ +You are an AI assistant helping triage GitHub issues into exactly 7 predefined themes. + +Each issue must go into exactly one of the themes below: + +{THEMES_MD} + +Format your output in Markdown like: +## 🎯 Prompt, Token, and Cost Management +- [#123](https://github.com/org/repo/issues/123) – Title here + +Classify these issues: +{issue_lines} +""") + +resp = client.chat.completions.create( + model="gpt-4.1", + messages=[{"role": "user", "content": prompt}], + temperature=0.2, +) + +md = resp.choices[0].message.content + +# ---------------------------------------------------------------- parse GPT +new_map: dict[int, str] = {} +current = None +for ln in md.splitlines(): + if ln.startswith("##"): + current = ln.lstrip("# ").strip() + elif ln.lstrip().startswith("- [#"): + try: + num = int(ln.split("[#")[1].split("]")[0]) + new_map[num] = current + except Exception: + pass # ignore malformed lines + +cache = load_cache() +cache.update(new_map) +save_cache(cache) +save_stamp() + +# ---------------------------------------------------------------- rebuild wiki +order = [ + "πŸ”— LLM Integration & Provider Support", + "πŸ–₯ App Build & Platform Compatibility", + "🎯 Prompt, Token, and Cost Management", + "🧩 Editor UX & Interaction Design", + "πŸ€– Agent & Automation Features", + "βš™οΈ System Config & Environment Setup", + "πŸ—ƒ Meta: Feature Comparison, Structure, and Naming", +] + +sections: dict[str, list[int]] = {t: [] for t in order} + +# ── fetch ALL current open issues once (PRs filtered out) ──────────────── +title_map: dict[int, tuple[str, str]] = {} +open_now: set[int] = set() + +page = 1 +while True: + batch = fetch_open_issues(since_iso=None) if page == 1 else [] + if not batch: + break + for it in batch: + num = it["number"] + title_map[num] = (it["title"], it["html_url"]) + open_now.add(num) + page += 1 + +# 🧹 drop any cached IDs that are no longer open issues (e.g., became a PR or were closed) +for stale in set(cache) - open_now: + del cache[stale] +save_cache(cache) # persist cleaned cache + +# build sections from cleaned cache +for num, theme in cache.items(): + if theme in sections: # extra safety + sections[theme].append(num) + +# ---------------------------------------------------------------- print roadmap +for theme in order: + issues = sections[theme] + if issues: + print(f"## {theme}") + for n in sorted(issues): + title, url = title_map.get(n, ("(missing)", f"https://github.com/{REPO}/issues/{n}")) + print(f"- [#{n}]({url}) – {title}") + print() diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml new file mode 100644 index 00000000..2f8d5013 --- /dev/null +++ b/.github/workflows/triage.yml @@ -0,0 +1,60 @@ +name: Issue Triage to Wiki + +on: + workflow_dispatch: + schedule: + - cron: '0 */6 * * *' # every 6 hrs (UTC) + +jobs: + roadmap: + runs-on: ubuntu-latest + + steps: + # 1️⃣ Check out code (so the script and cache files are present) + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 1 # shallow clone + + # 2️⃣ Set up Python + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + # 3️⃣ Install dependencies + - name: Install Python dependencies + run: | + pip install openai requests + + # 4️⃣ Clone your fork’s Wiki + - name: Clone your fork's Wiki + run: | + git clone https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.wiki.git wiki + + # 5️⃣ (Optional) Show repo tree for debugging + - name: Show repo tree (debug) + run: | + echo "PWD: $(pwd)" + ls -al + ls -al .github/scripts || true + ls -al void/.github/scripts || true + + # 6️⃣ Generate roadmap and push only if it changed + - name: Generate roadmap directly into wiki + run: | + python .github/scripts/issue_triage.py > wiki/_new.md + if ! cmp -s wiki/_new.md wiki/Issue-Categories.md ; then + mv wiki/_new.md wiki/Issue-Categories.md + cd wiki + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add Issue-Categories.md + git commit -m "Auto-update Issue-Categories.md from GPT triage" + git push + else + echo "No content change – skipping wiki update" + fi + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}