#!/usr/bin/env python3 from __future__ import annotations import os import re import subprocess from collections import OrderedDict from pathlib import Path CURRENT_TAG = os.environ["CURRENT_TAG"] PREVIOUS_TAG = os.environ.get("PREVIOUS_TAG", "") RELEASE_BRANCH = os.environ.get("RELEASE_BRANCH", "") GITHUB_REPOSITORY = os.environ.get("GITHUB_REPOSITORY", "zoujingli/ThinkAdmin") GROUPS = OrderedDict([ ("feat", "新增功能"), ("fix", "问题修复"), ("refactor", "重构调整"), ("perf", "性能优化"), ("pref", "性能优化"), ("style", "样式调整"), ("docs", "文档更新"), ("test", "测试质量"), ("build", "构建发布"), ("ci", "持续集成"), ("chore", "工程维护"), ("other", "其他变更"), ]) PREFIX_RE = re.compile(r"^(?P[a-z]+)(?:\((?P[^)]+)\))?[::]\s*(?P.+)$") def git_lines(*args: str) -> list[str]: out = subprocess.check_output(["git", *args], text=True) return [line for line in out.splitlines() if line] def release_range() -> str: if PREVIOUS_TAG: return f"{PREVIOUS_TAG}..{CURRENT_TAG}" return CURRENT_TAG def commits() -> list[tuple[str, str]]: lines = git_lines("log", "--pretty=format:%H%x01%s", release_range()) result: list[tuple[str, str]] = [] for line in lines: sha, subject = line.split("\x01", 1) result.append((sha, subject)) return result def normalize(subject: str) -> tuple[str, str]: match = PREFIX_RE.match(subject) if not match: return "other", subject typ = match.group("type") title = match.group("title") if typ not in GROUPS: typ = "other" scope = match.group("scope") if scope: title = f"【{scope}】{title}" return typ, title def main() -> None: grouped: dict[str, list[str]] = {key: [] for key in GROUPS} all_commits = commits() for sha, subject in all_commits: typ, title = normalize(subject) grouped[typ].append(f"- {title} ({sha[:8]})") lines: list[str] = [] lines.append(f"## Release {CURRENT_TAG}") lines.append("") if RELEASE_BRANCH: lines.append(f"- 发布分支:`{RELEASE_BRANCH}`") if PREVIOUS_TAG: compare_url = f"https://github.com/{GITHUB_REPOSITORY}/compare/{PREVIOUS_TAG}...{CURRENT_TAG}" lines.append(f"- 对比范围:[`{PREVIOUS_TAG}...{CURRENT_TAG}`]({compare_url})") else: lines.append("- 对比范围:首次发布标签") lines.append(f"- 提交数量:{len(all_commits)}") lines.append("") lines.append("## 本次变更摘要") lines.append("") summary = [f"{label} {len(items)} 项" for key, label in GROUPS.items() if (items := grouped[key])] lines.append("、".join(summary) if summary else "- 本次发布没有检测到提交变更。") lines.append("") lines.append("## 变更明细") lines.append("") for key, label in GROUPS.items(): items = grouped[key] if not items: continue lines.append(f"### {label}") lines.extend(items) lines.append("") lines.append("## 发布说明") lines.append("") lines.append("- 主仓库 Release 由 GitHub Actions 自动创建。") lines.append("- 插件仓库会同步推送同名分支和同名 Tag,Packagist 可通过 GitHub Hook 自动刷新。") lines.append("- v6 与 v8 使用不同主版本号 Tag,避免两个开发分支发布互相覆盖。") lines.append("") out = Path("log") / f"{CURRENT_TAG}.md" out.parent.mkdir(parents=True, exist_ok=True) out.write_text("\n".join(lines), encoding="utf-8") print(out) if __name__ == "__main__": main()