<!-- title: 用Skill造发布日志生成器 -->

# 用 Skill 造一个发布日志生成器：从 Git 历史到结构化 CHANGELOG

## 概述

本教程带你从零构建一个**真实可用、可分发**的 Claude Code 自定义 Skill——`release-notes`。它会读取一个 Git 仓库的 Conventional Commits 历史，自动归类、生成结构化的 `CHANGELOG.md`。你将掌握 Skill 的三层目录结构、YAML frontmatter 的每一个关键字段（尤其是触发词 `description` 与安全围栏 `allowed-tools`）、渐进式披露（progressive disclosure）设计，以及用 `-p` 无头模式做回归测试的工程化方法。适用人群：已跑通 Claude Code 基础流程、想从"写一段提示词"升级到"做一个可复用工具"的开发者。

---

## 前置条件

- 已安装 Claude Code（`claude --version` 能输出版本号）
- 一台装有 Git 和 Python 3.8+ 的 macOS / Linux / WSL2 环境
- 一个**含有 Conventional Commits 历史提交**的 Git 仓库（教程会提供一个示例）
- 理解 YAML 基本语法，能区分"键值对"与"块标量"
- 阅读过官方"什么是 Skill"概念文档（见延伸阅读）即可，无需先做过其它 Skill

---

## 核心概念：Skill 的三层结构

在动手前，先建立正确的心智模型。一个成熟的 Skill 不是一个大文件，而是**一个文件夹**，由三层组成：

```mermaid
flowchart LR
    A[Skill 文件夹<br/>release-notes/] --> B[SKILL.md<br/>入口与契约]
    A --> C[scripts/<br/>可执行脚本]
    A --> D[references/<br/>按需加载的参考资料]
    B -->|第一阶段| E[模型发现层<br/>只读 frontmatter]
    B -->|第二阶段| F[模型决策层<br/>读 SKILL.md 正文]
    D -->|第三阶段| G[深度展开层<br/>按需 Read 引用文件]
    C -.调用.-> G
```

这个分层有明确的目的：**控制上下文成本**。模型在一次会话里要面对成千上万个潜在 Skill，它不可能把每个 Skill 的全部内容都加载进来。Claude Code 的加载策略是分阶段的：

1. **发现阶段**：只把每个 Skill 的 `name` 和 `description` 注入上下文（通常每个几十 token），用于判断"要不要激活"。
2. **决策阶段**：选中后，读取 `SKILL.md` 正文，理解工作流。
3. **展开阶段**：正文里如果引用了 `references/xxx.md` 或 `scripts/xxx.py`，模型才会用 `Read` 工具按需加载。

这就是**渐进式披露**的核心思想——把"必须先知道的"放前面，把"可能用到的"留到后面按需加载。理解了这一点，你就理解了下面所有设计的"为什么"。

---

## 步骤一：用脚手架生成骨架

Claude Code 自带 `skill-creator` 技能，可以一键生成符合规范的骨架。在交互模式下直接说：

```text
/skill-creator 创建一个名为 release-notes 的 skill，用途是从 git 历史生成 changelog
```

但为了吃透每个字段，本教程我们**手工搭建**，这样你能看清每一块的作用。先建目录：

```bash
# 个人级 Skill（所有项目可用）
mkdir -p ~/.claude/skills/release-notes/scripts
mkdir -p ~/.claude/skills/release-notes/references

# 或项目级 Skill（仅当前仓库可用，更推荐用于与代码强绑定的 skill）
mkdir -p .claude/skills/release-notes/scripts
mkdir -p .claude/skills/release-notes/references
```

**预期结果**：得到如下目录树。

```text
release-notes/
├── SKILL.md            ← 入口文件，契约 + 指令
├── scripts/
│   └── parse_commits.py
└── references/
    └── conventional-commits.md
```

**常见问题**：

| 问题 | 原因 | 解决 |
|------|------|------|
| 放在 `~/.claude/skills/` 却不触发 | 目录名与 frontmatter `name` 不一致 | 保持目录名 = `name` 字段 |
| 项目级 Skill 不生效 | 未在仓库根目录的 `.claude/skills/` 下 | 用 `pwd` 确认在仓库根，且 Claude Code 从该目录启动 |
| Windows 下编辑后加载失败 | 文件头被存了 UTF-8 BOM | 用 `sed -i '1s/^\xEF\xBB\xBF//' SKILL.md` 去掉 BOM |

---

## 步骤二：精写 frontmatter——Skill 的"门面"

frontmatter 是 Skill 与运行时之间的**契约**。运行时严格解析它，却宽松解析正文。所以这里错一个字符，整个 Skill 就"沉默失活"——能加载，但永远不会被触发。

先看我们要写的完整 frontmatter，再逐字段拆解：

```yaml
---
name: release-notes
description: "Generate a structured CHANGELOG.md from a repository's git history using the Conventional Commits spec. Group entries by type (feat/fix/perf/...), highlight breaking changes, and emit Markdown. Use when: the user asks to generate release notes, draft a changelog, summarize commits since a tag, or prep a version bump. Not for: analyzing a single commit or writing commit messages."
allowed-tools: Read, Write, Bash, Grep
argument-hint: "[<from-rev>] [--to <rev>] [--dry-run]"
tags:
  - tool:git
  - type:documentation
  - lang:python
user-invokable: true
---
```

### 字段一：`name`（必填，唯一身份）

- 小写 kebab-case，与目录名**完全一致**
- 它同时是 `/release-notes` 斜杠命令的 slug
- 建议不超过 40 字符

### 字段二：`description`（必填，触发命中的关键）

这是**整个 Skill 最重要的一行**。模型在发现阶段只能看到这个字段，它决定 Skill 是否会被激活。官方统计显示，公开目录里 100% 的 Skill 都填了它——因为不填根本无法被发现。

写好它的三条铁律：

1. **第一句**说明做什么（`Generate a structured CHANGELOG.md from ...`）
2. **用 "Use when:"** 列出触发短语（`the user asks to generate release notes, draft a changelog ...`）
3. **用 "Not for:"** 排除近邻场景（`Not for: analyzing a single commit`），避免被相似的 Skill 吞掉

**为什么用英文写 description？** 因为模型做触发匹配时，英文关键词密度更高、命中更稳。正文可以中文，但 description 推荐英文。长度 200–800 字符为佳，写成**散文**而非要点列表。

### 字段三：`allowed-tools`（强烈推荐，安全围栏）

这是 Skill 的**权限边界**。列出后，激活期间模型只能调用这些工具。`release-notes` 需要：读历史（`Bash` 跑 `git log`）、写文件（`Write` 生成 CHANGELOG）、读引用（`Read`）、搜内容（`Grep`）。所以：

```yaml
allowed-tools: Read, Write, Bash, Grep
```

如果省略这个字段，Skill 会**继承会话的全部工具**——一个号称"只读仓库"的 Skill 理论上也能 `rm -rf`。这是公开目录里最被忽视的安全字段（统计显示不到 10% 的 Skill 填了）。即使最简单的只读 Skill，也至少写 `allowed-tools: Read, Grep`。

### 字段四：`argument-hint`（UX 细节）

当用户敲 `/release-notes` 不带参数时，运行时会把这行**原样显示**作为帮助。像 man page 的概要行那样写：尖括号必填、方括号选填。

### 字段五：`tags`（可发现性）

用 `命名空间:值` 形式，便于目录检索与去歧义。5–8 个为宜，超过 15 个就成关键词堆砌。

### 字段六：`user-invokable`（调用方式）

`true` 表示既能被模型自动触发，也能被用户用 `/release-notes` 显式调用。设为 `false` 则只能由模型按 `description` 自动决策触发（适合"元技能"或编排型 Skill）。

**frontmatter 的隐形陷阱**：

- 开头的 `---` 必须**顶格（第 0 列）**，前面不能有空格
- 必须有结尾的 `---`，否则解析器会一直读到下一个 YAML 文档标记，可能吞掉正文里的 YAML 代码块
- `description` 里只要含"冒号+空格"，整个值就必须**加引号**（否则被解析成键值对）
- 文件**必须以换行符结尾**

写完用这条命令自检 YAML 是否合法（不必进 Claude Code 就能验证）：

```bash
python3 -c "import yaml,sys; d=open(sys.argv[1]).read().split('---')[1]; print(yaml.safe_load(d))" \
  ~/.claude/skills/release-notes/SKILL.md
```

**预期结果**：打印出一个字典，包含 `name`、`description`、`allowed-tools` 等键，且无报错。

---

## 步骤三：编写核心脚本 `scripts/parse_commits.py`

把"机械的、确定性的"工作交给脚本，把"需要判断的"工作留给模型——这是 Skill 工程化的基本原则。解析 Git 提交、正则匹配 Conventional Commits 是确定性任务，写成 Python 脚本既快又稳、且无幻觉。

`scripts/parse_commits.py`：

```python
#!/usr/bin/env python3
"""Parse git log into Conventional Commits groups. Output: JSON to stdout.

Usage:
    parse_commits.py [<from-rev>]            # defaults to <from-rev>..HEAD
    parse_commits.py v1.1.0                  # commits since tag v1.1.0
    parse_commits.py v1.1.0 HEAD             # explicit range
"""
import subprocess, json, re, sys

# feat(api)!: add pagination  ->  type=feat, scope=api, breaking=True
SUBJECT_RE = re.compile(
    r'^(?P<type>\w+)(?:\((?P<scope>[\w-]+)\))?(?P<breaking>!)?:\s*(?P<desc>.+)$'
)

def git(*args):
    return subprocess.run(['git', *args], capture_output=True,
                          text=True, check=True).stdout

def main():
    argv = sys.argv[1:]
    since = argv[0] if argv else None
    until = argv[1] if len(argv) > 1 else 'HEAD'
    rng = f'{since}..{until}' if since else until

    # %H hash, %s subject, %b body; \t and \x1e as separators
    fmt = '%H%x09%s%x09%b%x1e'
    out = git('log', f'--pretty={fmt}', rng)
    records = [r for r in out.split('\x1e') if r.strip()]

    groups, unknown = {}, []
    for rec in records:
        parts = rec.strip().split('\t')
        if len(parts) < 2:
            continue
        h, subj, body = parts[0], parts[1], parts[2] if len(parts) > 2 else ''
        m = SUBJECT_RE.match(subj)
        breaking = 'BREAKING CHANGE' in body
        if not m:
            unknown.append({'hash': h[:7], 'subject': subj})
            continue
        t = m.group('type')
        groups.setdefault(t, []).append({
            'hash': h[:7], 'scope': m.group('scope') or '',
            'desc': m.group('desc').strip(),
            'breaking': breaking or bool(m.group('breaking')),
        })

    print(json.dumps({'range': rng, 'groups': groups,
                      'unknown': unknown, 'count': len(records)}, indent=2))

if __name__ == '__main__':
    main()
```

赋予执行权限并单独测试：

```bash
chmod +x ~/.claude/skills/release-notes/scripts/parse_commits.py
cd /path/to/your-repo
~/.claude/skills/release-notes/scripts/parse_commits.py v1.1.0
```

**预期结果**（节选）：

```json
{
  "range": "v1.1.0..HEAD",
  "groups": {
    "feat": [
      {"hash": "a3f2c10", "scope": "auth", "desc": "add OAuth2 login", "breaking": false}
    ],
    "fix": [
      {"hash": "b71e098", "scope": "", "desc": "crash on empty cart", "breaking": false}
    ]
  },
  "unknown": [],
  "count": 2
}
```

**常见问题**：

- **`fatal: ambiguous argument`**：传入的 tag 不存在。先 `git tag` 确认，或用 commit hash 代替。
- **`unknown` 列表很长**：说明仓库没用 Conventional Commits。这是预期行为，正文会教模型如何兜底（见步骤五）。
- **正文里有 `BREAKING CHANGE` 但没被识别**：检查 `%b`（body）是否被正确切分；某些提交工具把破坏性说明写在 footer。

---

## 步骤四：编写渐进式披露参考 `references/conventional-commits.md`

正文不会每次都讲清 Conventional Commits 的每个细节，但我们也不想在正文里塞一张大表污染上下文。把规范速查表放进 `references/`，让模型在需要时**自己决定读不读**。

`references/conventional-commits.md`（精简版）：

```markdown
# Conventional Commits 速查

## 提交格式
<type>[optional scope][!]: <description>
[optional body]
[optional footer(s)]

## 类型语义与 CHANGELOG 章节
| type     | 章节               | 严重度  |
|----------|--------------------|---------|
| feat     | Features           | 高      |
| fix      | Bug Fixes          | 高      |
| perf     | Performance        | 中      |
| refactor | Refactor           | 低/省略 |
| docs     | Documentation      | 低/省略 |
| test     | Tests              | 低/省略 |
| chore    | Maintenance        | 省略    |

## 破坏性变更
- subject 里 `type!:` 或 footer `BREAKING CHANGE:` 均算
- CHANGELOG 顶部独立 "⚠ Breaking Changes" 区块

## 版本号推断（SemVer）
- 含 breaking → MAJOR
- 含 feat     → MINOR
- 仅 fix/perf → PATCH
```

注意：这份文件**不会被自动加载**。只有在 `SKILL.md` 正文里写了"参考 `references/conventional-commits.md`"并被模型用 `Read` 打开时，它才进入上下文。这就是渐进式披露省 token 的机制。

---

## 步骤五：编写 SKILL.md 正文——告诉模型怎么干

正文是激活后模型真正遵循的指令。好的正文遵循"**确定性给脚本，判断性给规则**"原则，并明确引用按需加载的参考资料。

`SKILL.md` 正文（紧接 frontmatter 之后）：

```markdown
# Release Notes Generator

根据 Git 历史生成结构化 CHANGELOG。

## 工作流

1. **确定范围**。若无参数，用 `git describe --tags --abbrev=0` 取最近 tag 作为起点；
   若无任何 tag，则用仓库首个提交。
2. **解析提交**。运行脚本（确定性步骤，避免幻觉）：
   ```bash
   python3 scripts/parse_commits.py <from-rev>
   ```
   得到 JSON：`{groups, unknown, count}`。
3. **归类渲染**。按 `references/conventional-commits.md` 中的类型→章节映射，
   把 `groups` 渲染为 Markdown。`docs/test/chore` 默认折叠或省略。
4. **破坏性变更**。任何 `breaking: true` 顶部独立成 "⚠ Breaking Changes" 区块。
5. **兜底**。若 `unknown` 非空且占比 >30%，在 CHANGELOG 末尾附
   "## Other Changes" 原样列出，并在回复中提醒用户提交规范不统一。
6. **写文件**。默认追加到仓库根的 `CHANGELOG.md` 顶部（新版本在上）。
   带 `--dry-run` 时只打印不写盘。

## 输出格式
使用 Keep a Changelog 风格：`## [vX.Y.Z] - YYYY-MM-DD` 作版本标题，
下方分 `### Features` / `### Bug Fixes` 等。每条 `- <desc> (<hash>)`。

## 何时加载 references
当你不确定某 type 对应哪个章节、或需要推断版本号时，再 Read
`references/conventional-commits.md`。无需每次都读。
```

几个写正文的要点：

- **第 2 步把解析交给脚本**：这是关键。模型直接"读 git log 再自己归类"极易出错（漏匹配、编 hash），交给脚本保证 100% 准确。
- **明确"何时加载 references"**：把展开条件写清楚，避免模型要么从不读、要么每次都读。
- **`--dry-run` 分支**：让 Skill 安全可测，这点在步骤七的回归测试里很有用。

---

## 步骤六：安装、触发与人工验证

个人级 Skill 放对目录后**无需重启**，Claude Code 会自动发现。在交互模式下验证：

```text
> 帮我生成 v1.2.0 的发布日志，基于上个 tag
```

模型应自动触发 `release-notes`，你会看到它调用 `Bash` 跑 `git describe`、调用 `parse_commits.py`、必要时 `Read` references、最后 `Write` 到 `CHANGELOG.md`。

也可以显式触发：

```text
> /release-notes v1.1.0 --dry-run
```

**预期结果**：终端打印出渲染好的 Markdown，但不写盘。

**触发失败的排查清单**：

| 症状 | 排查 |
|------|------|
| 模型完全没注意到 Skill | `description` 缺 "Use when:" 触发短语 |
| 触发了但报"无权限" | `allowed-tools` 漏了 `Bash` 或 `Write` |
| 触发了别的 Skill | 加 "Not for:" 排除近邻场景 |
| `argument-hint` 不显示 | 确认 `user-invokable: true` |

---

## 步骤七：用 `-p` 无头模式做回归测试

Skill 写完一次对不算成功，要能**反复验证**。用 `-p`（print/无头）模式把"触发→执行→输出"自动化：

```bash
# 回归用例 1：dry-run 应只打印不写盘
claude -p "用 release-notes 基于 v1.1.0 生成 changelog，dry-run 模式" \
  | grep -q "## \[v1.2.0\]" && echo "PASS: 渲染正确" || echo "FAIL"

# 回归用例 2：破坏性变更应出现在顶部区块
claude -p "用 release-notes v1.1.0..HEAD，确认 breaking 变更在最前" \
  | grep -q "Breaking Changes" && echo "PASS" || echo "FAIL"

# 回归用例 3：未知提交占比高时应提醒
claude -p "用 release-notes（该仓库提交不规范）" \
  | grep -qi "提交规范" && echo "PASS: 兜底生效" || echo "FAIL"
```

把这三条存成 `~/.claude/skills/release-notes/test.sh`，每次改 Skill 跑一遍，这就是把"祈祷式开发"变成"工程化开发"的关键一步。

---

## 实战案例：给 demo-shop 仓库发 v1.2.0

下面是一个**完整可复现**的端到端示例。

**1. 准备示例仓库**（若没有现成仓库）：

```bash
mkdir demo-shop && cd demo-shop && git init -q
git config user.email "dev@shop.io"; git config user.name "Dev"
echo "# demo-shop" > README.md && git add . && git commit -qm "chore: init"
git tag v1.0.0
git commit --allow-empty -qm "feat(auth): add OAuth2 login"
git commit --allow-empty -qm "fix(cart): crash on empty cart"
git commit --allow-empty -qm "feat!(api): rename /users to /accounts

BREAKING CHANGE: endpoint renamed"
git commit --allow-empty -qm "docs: update readme"
```

**2. 触发 Skill**（在仓库目录下启动 claude）：

```text
> 基于 v1.0.0 生成 v1.2.0 的 CHANGELOG
```

**3. 预期生成的 `CHANGELOG.md`**：

```markdown
## [v1.2.0] - 2026-06-26

### ⚠ Breaking Changes
- rename /users to /accounts (5e...)

### Features
- add OAuth2 login (a3f2c10)

### Bug Fixes
- crash on empty cart (b71e098)

### Other Changes
- docs: update readme (c9...)
```

注意模型自动：把 `feat!` 提到顶部、把 `docs` 折进 Other Changes、按类型分章。这正是脚本（确定性解析）+ 正文（规则渲染）分工的成果。

```mermaid
sequenceDiagram
    participant U as 用户
    participant C as Claude Code
    participant S as SKILL.md
    participant P as parse_commits.py
    participant R as references/
    participant F as CHANGELOG.md

    U->>C: 生成 v1.2.0 changelog
    C->>S: 发现阶段：读 description 命中
    C->>S: 决策阶段：读正文工作流
    C->>P: Bash 跑 parse_commits.py v1.0.0
    P-->>C: 返回 JSON {groups,unknown}
    C->>R: Read conventional-commits.md（按需）
    R-->>C: 类型→章节映射表
    C->>F: Write 渲染后的 Markdown
    F-->>U: 展示生成的 CHANGELOG
```

---

## 延伸阅读

- **官方概念文档**：[What are skills?](https://support.claude.com/en/articles/12512176-what-are-skills) 与 [How to create custom skills](https://support.claude.com/en/articles/12512198-how-to-create-custom-skills)——理解"文件夹 + SKILL.md"的本质。
- **官方示例库**：[anthropics/skills](https://github.com/anthropics/skills)——包含 `template/` 骨架和 docx/pdf/pptx 等生产级 Skill 源码，是最好的"抄作业"对象。
- **frontmatter 字段全集**：[SKILL.md Frontmatter Fields 完整参考](https://claudskills.com/learn/claude-code-skill-frontmatter-fields/)——逐字段讲清 type、谁读取、坑点。
- **官方工程博客**：[Equipping agents for the real world with Agent Skills](https://anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills)——渐进式披露设计理念的一手来源。
- **Conventional Commits 规范**：[conventionalcommits.org](https://www.conventionalcommits.org/)——本 Skill 解析依据。
- **分发为插件**：用 `/plugin marketplace add anthropics/skills` 的思路，把你的 Skill 打包成可分发 Plugin。

---

## 训练营适用性评估

- **适合训练营类型**：Claude Code 进阶营 / 工程效率提升营 / DevOps 自动化营。不适合纯零基础营（需先掌握安装与首轮交互）。
- **前置模块**：建议放在"安装与首跑""CLAUDE.md 记忆"之后，作为第一个"造工具"模块。
- **教学时长**：建议 1.5–2 小时。其中概念讲解 20 分钟、手工搭建 50 分钟、回归测试与调试 30 分钟。
- **学员产出**：每人交付一个可触发、带 `allowed-tools` 围栏、有回归脚本的 Skill，并在班级仓库发起一次 CHANGELOG PR 作为结业作业。
- **难度分级**：⭐⭐⭐（满分 5）。难点在 `description` 触发词的打磨与"脚本/模型分工"的判断，而非语法本身。
- **可拓展方向**：把 `parse_commits.py` 换成调用 GitHub MCP 读取 PR；或加 `metadata.api_base` 让 Skill 适配私有 GitLab——可作为高阶作业。

---

## 参考来源

1. [anthropics/skills (GitHub)](https://github.com/anthropics/skills) — Anthropic 官方 Skill 示例库与 `template/` 骨架，含 `SKILL.md` 最小结构与 frontmatter 约定
2. [Extend Claude with skills (Claude Code Docs)](https://code.claude.com/docs/en/skills) — Claude Code 官方 Skill 创建、管理与共享文档
3. [Claude Code SKILL.md Frontmatter Fields: The Complete Reference](https://claudskills.com/learn/claude-code-skill-frontmatter-fields/) — 逐字段解析 `name`/`description`/`allowed-tools`/`argument-hint`/`tags` 等运行时行为与 YAML 坑点
4. [How to create custom skills (Claude Help Center)](https://support.claude.com/en/articles/12512198-how-to-create-custom-skills) — 官方创建自定义 Skill 的分步指引
5. [Conventional Commits 1.0.0](https://www.conventionalcommits.org/) — 本 Skill 提交解析与版本号推断依据的规范

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
