Compare commits
2 Commits
burn/659-1
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f514a14f5 | |||
| 2107a8cd73 |
@@ -1,176 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PR Triage Automation -- Categorize, deduplicate, report (#659)."""
|
||||
import argparse, json, os, re, sys, subprocess
|
||||
from collections import Counter, defaultdict
|
||||
from datetime import datetime
|
||||
from urllib.request import Request, urlopen
|
||||
from urllib.error import HTTPError
|
||||
|
||||
|
||||
def _token():
|
||||
t = os.environ.get("GITEA_TOKEN", "")
|
||||
if not t:
|
||||
p = os.path.expanduser("~/.config/gitea/token")
|
||||
if os.path.exists(p):
|
||||
t = open(p).read().strip()
|
||||
return t
|
||||
|
||||
|
||||
def _api(url, token, method="GET", data=None):
|
||||
h = {"Authorization": "token " + token, "Accept": "application/json"}
|
||||
body = json.dumps(data).encode() if data else None
|
||||
if data:
|
||||
h["Content-Type"] = "application/json"
|
||||
req = Request(url, data=body, headers=h, method=method)
|
||||
try:
|
||||
return json.loads(urlopen(req, timeout=30).read())
|
||||
except HTTPError:
|
||||
return None
|
||||
|
||||
|
||||
def fetch_prs(base, token, owner, repo):
|
||||
prs, page = [], 1
|
||||
while True:
|
||||
b = _api(base + "/api/v1/repos/" + owner + "/" + repo + "/pulls?state=open&limit=50&page=" + str(page), token)
|
||||
if not b:
|
||||
break
|
||||
prs.extend(b)
|
||||
if len(b) < 50:
|
||||
break
|
||||
page += 1
|
||||
return prs
|
||||
|
||||
|
||||
def fetch_issues(base, token, owner, repo):
|
||||
iss, page = {}, 1
|
||||
while True:
|
||||
b = _api(base + "/api/v1/repos/" + owner + "/" + repo + "/issues?state=open&limit=50&page=" + str(page), token)
|
||||
if not b:
|
||||
break
|
||||
for i in b:
|
||||
if "pull_request" not in i:
|
||||
iss[i["number"]] = i
|
||||
if len(b) < 50:
|
||||
break
|
||||
page += 1
|
||||
return iss
|
||||
|
||||
|
||||
def categorize(pr):
|
||||
c = (pr.get("title", "") + " " + pr.get("body", "") + " " + " ".join(l.get("name", "") for l in pr.get("labels", []))).lower()
|
||||
for kw, cat in [("training data", "training-data"), ("dpo", "training-data"), ("grpo", "training-data"),
|
||||
("fix:", "bug-fix"), ("bug", "bug-fix"), ("hotfix", "bug-fix"),
|
||||
("feat:", "feature"), ("feature", "feature"),
|
||||
("refactor", "maintenance"), ("cleanup", "maintenance"),
|
||||
("doc", "documentation"), ("test", "testing"), ("infra", "infrastructure")]:
|
||||
if kw in c:
|
||||
return cat
|
||||
return "other"
|
||||
|
||||
|
||||
def refs(pr):
|
||||
return [int(m) for m in re.findall(r"#(\d+)", pr.get("title", "") + " " + pr.get("body", ""))]
|
||||
|
||||
|
||||
def find_duplicates(prs):
|
||||
by = defaultdict(list)
|
||||
for p in prs:
|
||||
for r in refs(p):
|
||||
by[r].append(p)
|
||||
return [g for g in by.values() if len(g) > 1]
|
||||
|
||||
|
||||
def health(pr, issues):
|
||||
r = refs(pr)
|
||||
created = datetime.fromisoformat(pr["created_at"].replace("Z", "+00:00"))
|
||||
updated = datetime.fromisoformat(pr["updated_at"].replace("Z", "+00:00"))
|
||||
now = datetime.now(created.tzinfo)
|
||||
return {
|
||||
"pr": pr["number"], "title": pr["title"], "head": pr["head"]["ref"],
|
||||
"category": categorize(pr), "refs": r,
|
||||
"open": [x for x in r if x in issues], "closed": [x for x in r if x not in issues],
|
||||
"age": (now - created).days, "stale": (now - updated).days,
|
||||
"mergeable": pr.get("mergeable"), "author": pr.get("user", {}).get("login", ""),
|
||||
}
|
||||
|
||||
|
||||
def report(repo, checks, dups):
|
||||
lines = ["# PR Triage -- " + repo,
|
||||
"Generated: " + datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||
"Open PRs: " + str(len(checks)), "", "## Summary", ""]
|
||||
cats = Counter(h["category"] for h in checks)
|
||||
lines.append("| Category | Count |")
|
||||
lines.append("|----------|-------|")
|
||||
for c, n in cats.most_common():
|
||||
lines.append("| " + c + " | " + str(n) + " |")
|
||||
stale = [h for h in checks if h["stale"] > 7]
|
||||
lines.extend(["", "Stale (>7d): " + str(len(stale)),
|
||||
"Duplicate groups: " + str(len(dups)), ""])
|
||||
if dups:
|
||||
lines.append("## Duplicates")
|
||||
for g in dups:
|
||||
rs = set()
|
||||
for p in g:
|
||||
rs.update(refs(p))
|
||||
lines.append("Issues " + ", ".join("#" + str(r) for r in sorted(rs)) + ":")
|
||||
for p in g:
|
||||
lines.append(" - #" + str(p["number"]) + ": " + p["title"])
|
||||
lines.append("")
|
||||
if stale:
|
||||
lines.append("## Stale (>7d)")
|
||||
for h in sorted(stale, key=lambda x: x["stale"], reverse=True):
|
||||
lines.append("- #" + str(h["pr"]) + ": " + h["title"] + " -- " + str(h["stale"]) + "d")
|
||||
lines.append("")
|
||||
lines.append("## All PRs")
|
||||
lines.append("| # | Title | Category | Age | Stale | Merge |")
|
||||
lines.append("|---|-------|----------|-----|-------|-------|")
|
||||
for h in sorted(checks, key=lambda x: x["pr"]):
|
||||
m = "Y" if h["mergeable"] else ("N" if h["mergeable"] is False else "?")
|
||||
s = str(h["stale"]) + "d" if h["stale"] > 7 else "-"
|
||||
lines.append("| " + str(h["pr"]) + " | " + h["title"][:50] + " | " + h["category"] +
|
||||
" | " + str(h["age"]) + "d | " + s + " | " + m + " |")
|
||||
return chr(10).join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="PR Triage Automation")
|
||||
p.add_argument("--base-url", default="https://forge.alexanderwhitestone.com")
|
||||
p.add_argument("--owner", default="Timmy_Foundation")
|
||||
p.add_argument("--repo", default="")
|
||||
p.add_argument("--json", action="store_true", dest="js")
|
||||
p.add_argument("--output", default="")
|
||||
a = p.parse_args()
|
||||
token = _token()
|
||||
if not token:
|
||||
print("No token"); sys.exit(1)
|
||||
repo = a.repo
|
||||
if not repo:
|
||||
try:
|
||||
remote = subprocess.check_output(["git", "remote", "get-url", "origin"], text=True).strip()
|
||||
m = re.search(r"[/:](\w[\w-]*)/(\w[\w-]*?)(?:\.git)?$", remote)
|
||||
if m:
|
||||
a.owner, repo = m.group(1), m.group(2)
|
||||
except Exception:
|
||||
pass
|
||||
if not repo:
|
||||
print("No repo specified"); sys.exit(1)
|
||||
print("Triaging " + a.owner + "/" + repo + "...", file=sys.stderr)
|
||||
prs = fetch_prs(a.base_url, token, a.owner, repo)
|
||||
issues = fetch_issues(a.base_url, token, a.owner, repo)
|
||||
checks = [health(pr, issues) for pr in prs]
|
||||
dups = find_duplicates(prs)
|
||||
if a.js:
|
||||
print(json.dumps({"repo": repo, "prs": checks,
|
||||
"duplicates": [[{"number": p["number"], "title": p["title"]} for p in g] for g in dups]},
|
||||
indent=2))
|
||||
else:
|
||||
r = report(repo, checks, dups)
|
||||
print(r)
|
||||
if a.output:
|
||||
with open(a.output, "w") as f:
|
||||
f.write(r)
|
||||
print("\n" + str(len(checks)) + " PRs, " + str(len(dups)) + " duplicate groups", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
24
tasks.py
24
tasks.py
@@ -616,22 +616,26 @@ def normalize_candidate_entry(candidate, batch_id, index):
|
||||
|
||||
|
||||
def normalize_training_examples(examples, batch_id, tweet_ids, fallback_prompt, fallback_response):
|
||||
_CORE_FIELDS = {"prompt", "instruction", "response", "answer", "task_type"}
|
||||
normalized = []
|
||||
for index, example in enumerate(examples, start=1):
|
||||
prompt = str(example.get("prompt") or example.get("instruction") or "").strip()
|
||||
response = str(example.get("response") or example.get("answer") or "").strip()
|
||||
if not prompt or not response:
|
||||
continue
|
||||
normalized.append(
|
||||
{
|
||||
"example_id": f"{batch_id}-example-{index:02d}",
|
||||
"batch_id": batch_id,
|
||||
"task_type": str(example.get("task_type") or "analysis").strip() or "analysis",
|
||||
"prompt": prompt,
|
||||
"response": response,
|
||||
"tweet_ids": tweet_ids,
|
||||
}
|
||||
)
|
||||
entry = {
|
||||
"example_id": f"{batch_id}-example-{index:02d}",
|
||||
"batch_id": batch_id,
|
||||
"task_type": str(example.get("task_type") or "analysis").strip() or "analysis",
|
||||
"prompt": prompt,
|
||||
"response": response,
|
||||
"tweet_ids": tweet_ids,
|
||||
}
|
||||
# Preserve optional metadata fields (category, tags, source_issue, etc.)
|
||||
for key, value in example.items():
|
||||
if key not in _CORE_FIELDS and key not in entry and value is not None:
|
||||
entry[key] = value
|
||||
normalized.append(entry)
|
||||
if normalized:
|
||||
return normalized
|
||||
return [
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
"""Tests for PR triage automation (#659)."""
|
||||
import pytest
|
||||
|
||||
class TestCategorize:
|
||||
def _pr(self, title="", body=""):
|
||||
return {"title": title, "body": body, "labels": []}
|
||||
|
||||
def test_training(self):
|
||||
from scripts.pr_triage import categorize
|
||||
assert categorize(self._pr("Add DPO pairs")) == "training-data"
|
||||
|
||||
def test_bug(self):
|
||||
from scripts.pr_triage import categorize
|
||||
assert categorize(self._pr("fix: crash")) == "bug-fix"
|
||||
|
||||
def test_feature(self):
|
||||
from scripts.pr_triage import categorize
|
||||
assert categorize(self._pr("feat: dark mode")) == "feature"
|
||||
|
||||
def test_other(self):
|
||||
from scripts.pr_triage import categorize
|
||||
assert categorize(self._pr("random")) == "other"
|
||||
|
||||
class TestRefs:
|
||||
def test_simple(self):
|
||||
from scripts.pr_triage import refs
|
||||
assert 123 in refs({"title": "Fix #123", "body": ""})
|
||||
|
||||
def test_multiple(self):
|
||||
from scripts.pr_triage import refs
|
||||
r = refs({"title": "", "body": "Closes #100, Refs #200"})
|
||||
assert 100 in r and 200 in r
|
||||
|
||||
class TestDuplicates:
|
||||
def test_found(self):
|
||||
from scripts.pr_triage import find_duplicates
|
||||
prs = [{"title": "", "body": "Fix #1", "number": 1, "head": {"ref": "a"}, "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z", "user": {}},
|
||||
{"title": "", "body": "Refs #1", "number": 2, "head": {"ref": "b"}, "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z", "user": {}}]
|
||||
assert len(find_duplicates(prs)) == 1
|
||||
|
||||
def test_none(self):
|
||||
from scripts.pr_triage import find_duplicates
|
||||
prs = [{"title": "", "body": "Fix #1", "number": 1, "head": {"ref": "a"}, "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z", "user": {}},
|
||||
{"title": "", "body": "Fix #2", "number": 2, "head": {"ref": "b"}, "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z", "user": {}}]
|
||||
assert find_duplicates(prs) == []
|
||||
@@ -323,6 +323,89 @@ class TestNormalizeTrainingExamples:
|
||||
assert result[0]["response"] == "A1"
|
||||
|
||||
|
||||
def test_metadata_category_preserved(self):
|
||||
"""Category metadata passes through normalization."""
|
||||
examples = [
|
||||
{"prompt": "Q1", "response": "A1", "category": "crisis-response"},
|
||||
]
|
||||
result = normalize_training_examples(
|
||||
examples, "b001", ["t1"], "fp", "fr"
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert result[0]["category"] == "crisis-response"
|
||||
|
||||
def test_metadata_tags_preserved(self):
|
||||
"""Tags metadata passes through normalization."""
|
||||
examples = [
|
||||
{"prompt": "Q1", "response": "A1", "tags": ["manipulation", "edge-case"]},
|
||||
]
|
||||
result = normalize_training_examples(
|
||||
examples, "b001", ["t1"], "fp", "fr"
|
||||
)
|
||||
assert result[0]["tags"] == ["manipulation", "edge-case"]
|
||||
|
||||
def test_metadata_source_issue_preserved(self):
|
||||
"""Source issue metadata passes through normalization."""
|
||||
examples = [
|
||||
{"prompt": "Q1", "response": "A1", "source_issue": 598},
|
||||
]
|
||||
result = normalize_training_examples(
|
||||
examples, "b001", ["t1"], "fp", "fr"
|
||||
)
|
||||
assert result[0]["source_issue"] == 598
|
||||
|
||||
def test_multiple_metadata_fields_preserved(self):
|
||||
"""All metadata fields pass through together."""
|
||||
examples = [
|
||||
{
|
||||
"prompt": "Q1",
|
||||
"response": "A1",
|
||||
"category": "boundary-test",
|
||||
"tags": ["joking"],
|
||||
"source_issue": 598,
|
||||
"difficulty": "hard",
|
||||
},
|
||||
]
|
||||
result = normalize_training_examples(
|
||||
examples, "b001", ["t1"], "fp", "fr"
|
||||
)
|
||||
assert result[0]["category"] == "boundary-test"
|
||||
assert result[0]["tags"] == ["joking"]
|
||||
assert result[0]["source_issue"] == 598
|
||||
assert result[0]["difficulty"] == "hard"
|
||||
|
||||
def test_metadata_does_not_override_core_fields(self):
|
||||
"""Metadata cannot override core fields like prompt, response, batch_id."""
|
||||
examples = [
|
||||
{"prompt": "Q1", "response": "A1", "batch_id": "SHOULD_NOT_APPEAR"},
|
||||
]
|
||||
result = normalize_training_examples(
|
||||
examples, "b001", ["t1"], "fp", "fr"
|
||||
)
|
||||
assert result[0]["batch_id"] == "b001" # Original batch_id wins
|
||||
|
||||
def test_no_metadata_backward_compatible(self):
|
||||
"""Examples without metadata still work exactly as before."""
|
||||
examples = [
|
||||
{"prompt": "Q1", "response": "A1", "task_type": "analysis"},
|
||||
]
|
||||
result = normalize_training_examples(
|
||||
examples, "b001", ["t1"], "fp", "fr"
|
||||
)
|
||||
expected_keys = {"example_id", "batch_id", "task_type", "prompt", "response", "tweet_ids"}
|
||||
assert set(result[0].keys()) == expected_keys
|
||||
|
||||
def test_none_metadata_values_skipped(self):
|
||||
"""None metadata values are not added."""
|
||||
examples = [
|
||||
{"prompt": "Q1", "response": "A1", "category": None},
|
||||
]
|
||||
result = normalize_training_examples(
|
||||
examples, "b001", ["t1"], "fp", "fr"
|
||||
)
|
||||
assert "category" not in result[0]
|
||||
|
||||
|
||||
class TestNormalizeRubricScores:
|
||||
"""normalize_rubric_scores() cleans eval rubric output."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user