ci-workflows/scripts/auto-label.py
feibisi adef8e6ac2
All checks were successful
Go 项目 CI / ci (push) Has been skipped
gitleaks 密钥泄露扫描 / gitleaks (push) Successful in -8h1m14s
TypeScript/JS 项目 CI / ci (push) Has been skipped
WordPress 插件 CI / ci (push) Has been skipped
refactor: auto-label 改为集中式 Python 脚本
- 遍历所有原创仓库的 open PR
- 根据文件类型和 PR 大小自动打标签
- 自动创建缺失的标签(带颜色)
- 跳过已有标签的 PR
2026-02-19 01:16:05 +08:00

165 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""Auto-Label Bot - 遍历所有原创仓库,给未标记的 PR 自动打标签"""
import json
import os
import sys
import urllib.request
API_BASE = "https://feicode.com/api/v1"
TOKEN = os.environ.get("FORGEJO_TOKEN", "")
DRY_RUN = "--dry-run" in sys.argv
# 文件路径 → 标签映射
FILE_LABELS = [
(r".php$", "php", "#4F5D95"),
(r".(js|ts|tsx|jsx)$", "frontend", "#f1e05a"),
(r".(css|scss|less)$", "style", "#e34c26"),
(r".go$", "go", "#00ADD8"),
(r".(yml|yaml)$", "ci/cd", "#0075ca"),
(r"(composer.|package.json|go.mod)", "dependencies", "#0366d6"),
(r".(md|txt|rst)$", "documentation", "#0075ca"),
(r"(Dockerfile|docker-compose)", "docker", "#0db7ed"),
(r".py$", "python", "#3572A5"),
]
SIZE_LABELS = [
(10, "size/S", "#009900"),
(100, "size/M", "#FFCC00"),
(500, "size/L", "#FF6600"),
(float("inf"), "size/XL", "#CC0000"),
]
def api(method, path, data=None):
url = f"{API_BASE}/{path}"
body = json.dumps(data).encode() if data else None
req = urllib.request.Request(url, data=body, method=method)
req.add_header("Authorization", f"token {TOKEN}")
if body:
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())
except Exception as e:
print(f" API error: {method} {path}: {e}", file=sys.stderr)
return None
def get_original_repos():
data = api("GET", "repos/search?limit=50&sort=updated&order=desc")
if not data:
return []
return [r["full_name"] for r in data.get("data", [])
if not r.get("mirror") and not r.get("fork") and not r.get("archived")]
def ensure_label(repo, name, color, label_cache):
if name in label_cache:
return label_cache[name]
result = api("POST", f"repos/{repo}/labels",
{"name": name, "color": color})
if result and "id" in result:
label_cache[name] = result["id"]
return result["id"]
return None
def get_label_cache(repo):
labels = api("GET", f"repos/{repo}/labels?limit=50")
if not labels:
return {}
return {l["name"]: l["id"] for l in labels}
def determine_labels(files, changes):
import re
labels = set()
for f in files:
filename = f.get("filename", "")
for pattern, label, _ in FILE_LABELS:
if re.search(pattern, filename):
labels.add(label)
for threshold, label, _ in SIZE_LABELS:
if changes < threshold:
labels.add(label)
break
return labels
def process_repo(repo):
# 获取最近 7 天内的 open PR
prs = api("GET", f"repos/{repo}/pulls?state=open&sort=updated&limit=20")
if not prs:
return
label_cache = get_label_cache(repo)
has_work = False
for pr in prs:
number = pr["number"]
existing = {l["name"] for l in pr.get("labels", [])}
# 跳过已有文件类型标签或 size 标签的 PR
has_file_label = any(l in existing for _, l, _ in FILE_LABELS)
has_size_label = any(l in existing for _, l, _ in SIZE_LABELS)
if has_file_label and has_size_label:
continue
# 获取 PR 文件列表
files = api("GET", f"repos/{repo}/pulls/{number}/files")
if not files:
continue
changes = pr.get("additions", 0) + pr.get("deletions", 0)
needed = determine_labels(files, changes)
new_labels = needed - existing
if not new_labels:
continue
has_work = True
label_ids = []
for name in new_labels:
color = "#ededed"
for _, ln, lc in FILE_LABELS + SIZE_LABELS:
if ln == name:
color = lc
break
lid = label_cache.get(name) or ensure_label(repo, name, color, label_cache)
if lid:
label_ids.append(lid)
if label_ids:
print(f" {repo}#{number}: +{','.join(new_labels)}")
if not DRY_RUN:
api("POST", f"repos/{repo}/issues/{number}/labels",
{"labels": label_ids})
if not has_work:
pass # 无需标记的 PR
def main():
if not TOKEN:
print("ERROR: FORGEJO_TOKEN not set", file=sys.stderr)
sys.exit(1)
print("=== Auto-Label 开始 ===")
if DRY_RUN:
print("(dry-run 模式)")
repos = get_original_repos()
print(f"发现 {len(repos)} 个原创仓库")
for repo in repos:
print(f"处理 {repo} ...")
process_repo(repo)
print("=== Auto-Label 完成 ===")
if __name__ == "__main__":
main()