【学习笔记】搭建 git-cliff + Agent + GitHub Action 的自动化 Changelog 闭环
一篇通用技术笔记:如何用 git-cliff(生成 CHANGELOG 骨架)+ Agent skill(编排发版与注入 AI 摘要)+ GitHub Action(自动创建 Release)三件套,为任意 git 项目搭建从提交到发布的全自动 changelog 闭环。
适用对象:任何采用 Conventional Commits、以 git tag 驱动发布的仓库。不限定语言、不限定框架。文中的踩坑与适配方案(非规范历史处理、多段提交截断、gitignore 逐层穿透、数据快照取舍)对所有项目通用。
0. TL;DR
体系由 3 个组件协作,输出单文件 CHANGELOG.md,每次发布前置插入一个新版本段:
| 组件 | 职责 | 落点 |
|---|---|---|
| 生成器(git-cliff) | 解析 git 历史,按 Conventional Commits 分组,渲染 CHANGELOG 骨架 | cliff.toml |
| 编排器(Agent skill) | 定版本 → 预览 → 生成 → 注入 AI 摘要 → 同步版本号 → tag → push | agent skill 文件 |
| 自动化(GitHub Action) | push v* tag 时从 CHANGELOG 切片,自动创建 GitHub Release | .github/workflows/release.yml |
闭环流程:
Conventional Commit ──┐
▼
Agent release skill
│
┌─────────┼─────────┐
▼ ▼ ▼
git-cliff AI 摘要 版本号同步
生成骨架 注入占位符 (version 文件)
└─────────┼─────────┘
▼
git tag + push
│
▼
GitHub Action 自动建 Release阅读路线:§1–§2 是选型与全景,快速了解”为什么”和”怎么搭”;§3–§7 是各组件的配置与踩坑详解(每个组件自成一章,配置 + 决策 + 踩坑放在一起);§8 是从零复现的有序步骤;附录 A 是可直接复制的 skill 模板。
1. 为什么选 git-cliff
常见的 CHANGELOG / 发版方案对比:
| 方案 | 依赖 | 适用场景 | 局限 |
|---|---|---|---|
| changesets / semantic-release / standard-version | Node | JS/TS 项目 | 强制引入 Node 运行时,非 JS 项目不友好 |
| release-please (GitHub Action) | 无(独立二进制) | 任意项目,Google 出品 | 配置项多,对非 package.json 的版本源支持需额外配置 |
| 手写脚本 | 无 | 极简项目 | 分组、链接、摘要都要自己实现,维护成本高 |
| git-cliff | Rust 单二进制 | 任意项目 | 需单独安装(cargo/winget/brew) |
选 git-cliff 的三个决定性理由:
- 零运行时依赖——单个 Rust 二进制,
winget install git-cliff/cargo install git-cliff/brew install git-cliff即装,不污染项目的语言工具链。对 Python / Go / Rust / 纯文档项目都同样友好。 - 模板灵活——用 Tera(Jinja2 风格)模板,可以在 body 里放任意占位符(如
<!-- AI_SUMMARY -->),agent 后处理替换。这是「结构化骨架 + AI 摘要」混合方案的基础,纯 CI 方案做不到。 - 配置即文档——单个
cliff.toml描述分组、过滤、模板、bump 规则,可读、可 review、可进版本库。
2. 体系架构
三组件各司其职,通过 git tag 串联成一个闭环:
┌─────────────────────────────────────────────────────┐
│ 开发阶段 │
│ Conventional Commits (feat/fix/refactor/...) │
│ → 每次提交自动归类,无需手写 changelog 条目 │
└──────────────────────┬──────────────────────────────┘
│ 对 agent 说「发布」
▼
┌─────────────────────────────────────────────────────┐
│ 发布阶段 (Agent release skill) │
│ │
│ 1. 读 tag → 定版本号 (PATCH/MINOR/MAJOR) │
│ 2. git cliff --prepend → 生成骨架 │
│ 3. 替换 <!-- AI_SUMMARY --> → 注入自然语言摘要 │
│ 4. 同步版本号 (plugin.json / 各组件 version) │
│ 5. git tag + push │
└──────────────────────┬──────────────────────────────┘
│ push v* tag
▼
┌─────────────────────────────────────────────────────┐
│ 自动化阶段 (GitHub Action) │
│ │
│ tag push 触发 → awk 切片 CHANGELOG → 建 Release │
└─────────────────────────────────────────────────────┘核心分工:
- 结构交给工具——git-cliff 按 commit 类型自动分组、生成链接
- 语义交给 AI——agent 注入 2-3 句自然语言摘要,概括”本次发布干了什么”
- 决策交给人——Step 2 预览后必须人工确认才继续
下面按组件逐章详解(配置 + 关键决策 + 踩坑在一起,无需跨节跳读)。
3. git-cliff:配置与调优
3.1 完整配置(cliff.toml)
核心设计三点:HTML 注释前缀排序、AI 摘要占位符、兜底 parser。
[changelog]
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
body = """
{% if version %}## {{ version }} ({{ timestamp | date(format="%Y-%m-%d") }}){% else %}## [Unreleased]{% endif %}
{% if previous.version %}**[Full diff](https://github.com/<owner>/<repo>/compare/{{ previous.version }}...{{ version }})**{% endif %}
<!-- AI_SUMMARY -->
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim }}
{% for commit in commits %}
- {% if commit.scope %}**{{ commit.scope }}**: {% endif %}\
{{ commit.message | split(pat="\n") | first | upper_first | trim }}\
{% if commit.breaking %} (**BREAKING**){% endif %}\
([{{ commit.id | truncate(length=7, end="") }}](https://github.com/<owner>/<repo>/commit/{{ commit.id }}))\
{% endfor %}
{% endfor %}
"""
trim = true
footer = """
<!-- generated by git-cliff -->
"""
[git]
conventional_commits = true
filter_unconventional = false
commit_parsers = [
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
{ message = "^refactor", group = "<!-- 2 -->🔨 Refactor" },
{ message = "^perf", group = "<!-- 5 -->⚡ Performance" },
{ message = "^doc", group = "<!-- 6 -->📝 Documentation" },
{ message = "^chore\\(release\\)", skip = true },
{ message = "^chore|^ci", group = "<!-- 8 -->⚙️ Miscellaneous" },
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
{ message = "^Merge", skip = true },
# 兜底(见 §3.3):捕获非 Conventional 历史提交,避免丢失
{ body = ".*", group = "<!-- 10 -->📦 Other Changes" },
]
protect_breaking_commits = false
filter_commits = false
tag_pattern = "v\\d+"
sort_commits = "oldest"
[bump]
initial_tag = "0.0.1"替换
<owner>/<repo>为实际仓库地址(git remote get-url origin可读出)。
3.2 关键设计:HTML 注释前缀排序
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },模板里 {{ group | striptags | trim }} 会剥掉 <!-- N -->,渲染后不可见但保证排序——比依赖 emoji 字符序更可靠,分组顺序永远稳定。
3.3 踩坑:非 Conventional 历史提交消失
现象:照搬标准 cliff.toml 后,git cliff --tag v1.0.0 --unreleased 预览空空如也——历史 commit 一个都没渲染。
根因:很多项目早期提交是非 Conventional 风格(Add ... / Fix ... / Refactor ...,没有 feat: / fix: 前缀)。配置里:
conventional_commits = true→ git-cliff 按 Conventional Commits 解析filter_unconventional = false→ 非规范提交保留,但不解析commit_parsers只匹配^feat/^fix/ … → 非规范提交匹配不到任何 parser → 没有归属 group → 模板{% for group, commits in commits | group_by(attribute="group") %}按 group 聚合时,无 group 的提交不渲染
解决:在 commit_parsers 末尾加兜底规则(见上方配置第 10 条):
{ body = ".*", group = "<!-- 10 -->📦 Other Changes" },git-cliff 按顺序评估 parser,第一条匹配生效,所以放最后的无条件规则兜底所有未匹配提交。
⚠️ 坑(实测,文档未明确):
{ group = "..." }(无条件,仅 group 字段)不生效——预览还是空。{ regex = ".*", group = "..." }也不生效。- 必须
{ body = ".*", group = "..." }才生效(匹配每个剩余 commit 的 body 字段)。这是 git-cliff 2.13.1 的实际行为,靠逐个实测才发现。
3.4 踩坑:多段 commit message 正文泄漏
现象:兜底规则生效后,多段提交的正文也灌进了 CHANGELOG:
- Add Wechat RSS monitoring scripts and related functionality
- Implement check_updates.py to fetch and check Wechat2RSS feeds...
- Create fetch_articles.py to retrieve full article content...本应是干净的单行。Conventional 项目不遇到这问题(只有 subject 行),但兜底组里的历史提交通常是多段的。
根因:commit.message 字段对多段提交包含完整正文(含换行)。
失败的尝试(Rust regex crate 限制):
commit.preprocessors = [
{ pattern = '(?s)\n.*', replace = "" }, # ❌ 无效
{ pattern = '\n[\s\S]*', replace = "" }, # ❌ 无效
{ pattern = '(?m)\n.*$', replace = "" }, # ❌ 无效
]git-cliff 的 preprocessor 用 Rust regex crate,默认 . 不匹配 \n,这些跨行写法都不生效。
解决:不用 preprocessor,改在模板层取首行——用 Tera 的 split + first(见上方 body 模板):
{{ commit.message | split(pat="\n") | first | upper_first | trim }}split(pat="\n") 把多行 message 按换行切成数组,first 取第一行。干净、可靠、无副作用,对所有提交(单行/多段)都正确。
3.5 AI 摘要占位符机制
模板 body 里留 <!-- AI_SUMMARY -->,git-cliff 不解析它(原样输出),release skill(§4 Step 3)用真实摘要替换。这让「分组骨架」和「自然语言摘要」解耦:git-cliff 负责结构,AI 负责语义。
4. release skill:发布编排工作流
4.1 工作流总览
一个 agent skill,用自然语言触发(「发布」「release」「发版」「打 tag」),workflow 分 5 步,Step 2 后必须人工确认:
Step 0 前置检查 git-cliff 已装 + 工作树干净
Step 1 定版本号 读上一个 tag → 按 PATCH/MINOR/MAJOR 递增
Step 2 预览 git cliff --unreleased + 变更分析 → 展示摘要
─── 等待用户确认 ───
Step 3 生成 首次 -o / 后续 --prepend → 校验历史未丢 → 替换 AI_SUMMARY → commit
Step 3.5 同步版本号 更新版本源文件(见 §5)
Step 4 tag + push git tag -a + git push origin HEAD --tags这个编排逻辑与具体 agent 平台无关——可以是 ZCode / Claude Code 的 skill,也可以是任何能执行 shell 命令的 agent,甚至是一个普通 shell 脚本。完整可复制的模板见附录 A。
4.2 定版本号(Step 1)
采用语义化版本 v<MAJOR>.<MINOR>.<PATCH>。读上一个 tag,按变更规模递增:
| 类型 | 规则 | 适用场景 |
|---|---|---|
| PATCH | v1.0.x → v1.0.x+1 | 修 bug、文档、chore、配置同步 |
| MINOR | v1.x.* → v1.x+1.0 | 新增功能、能力扩展 |
| MAJOR | vx.*.* → v+1.0.0 | 架构级重构、不兼容变更(罕见) |
# 获取上一个版本号
git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1
# 无 tag(首次发布)时,从版本源文件读取
grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' <VERSION_FILE> | grep -o '"[0-9][^"]*"$' | tr -d '"'4.3 生成 CHANGELOG(Step 3)—— 含两道防护
首次发版和后续发版的写入方式完全不同,混淆会丢数据:
test -f CHANGELOG.md && echo exists || echo not-exists
# 不存在(首次):全量生成
git cliff --tag <VERSION> -o CHANGELOG.md
# 已存在(后续):前置插入
git cliff --tag <VERSION> --prepend CHANGELOG.md⚠️ 切勿对已存在的 CHANGELOG.md 使用
-o(覆盖写入)!-o会用新版本内容整文件覆盖,丢失所有历史版本。必须用--prepend。 这是实测踩过的坑——详见下方”根因与防护”。
防护 1:生成后校验版本数(关键)
生成后比对 CHANGELOG 版本数与 git tag 数,不等则恢复重试:
echo "CHANGELOG 版本数: $(grep -c '^## v' CHANGELOG.md)"
echo "Git tag 数: $(git tag --list 'v*' | wc -l)"两个数字必须相等。若 CHANGELOG 版本数 < tag 数,说明历史被覆盖,立即:
git checkout CHANGELOG.md # 恢复
# 然后用 --prepend 重试为什么用版本数校验而非 git diff:
git diff需要人眼判断哪些段消失了;版本数比对是纯数字,agent 和人都能一眼看出不等=出错,零判断成本。
防护 2:AI 摘要注入
git-cliff 模板中 <!-- AI_SUMMARY --> 是占位符,需要用 AI 生成的摘要替换。基于本次 commits 的分组统计 + Skill 变更分析,生成 2-3 句自然语言摘要:
> <自然语言总结,2-3 句,概括本次发布最核心的变化>
>
> 共 <N> commits,其中 🚀 Features <N> | 🐛 Fixes <N> | 📝 Docs <N> | ...
>
> **[Full diff](https://github.com/<owner>/<repo>/compare/<PREV_TAG>...<VERSION>)**首次发布无 PREV_TAG 时,Full diff 链接为 https://github.com/<owner>/<repo>/commits/<VERSION>。
5. 版本号管理
5.1 版本号来源
不同项目的版本号来源不同:
- Node 项目 →
package.json的"version" - Rust 项目 →
Cargo.toml的version - Python 项目 →
pyproject.toml或__version__.py - 插件项目 → 插件清单文件(如
plugin.json) - 纯文档 / 无清单项目 → 仅靠 git tag
5.2 模型一:三同步(单组件仓库,通用默认)
适用于绝大多数项目(单 package.json / 单 Cargo.toml)。发版时让三处保持一致:
git tag v1.0.1 ←→ CHANGELOG "## v1.1.0" ←→ 版本源文件 "version": "1.0.1"release skill 的 Step 3.5 负责更新版本源文件(去 v 前缀),与 CHANGELOG 一起 commit。如果项目没有版本源文件(仅靠 tag),这步可跳过。
一个常见的坑:版本源文件可能是 JSON / TOML / YAML 等格式,更新时只改 version 行,保持其余结构与缩进不变。用 sed 精确替换该行:
# JSON 格式(package.json / plugin.json 等)
sed -i 's/"version":[[:space:]]*"[0-9][^"]*"/"version": "<VERSION_WITHOUT_V>"/' <VERSION_FILE>
# TOML 格式(Cargo.toml / pyproject.toml 等)——注意只改 [package] 段的 version5.3 模型二:两层版本(多组件仓库)
适用于 monorepo(多包)或插件市场(多 skill)——仓库包含多个可独立演进的组件,每个有自己的版本号,改谁 bump 谁,互不影响。
三同步模型会让没改动的组件也被强行升版本。两层版本模型解决这个:
| 层级 | 载体 | 规则 |
|---|---|---|
| 全局版(代表整个仓库的发版) | git tag + CHANGELOG + 顶层版本源(如 plugin.json) | 每次发版必 bump |
| 组件独立版 | 各组件清单的 version 字段 | 只 bump 有改动的组件,未改不动,与全局版不绑定 |
release skill 的 Step 3.5 需分两步:
- 全局版:照常更新顶层版本源(§5.2)
- 组件版:用
git diff检测哪些组件有改动,只 bump 这些组件的version字段:PREV_TAG=$(git tag --sort=-creatordate | head -1) # 检测本次改动的组件(对比上一个 tag) CHANGED=$(git diff --name-only "$PREV_TAG" HEAD -- 'components/*' \ | sed 's|components/||;s|/.*||' | sort -u) # 对每个有改动的组件 bump 版本号 for c in $CHANGED; do sed -i 's/^version:[[:space:]]*.*/version: "<NEW_VERSION>"/' "components/$c/metadata.yaml" done注意:全新组件若仍是 untracked,
git diff看不到——以git status为准。
何时用哪个模型:判断标准是”发版时是否需要改谁 bump 谁”。单
package.json项目用 §5.2 三同步即可;只有当仓库内有多个各自有版本号、各自演进的组件时才需要两层模型。
6. GitHub Action:自动创建 Release
仓库无关、逐字可复用。它不生成 changelog(已由本地 skill 完成),只做切片 + 建 Release:
name: Release
on:
push:
tags: ['v*']
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # 全历史,awk 切片需要
- name: Extract release notes
run: |
TAG="${GITHUB_REF_NAME}"
awk "/^## ${TAG}/{found=1} /^## / && found && !/^## ${TAG}/{exit} found{print}" \
CHANGELOG.md > release-notes.md
- uses: softprops/action-gh-release@v2
with:
body_path: release-notes.mdawk 逻辑:找到 ## v1.0.0 行开始输出,遇到下一个 ## 行就退出——精准切出当前版本的段。
设计原则:CI 只做切片 + 建 Release,不生成内容,无状态、可复现。所有”生成”逻辑都在本地 release skill 完成,CI 失败可手动重跑,不影响 CHANGELOG 正确性。
7. 仓库卫生实践
7.1 gitignore 逐层穿透(反忽略嵌套目录)
场景:你的 agent 工作目录(如 .zcode/、.claude/、.idea/ 等)整体被 .gitignore 忽略,但你想把其中的某个子目录(比如 release skill)纳入版本控制。
坑:直觉写法不生效:
.zcode/ # ❌ 忽略整个目录
!.zcode/skills/ # ❌ git 不会进入 .zcode/,这条反忽略永远不被评估根因:git 的规则——一旦一个目录被忽略,git 不会「进入」它,里面的反忽略规则永远不会被评估。
解决:逐层反忽略——先放开目录本身让 git 能进入,再放开目标子目录:
.zcode/* # 忽略 .zcode 下所有内容(但不忽略 .zcode 本身)
!.zcode/skills/ # 放开 skills 子目录 → git 能进入
.zcode/skills/* # 再次忽略 skills 下所有内容
!.zcode/skills/release/ # 放开 release 子目录验证:
git check-ignore .zcode/skills/release/SKILL.md # exit 1 = 未忽略 ✓
git add .zcode/ # 只会 add release/SKILL.md 一个文件这个技巧适用于任何「整体忽略 + 局部追踪」的场景,与具体目录名无关。
7.2 数据快照 vs 源配置的取舍
通用原则:版本库应只含「人写的、不可自动生成的」内容。能由脚本从外部数据源全量重建的文件,属于派生数据/缓存,提交会让仓库膨胀且持续产生噪音。
判断标准——问自己两个问题:
- 这个文件是手写的吗? → 是 → 提交(它是源配置)
- 删掉它,脚本能从原始来源重新生成吗? → 是 → 考虑 ignore(它是派生数据)
典型该 ignore 的派生数据(带 fetch_time/时间戳的抓取快照、构建产物、渲染输出):
- 抓取的 feed 列表 / 排行榜数据(带
metadata.fetch_time) - 编译产物(
dist/、build/、*.min.js) - 数据文件的格式转换副本(如 JSON 的 Markdown 渲染版)
典型该提交的源配置:
- 手写的 curated 配置(编辑它 = 改变行为)
- 不可自动生成的 seed 数据
实操要点:如果一个派生文件已经被 tracked,用
git rm --cached <file>从索引移除(本地保留),再加入.gitignore。不要直接git rm(会删本地文件)。
8. 复现指南:给 agent 照着搭建
本节是为「agent 自助搭建」设计的有序流程。每步有明确的动作 + 验证,验证不过不进下一步。把本节 + §3.1(cliff.toml)+ §6(workflow)+ 附录 A(skill 模板)一起给 agent,它就能为新项目完整搭出闭环。
前置确认(开工前问清 4 件事)
| 确认项 | 怎么查 | 不过怎么办 |
|---|---|---|
| 是 git 仓库 | git rev-parse --is-inside-work-tree | 先 git init |
| 有 GitHub 远程 | git remote get-url origin | 先 git remote add origin <url> |
| git-cliff 已装 | git cliff --version | winget install git-cliff / cargo install git-cliff / brew install git-cliff |
| 版本源文件 | 见下方「检测版本源」 | 无则跳过版本同步(仅用 tag) |
检测版本源(按优先级试,命中即止):
# Node
[ -f package.json ] && grep '"version"' package.json
# Rust
[ -f Cargo.toml ] && grep '^version' Cargo.toml
# Python (PEP 621)
[ -f pyproject.toml ] && grep 'version' pyproject.toml | head -1
# 插件清单(通用名)
ls */.claude-plugin/plugin.json .claude-plugin/plugin.json 2>/dev/null记录命中的文件路径 + 字段格式(JSON 的 "version": "x" vs TOML 的 version = "x"),后续 Step 3.5 和附录 A 的 <VERSION_FILE> 占位符要用。
Step 1:创建 cliff.toml
- 复制 §3.1 的完整配置到仓库根
cliff.toml - 替换两处占位符
<owner>/<repo>为实际仓库(git remote get-url origin可读出) - 验证:
git cliff --unreleased能跑通、无报错(有提交则能看到分组输出;空仓库则只输出 header,也正常)
Step 2:创建 GitHub Action
- 新建
.github/workflows/release.yml,原样复制 §6(仓库无关,无需改) - 验证:YAML 语法检查
python -c "import yaml; yaml.safe_load(open('.github/workflows/release.yml'))" && echo OK
Step 3:创建 release skill
- 复制附录 A 的完整 skill 模板,落到 agent skill 目录(如
.zcode/skills/release/SKILL.md) - 填入附录 A 顶部标注的 3 个占位符(仓库 URL、版本源文件路径、skill 监控的业务目录)
- 如该目录被
.gitignore忽略,按 §7.1 逐层反忽略 - 验证:对 agent 说「发布」或「release」,确认 skill 被触发并进入 Step 0 前置检查
Step 4:提交搭建产物
git add cliff.toml .github/workflows/release.yml <skill路径> .gitignore
git commit -m "chore: 引入 git-cliff changelog 闭环体系"验证:git status --porcelain 干净;git cliff --tag v0.1.0 --unreleased 预览正常。
Step 5:首次发版(端到端验证)
工作树干净后,对 agent 说「发布」。走完整个 §4 流程,确认 4 件事全绿:
CHANGELOG.md生成,含 AI 摘要 + 分组- 版本源文件已同步(如有)
git tag列出新 tag- push 后 GitHub Actions 出现 Release workflow 运行 → GitHub Releases 页出现新 Release
9. 设计要点总结
- 混合生成:git-cliff 出结构骨架(分组/链接),agent 后处理注入 AI 摘要——纯 CI 方案做不到,纯手写太累
--prepend策略 + 生成后校验:CHANGELOG 永不全量重生成,每次只前置插入新段;生成后用版本数校验防覆盖(§4.3)- 版本号同步模型随仓库结构而定:单组件仓库用三同步(§5.2),多组件仓库用两层模型——全局版每次 bump,组件版改谁 bump 谁(§5.3)
- 兜底优先:
body = ".*"兜底 parser +split(pat="\n") | first取首行,保证任何历史提交都不丢且渲染干净(§3.3–§3.4) - CI 最小化:GitHub Action 只做切片 + 建 Release,不生成内容,无状态、可复现(§6)
这套体系的核心哲学:结构交给工具(git-cliff),语义交给 AI(摘要),决策交给人(确认)。三者各司其职,发版从「手工拼凑」变成「一句话 + 一次确认」。
附录 A:release skill 完整模板(可直接复制)
以下是完整的、可复制的 release skill 文件。复制后只需替换顶部 3 个
<...>占位符即可使用。本模板面向 ZCode / Claude Code 的 skill 格式(YAML frontmatter + Markdown body);其他 agent 平台可按 §4.1 的流程等价实现,或直接用 shell 脚本包装下面的命令。
使用前替换这 3 处占位符:
| 占位符 | 含义 | 示例 |
|---|---|---|
<OWNER>/<REPO> | GitHub 仓库(compare/commit 链接用) | BingqiangZhou/Skills |
<VERSION_FILE> | 版本源文件路径(无则删 Step 3.5) | plugins/daily-digest/.claude-plugin/plugin.json |
<WATCH_DIR> | release 预览时要分析的「业务变更目录」 | plugins/daily-digest/skills/ |
---
name: release
version: "1.0"
description: 项目发布工具。分析 git 历史、通过 git-cliff 生成 CHANGELOG.md、同步版本号、创建 git tag 并推送。推送后 GitHub Action 自动创建 Release。基于语义化版本号。**触发场景**:用户提到"发布""release""发版""打 tag""生成 changelog""更新版本""创建 release",或需要将当前项目状态发布为新版本时使用。
---
# Release — 项目发布
一键发布流程:分析变更 → 生成 CHANGELOG → 同步版本号 → 创建 tag → 推送 → GitHub Action 自动创建 Release。
## 前置工具
| 工具 | 用途 | 安装 |
|------|------|------|
| `git-cliff` | 生成 CHANGELOG | `winget install git-cliff` |
GitHub Release 由 `.github/workflows/release.yml` 自动创建,无需本地 `gh` CLI。
## 工作流
按顺序执行。**Step 1 和 Step 2 完成后展示摘要,等待用户确认再继续。**
### Step 0: 前置检查
逐项验证,失败则中止并提示用户处理:
1. **git-cliff**: `git cliff --version`,未安装则提示安装命令,中止
2. **工作树干净**: `git status --porcelain`,有输出则提示先 commit 或 stash,中止
### Step 1: 确定版本号
采用语义化版本 `v<MAJOR>.<MINOR>.<PATCH>`。
1. **获取上一个版本号**:
```bash
git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1如果无 tag(首次发布),从版本源文件读取(去 v 前缀):
grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' <VERSION_FILE> | grep -o '"[0-9][^"]*"$' | tr -d '"'若无版本源文件,询问用户首版号(默认 v0.1.0)。
按变更规模递增(参考 Step 2 分析):
- PATCH: 仅修 bug、文档、chore、配置同步
- MINOR: 新增功能、能力扩展
- MAJOR: 架构级重构、不兼容变更(罕见)
检查 tag 是否已存在,冲突则 PATCH 末位 +1:
git tag -l "v<新版本号>"
Step 2: 预览变更
找到上一个 tag(无 tag 则用初始 commit):
git tag --sort=-creatordate | head -1 || git rev-list --max-parents=0 HEAD生成 changelog 预览(终端输出,不写文件):
git cliff --tag <VERSION> --unreleased统计 commits:
git rev-list <PREV>..HEAD --count首次发布(PREV 是初始 commit)则
git rev-list <INIT>..HEAD --count分析业务变更(替换 <WATCH_DIR> 为实际目录,无则跳过):
git ls-tree -d --name-only <PREV> <WATCH_DIR> 2>/dev/null git diff <PREV>..HEAD --stat -- '<WATCH_DIR>'展示发布摘要,格式:
📦 Release <VERSION> Summary ━━━━━━━━━━━━━━━━━━━━━━━━━━━ Previous: <PREV_TAG> Commits: <N> Changes: <变更摘要> Output: [x] CHANGELOG.md [x] 版本源文件 → <VERSION> (如有) [x] Git tag <VERSION> [x] Push to origin → GitHub Action auto-creates Release 确认发布?(y/n)等待用户确认(可确认 / 改版本号 / 取消)
Step 3: 生成 CHANGELOG.md
检查是否已存在:
test -f CHANGELOG.md && echo exists || echo not-exists不存在(首次):
git cliff --tag <VERSION> -o CHANGELOG.md已存在(后续):
git cliff --tag <VERSION> --prepend CHANGELOG.md⚠️ 切勿对已存在的 CHANGELOG.md 使用
-o(覆盖写入)!-o会用新版本内容整文件覆盖,丢失所有历史版本。必须用--prepend。 详见 §4.3。校验历史版本未丢失(生成后必须执行):
echo "CHANGELOG 版本数: $(grep -c '^## v' CHANGELOG.md)" echo "Git tag 数: $(git tag --list 'v*' | wc -l)"两个数字必须相等。若 CHANGELOG 版本数 < tag 数,说明历史被覆盖, 立即
git checkout CHANGELOG.md恢复后用--prepend重试。生成 AI 摘要,替换
<!-- AI_SUMMARY -->占位符。格式:> <2-3 句自然语言总结,概括本次发布最核心的变化> > > 共 <N> commits,其中 🚀 Features <N> | 🐛 Fixes <N> | 📝 Docs <N> | ... > > **[Full diff](https://github.com/<OWNER>/<REPO>/compare/<PREV>...<VERSION>)**首次发布无 PREV 时,链接用
https://github.com/<OWNER>/<REPO>/commits/<VERSION>暂存:
git add CHANGELOG.md
Step 3.5: 同步版本源文件(如有)
把 <VERSION_FILE> 的 version 更新为v 前缀):
# JSON 格式(package.json / plugin.json 等)
sed -i 's/"version":[[:space:]]*"[0-9][^"]*"/"version": "<VERSION_WITHOUT_V>"/' <VERSION_FILE>
# TOML 格式(Cargo.toml / pyproject.toml 等)——注意只改 [package] 段的 version校验只动了 version 行:git diff <VERSION_FILE> 暂存:git add <VERSION_FILE>
若项目无版本源文件(仅用 tag),跳过本步。
多组件仓库(monorepo / 多 skill):除顶层版本源外,还需检测并 bump 有改动的组件, 只 bump 变动的、不动未改的。详见 §5.3。
Step 3.6: 提交
git commit -m "docs: release <VERSION>"Step 4: 创建 Tag 并推送
git tag -a <VERSION> -m "Release <VERSION>"
git push origin HEAD --tagspush 失败则提示用户稍后手动推送:git push origin HEAD --tags
完成
✅ Release <VERSION> 发布完成
CHANGELOG.md: 已更新
版本源文件: version → <VERSION_WITHOUT_V> (如有)
Git tag: <VERSION>
GitHub Release: 等待 GitHub Action 自动创建
查看: https://github.com/<OWNER>/<REPO>/actions错误处理
| 场景 | 处理 |
|---|---|
| git-cliff 未安装 | 中止,提示安装命令 |
| 工作树有未提交变更 | 中止,提示 commit 或 stash |
| tag 已存在 | PATCH 末位 +1 直到不冲突 |
| push 失败 | 提示手动推送 git push origin HEAD --tags |
| 无上一个 tag | 从版本源文件读首版 / 询问用户,用初始 commit 全量生成 |
| 0 commits since last tag | 中止:“No new commits since |
> **agent 搭建时的提示**:复制本附录到一个新 `.md` 文件后,用编辑器的全局替换功能把 3 个占位符换掉即可。占位符在 frontmatter 里没有(保持通用),在 body 的命令和链接里出现——逐个确认替换完整,别遗漏 `compare/` 和 `commit/` 两类链接里的 `<OWNER>/<REPO>`。