Compare commits

...

3 Commits

Author SHA1 Message Date
xiaoxia 3b9c2b8ec3 style: isort + black format generate_changelog.py
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Successful in 32s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m9s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m11s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m10s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
2026-07-14 15:10:00 +08:00
xiaoxia 71351b6399 style: black format generate_changelog.py
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 21s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m20s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m27s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m59s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
2026-07-14 15:01:17 +08:00
xiaoxia-bot 3b6c4b77f0 feat(ci): 新增CHANGELOG自动生成脚本 - 从Gitea API获取合并PR并分类整理
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 15s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 57s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m10s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 3m39s
2026-07-14 11:29:59 +08:00
+183
View File
@@ -0,0 +1,183 @@
#!/usr/bin/env python3
"""
自动生成 CHANGELOG 条目。
用法:
python3 scripts/generate_changelog.py v0.1.128 v0.1.129
python3 scripts/generate_changelog.py v0.1.128 HEAD
"""
from __future__ import annotations
import json
import os
import re
import sys
import urllib.error
import urllib.request
from datetime import datetime
GITEA_URL = os.environ.get("GITEA_URL", "https://git.xiaoxiajianji.com")
REPO = os.environ.get("GITEA_REPO", "xiaoxia/xiaoxia-saas")
TOKEN = os.environ.get("GITEA_TOKEN", "")
def gitea_api(path: str) -> dict | list:
url = f"{GITEA_URL}/api/v1{path}"
req = urllib.request.Request(url)
if TOKEN:
req.add_header("Authorization", f"token {TOKEN}")
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
print(f"API Error: {e.code} {e.reason}", file=sys.stderr)
raise
def get_tag_date(tag: str) -> str:
try:
info = gitea_api(f"/repos/{REPO}/git/refs/tags/{tag}")
if isinstance(info, dict):
sha = info.get("object", {}).get("sha", "")
if sha:
commit = gitea_api(f"/repos/{REPO}/git/commits/{sha}")
if isinstance(commit, dict):
return commit.get("committer", {}).get("date", "")[:10]
except Exception:
pass
return ""
def get_merged_prs_between(from_tag: str, to_tag: str) -> list[dict]:
all_prs: list[dict] = []
page = 1
while True:
prs = gitea_api(f"/repos/{REPO}/pulls?state=closed&sort=merged&direction=desc" f"&per_page=50&page={page}")
if not isinstance(prs, list) or not prs:
break
all_prs.extend(prs)
if len(prs) < 50:
break
page += 1
if page > 10:
break
merged = [pr for pr in all_prs if pr.get("merged_at")]
from_date = get_tag_date(from_tag)
to_date = get_tag_date(to_tag) if not to_tag.startswith("HEAD") else datetime.now().strftime("%Y-%m-%d")
if not from_date:
return merged[:50]
result = []
for pr in merged:
merged_at = pr.get("merged_at", "")[:10]
if from_date <= merged_at <= to_date:
result.append(pr)
return result
def categorize_pr(title: str) -> tuple[str, str]:
title = title.strip()
lower = title.lower()
m = re.match(r"^(feat|fix|chore|perf|docs|refactor|test|ci|style|build|security)\s*[:]", title)
if m:
prefix = m.group(1)
clean_title = title[m.end() :].strip()
else:
prefix = ""
clean_title = title
if prefix in ("feat", "feature"):
return "✨ 功能", clean_title
elif prefix == "fix":
return "🐛 Bug 修复", clean_title
elif prefix in ("refactor", "chore", "style"):
return "🔄 重构与清理", clean_title
elif prefix in ("perf", "performance"):
return "⚡ 性能优化", clean_title
elif prefix == "security":
return "🔒 安全修复", clean_title
elif prefix == "docs":
return "📝 文档", clean_title
elif prefix == "test":
return "🧪 测试", clean_title
elif prefix in ("ci", "build"):
return "🚀 CI/CD & 基础设施", clean_title
else:
if any(k in lower for k in ["安全", "security", "cve", "漏洞"]):
return "🔒 安全修复", clean_title
elif any(k in lower for k in ["修复", "bug"]):
return "🐛 Bug 修复", clean_title
elif any(k in lower for k in ["新增", "添加", "feat", "功能"]):
return "✨ 功能", clean_title
elif any(k in lower for k in ["ci", "构建", "workflow", "pipeline"]):
return "🚀 CI/CD & 基础设施", clean_title
elif any(k in lower for k in ["测试", "test", "e2e"]):
return "🧪 测试", clean_title
else:
return "📌 其他", clean_title
def generate_changelog(from_tag: str, to_tag: str, version: str = "") -> str:
if not version:
version = to_tag
prs = get_merged_prs_between(from_tag, to_tag)
categories: dict[str, list[tuple[int, str]]] = {}
for pr in prs:
cat, title = categorize_pr(pr["title"])
pr_num = pr["number"]
categories.setdefault(cat, []).append((pr_num, title))
order = [
"🔒 安全修复",
"✨ 功能",
"🐛 Bug 修复",
"⚡ 性能优化",
"🔄 重构与清理",
"📝 文档",
"🧪 测试",
"🚀 CI/CD & 基础设施",
"📌 其他",
]
date_str = get_tag_date(to_tag) if not to_tag.startswith("HEAD") else datetime.now().strftime("%Y-%m-%d")
lines = [f"## [{version}] - {date_str}", ""]
for cat in order:
items = categories.get(cat, [])
if not items:
continue
lines.append(f"### {cat}")
lines.append("")
for num, title in sorted(items, key=lambda x: x[0]):
short_title = title.split("")[0].split(" - ")[0]
if len(short_title) > 80:
short_title = short_title[:77] + "..."
lines.append(f"- #{num} {short_title}")
lines.append("")
lines.append("---")
lines.append("")
return "\n".join(lines)
def main():
if len(sys.argv) < 3:
print(f"用法: {sys.argv[0]} <from_tag> <to_tag> [version]")
sys.exit(1)
from_tag = sys.argv[1]
to_tag = sys.argv[2]
version = sys.argv[3] if len(sys.argv) > 3 else ""
changelog = generate_changelog(from_tag, to_tag, version)
print(changelog)
if __name__ == "__main__":
main()