维护开源项目或工具清单时,经常会遇到一个问题:数据文件改了,README 忘记同步。时间一长,两边对不上,要么手动逐条复制,要么干脆摆烂。
这篇文章用一个真实场景——书签工具清单——来演示怎么用 GitHub Actions + Python 脚本,实现「只改 bookmarks.yaml,README 自动更新」的工作流。
思路
整个方案分两部分:
- Python 脚本:读取
bookmarks.yaml,按固定格式生成README.md - GitHub Actions:监听
bookmarks.yaml的变动,自动触发脚本并提交结果
改动 bookmarks.yaml 之后,不管是本地 push 还是直接在 GitHub 网页上编辑,都会自动触发,不需要手动运行任何命令。
数据文件结构
bookmarks.yaml 的结构很简单,每个分类包含图标、名称和工具列表:
- category: 编程工具
icon: 🛠️
items:
- name: Zed
url: https://zed.dev/
desc: 轻量极速的现代代码编辑器
opensource: true
- name: WezTerm
url: https://wezfurlong.org/wezterm
desc: 高性能跨平台终端模拟器
opensource: true
opensource: true 是可选字段,没有这个字段默认视为非开源。
Python 脚本
新建 gen_readme.py,放在仓库根目录:
#!/usr/bin/env python3
import yaml
HEADER = """# 🛠️ tools
> 精选隐私友好、开源优先的实用工具清单。
> A curated list of privacy-friendly, open-source tools I actually use.
🔓 = 开源 / Open Source
---
"""
FOOTER = """
---
欢迎提 Issue 或 PR 推荐新工具。
"""
def generate_readme(yaml_path="bookmarks.yaml", output_path="README.md"):
with open(yaml_path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
lines = [HEADER]
for category in data:
icon = category.get("icon", "")
name = category.get("category", "")
items = category.get("items", [])
lines.append(f"## {icon} {name}\n")
lines.append("| 工具 | 描述 | 开源 |")
lines.append("|------|------|------|")
for item in items:
tool_name = item.get("name", "")
url = item.get("url", "")
desc = item.get("desc", "")
opensource = "🔓" if item.get("opensource") else ""
lines.append(f"| [{tool_name}]({url}) | {desc} | {opensource} |")
lines.append("")
lines.append(FOOTER.strip())
with open(output_path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
print(f"✅ README.md 已生成,共 {len(data)} 个分类")
if __name__ == "__main__":
generate_readme()
几个关键点:
yaml.safe_load()而不是yaml.load()——safe 版本不会执行 YAML 中的任意 Python 对象,更安全category.get("icon", "")用get加默认值,字段缺失时不会报错"🔓" if item.get("opensource") else ""——get找不到字段返回None,条件判断直接当False处理,所以不需要额外判断
GitHub Actions 配置
在仓库里新建 .github/workflows/update-readme.yml。
GitHub 网页上怎么新建带目录的文件:
GitHub 不支持直接新建空文件夹,但可以在新建文件时顺带创建目录。在仓库主页点 Add file → Create new file,文件名输入框里直接输入完整路径:
.github/workflows/update-readme.yml
输入 / 的时候,GitHub 会自动把前面的部分折叠成目录层级,最终效果就是在 .github/workflows/ 下创建了 update-readme.yml。粘贴内容后点右上角 Commit changes 保存。
内容如下:
name: Update README
on:
push:
paths:
- bookmarks.yaml
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install dependencies
run: pip install pyyaml
- name: Generate README
run: python gen_readme.py
- name: Commit and push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add README.md
git diff --staged --quiet || git commit -m "docs: auto-update README from bookmarks.yaml"
git push
逐段说明:
触发条件
on:
push:
paths:
- bookmarks.yaml
paths 过滤器让 Actions 只在 bookmarks.yaml 有变动时才触发,改其他文件不会白跑一次。
运行环境
runs-on: ubuntu-latest
在 GitHub 提供的 Ubuntu 虚拟机上运行,免费额度对个人项目完全够用。
checkout
- uses: actions/checkout@v4
把仓库代码拉到虚拟机里,后续步骤才能读取文件。@v4 是版本号,锁定版本避免上游更新导致行为变化。
提交逻辑
git diff --staged --quiet || git commit -m "..."
这一行是关键:git diff --staged --quiet 检查暂存区有没有变化,没变化退出码为 0(静默成功);|| 表示前面命令失败(有变化)才执行后面的 commit。这样避免了「README 没变但强行提交空 commit」的情况。
仓库结构
最终仓库文件结构:
tools/
├── bookmarks.yaml # 只需要维护这一个
├── gen_readme.py # 生成脚本
├── README.md # 自动生成,不要手动改
└── .github/
└── workflows/
└── update-readme.yml
README.md 不再手动维护,所有改动都通过 bookmarks.yaml 进行。
必要设置:开启写入权限
配置完之后第一次触发,可能会遇到这个报错:
remote: Permission to xxx/tools.git denied to github-actions[bot].
fatal: unable to access '...': The requested URL returned error: 403
原因是 GitHub Actions 默认只有只读权限,bot 无法往仓库写入。去仓库设置里开一下:
Settings → Actions → General → Workflow permissions → Read and write permissions
保存后重新提交一次 bookmarks.yaml 触发即可。
效果
推送 bookmarks.yaml 后,去仓库的 Actions 标签页可以看到工作流运行状态。成功后会多出一条 bot 提交的 commit,README.md 已经同步更新。
这套方案的核心思路——单一数据源,自动派生其他格式——可以推广到很多类似场景:用 JSON 维护数据、自动生成文档站;用 CSV 维护变更日志、自动更新 CHANGELOG 等等。