如何从多个GitHub仓库生成并维护单个CHANGELOG.md?
用Python自动化维护多GitHub仓库的统一CHANGELOG.md
完全可以用Python实现这个需求,下面是具体思路、示例脚本和优化建议:
核心实现思路
1. 仓库配置管理
用硬编码列表或配置文件(JSON/YAML)维护所有需要监控的仓库信息(owner、repo名称),方便后续扩展或调整,不用频繁修改脚本逻辑。
2. 拉取仓库变更数据
借助PyGitHub库调用GitHub API,获取每个仓库的发布版本(Releases)、标签(Tags)和提交记录。可以按需过滤:
- 跳过草稿/预发布版本
- 忽略合并提交
- 只保留符合语义化规范的提交(如
feat:/fix:前缀)
3. 统一变更格式
为所有仓库的变更定义统一的排版规则,比如:
- 按仓库分组,用二级标题标识仓库名
- 每个版本用三级标题标注版本号和发布日期
- 提交记录用无序列表展示
4. 增量更新与去重
读取现有CHANGELOG.md,或用单独的状态文件跟踪已处理的版本/提交哈希,只新增未收录的内容,避免重复生成。
5. 自动化触发
将脚本结合GitHub Actions,设置定时任务或监听仓库事件(新tag/push),自动运行脚本并提交更新后的CHANGELOG。
简化示例脚本
首先安装依赖:
pip install PyGitHub
脚本代码:
from github import Github import datetime import os import json # 配置:从环境变量读取GitHub Token,避免硬编码 GITHUB_TOKEN = os.getenv("GITHUB_TOKEN") # 监控的仓库列表 REPOS = [ {"owner": "your-username", "repo": "repo-1"}, {"owner": "your-username", "repo": "repo-2"}, # 添加更多仓库... ] # 状态文件路径,用于跟踪已处理的版本 STATE_FILE = "changelog_state.json" def load_state(): """加载上次处理的状态""" if os.path.exists(STATE_FILE): with open(STATE_FILE, "r") as f: return json.load(f) return {} def save_state(state): """保存最新处理状态""" with open(STATE_FILE, "w") as f: json.dump(state, f, indent=2) def get_repo_changes(repo_obj, last_processed_tag): """获取仓库自上次处理后的新变更""" changes = [] # 按发布时间倒序获取所有版本 releases = sorted(repo_obj.get_releases(), key=lambda r: r.created_at, reverse=True) for release in releases: # 跳过草稿、预发布,以及已经处理过的版本 if release.draft or release.prerelease or release.tag_name == last_processed_tag: continue # 获取版本对应的提交记录 try: # 对比当前版本与上一个版本的差异 prev_release = next((r for r in releases if r.created_at < release.created_at), None) if prev_release: compare = repo_obj.compare(prev_release.tag_name, release.tag_name) commits = compare.commits else: # 第一个版本,取所有提交到该tag的记录 commits = repo_obj.get_commits(sha=release.tag_name) # 过滤提交,只保留有意义的信息 commit_entries = [] for commit in commits: msg_line = commit.commit.message.split("\n")[0] # 跳过合并提交 if not msg_line.startswith("Merge"): commit_entries.append(f"- {msg_line}") # 整理当前版本的变更条目 change_entry = f"### {release.tag_name} ({release.created_at.strftime('%Y-%m-%d')})\n" change_entry += "\n".join(commit_entries) + "\n\n" changes.append({ "repo": repo_obj.name, "entry": change_entry, "tag": release.tag_name, "date": release.created_at.isoformat() }) except Exception as e: print(f"处理仓库 {repo_obj.name} 的版本 {release.tag_name} 时出错: {str(e)}") continue return changes def update_changelog(all_changes): """更新统一的CHANGELOG.md""" changelog_path = "CHANGELOG.md" # 读取现有内容,保留头部信息 if os.path.exists(changelog_path): with open(changelog_path, "r") as f: existing_content = f.read() # 拆分头部和现有变更内容(假设头部到第一个二级标题前) if "## " in existing_content: header, existing_entries = existing_content.split("## ", 1) existing_content = header + "## " + existing_entries else: existing_content = "# CHANGELOG\n\n所有监控仓库的变更记录:\n\n" else: existing_content = "# CHANGELOG\n\n所有监控仓库的变更记录:\n\n" # 按时间倒序排序新变更 sorted_changes = sorted(all_changes, key=lambda x: datetime.datetime.fromisoformat(x["date"]), reverse=True) # 按仓库分组整理新内容 repo_groups = {} for change in sorted_changes: repo_name = change["repo"] if repo_name not in repo_groups: repo_groups[repo_name] = [] repo_groups[repo_name].append(change["entry"]) # 生成新的变更内容,插入到现有内容的头部(最新变更在前) new_entries = "" for repo in sorted(repo_groups.keys()): new_entries += f"## {repo}\n\n" new_entries += "".join(repo_groups[repo]) # 合并现有内容和新内容(避免重复) final_content = new_entries + existing_content # 写入文件 with open(changelog_path, "w") as f: f.write(final_content) def main(): state = load_state() g = Github(GITHUB_TOKEN) all_changes = [] updated_state = state.copy() for repo_config in REPOS: repo_full_name = f"{repo_config['owner']}/{repo_config['repo']}" try: repo = g.get_repo(repo_full_name) print(f"正在处理仓库: {repo_full_name}") last_tag = state.get(repo_full_name, "") repo_changes = get_repo_changes(repo, last_tag) if repo_changes: all_changes.extend(repo_changes) # 更新状态为最新处理的版本 updated_state[repo_full_name] = repo_changes[0]["tag"] except Exception as e: print(f"访问仓库 {repo_full_name} 出错: {str(e)}") continue if all_changes: update_changelog(all_changes) save_state(updated_state) print("CHANGELOG已成功更新") else: print("没有新的变更需要更新") if __name__ == "__main__": main()
优化建议
- 语义化提交过滤:可以在脚本中加入逻辑,只提取符合语义化规范的提交(如
feat:、fix:、docs:),让CHANGELOG更简洁有用。 - 权限安全:GitHub Token只授予必要的
repo权限,在GitHub Actions中用Secrets存储,不要硬编码在脚本里。 - GitHub Actions集成:创建如下
.github/workflows/update-changelog.yml文件,实现自动触发:
name: 更新全局CHANGELOG on: schedule: - cron: "0 0 * * *" # 每天凌晨运行一次 workflow_dispatch: # 支持手动触发 jobs: update: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: 设置Python环境 uses: actions/setup-python@v5 with: python-version: "3.10" - name: 安装依赖 run: pip install PyGitHub - name: 运行更新脚本 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: python update_changelog.py - name: 提交并推送变更 run: | git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" git add CHANGELOG.md changelog_state.json git diff --quiet || git commit -m "自动更新全局CHANGELOG" git push
内容的提问来源于stack exchange,提问作者Ranji Raj
相关产品推荐
相关产品推荐

