首页 › tutorials › 用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 基础流程、想从"写一段提示词"升级到"做一个可复用工具"的开发者。


前置条件


核心概念:Skill 的三层结构

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

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 技能,可以一键生成符合规范的骨架。在交互模式下直接说:

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

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

# 个人级 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

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

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,再逐字段拆解:

---
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(必填,唯一身份)

字段二: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)。所以:

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 的隐形陷阱:

写完用这条命令自检 YAML 是否合法(不必进 Claude Code 就能验证):

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:

#!/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()

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

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

预期结果(节选):

{
  "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
}

常见问题:


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

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

references/conventional-commits.md(精简版):

# 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 之后):

# 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`。无需每次都读。

几个写正文的要点:


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

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

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

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

也可以显式触发:

> /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/无头)模式把"触发→执行→输出"自动化:

# 回归用例 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. 准备示例仓库(若没有现成仓库):

mkdir demo-shop && cd demo-shop && git init -q
git config user.email "[email protected]"; 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):

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

3. 预期生成的 CHANGELOG.md:

## [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、按类型分章。这正是脚本(确定性解析)+ 正文(规则渲染)分工的成果。

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

延伸阅读


训练营适用性评估


参考来源

  1. anthropics/skills (GitHub) — Anthropic 官方 Skill 示例库与 template/ 骨架,含 SKILL.md 最小结构与 frontmatter 约定
  2. Extend Claude with skills (Claude Code Docs) — Claude Code 官方 Skill 创建、管理与共享文档
  3. Claude Code SKILL.md Frontmatter Fields: The Complete Reference — 逐字段解析 name/description/allowed-tools/argument-hint/tags 等运行时行为与 YAML 坑点
  4. How to create custom skills (Claude Help Center) — 官方创建自定义 Skill 的分步指引
  5. Conventional Commits 1.0.0 — 本 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学通信知识星球海报-扫码加入