get-shit-done 的 Changeset 片段工作流:用 per-PR CHANGELOG 碎片机制消除发布记录合并冲突
get-shit-done(GSD) 是一个面向 Claude Code 的元提示(meta-prompting)、上下文工程与规格驱动开发(spec-driven development)系统。本仓库在 docs/、agents/、get-shit-done/workflows/ 与 sdk/src/ 之外,维护着一个独立且完整的"发布记录流水线":以 .changeset/ 目录存放每 PR 一个 CHANGELOG 片段(per-PR CHANGELOG fragment),由发布时一次性折叠进顶层 CHANGELOG.md。阅读本文后,你将掌握片段的编写规范、脚手架与合并命令、CI 门禁的判定逻辑,以及从"提交 PR"到"渲染 release note"的完整链路,可将其直接迁移到任意高并发协作的仓库。
本文以 .changeset/README.md 为主体,结合 scripts/changeset/ 下各模块源码与 tests/changeset-*.test.cjs 测试展开。
为什么需要"每 PR 一个片段"而非"PR 直接改 CHANGELOG.md"
任何维护活跃的开源仓库都会遇到同一个痛点:两个 PR 恰好都要在 CHANGELOG.md 的 ### Fixed 块里追加一行时,git 无法自动决定串行顺序,合并必然产生冲突,只能靠人工裁决。
changeset 机制用一个简单的事实绕开它:两个 PR 同时编辑同一文件、同一区块必然冲突;两个 PR 各自新增一个全新的、互不共享行号的碎片文件则永不冲突。因此 .changeset/ 目录下每个带用户可见改动的 PR 都会放置一个(或多个)描述其 CHANGELOG 条目的 <随机名>.md 文件,这些碎片在发布时统一合并进顶层 CHANGELOG.md。该机制的完整设计背景记录于项目 issue #2975(详见 .changeset/README.md)。
值得强调的是,这并非仓库的"文档说明":.changeset/ 下当前真实存放着约 250 个待消费片段(例如 agile-birds-cheer.md、3740-consolidate-phase-tests.md、dynamic-routing.md),并且对应测试从 tests/changeset-parse.test.cjs 到 tests/changeset-github-release-notes.test.cjs 一应俱全,是一条"已被 CI 完整约束过"的生产流水线。
.changeset/ 目录结构与真实片段示例
目录顶层组织如下:
.changeset/README.md—— 本工作流的规范文档;.changeset/*.md—— 待消费的片段本体,可并行积累多个版本周期;- 顶层
CHANGELOG.md—— 发布时折叠后的最终产物。
仓库中的真实片段通常有两类命名风格:
- 由脚手架生成的三随机词风格,如 .changeset/agile-birds-cheer.md:
---
type: Fixed
pr: 3046
---
extractCurrentMilestone no longer silently falls through to archived milestones when the active milestone uses a <details><summary>vX.Y…</summary> structure. Phase lookups now correctly resolve to the active milestone's phases in FAMP-style ROADMAPs. Closes #2641.
- 带 PR 号前缀的语义化 slug风格,如 .changeset/3740-consolidate-phase-tests.md,这类片段通常承载较大改动的多段正文,甚至允许携带
<!-- docs-exempt: ... -->标记(详见下文"docs-exempt 豁免标记")。
新增一个片段:new.cjs 脚手架
GSD 不要求手写片段文件,而是提供脚手架脚本。原文档给出的核心命令为:
node scripts/changeset/new.cjs \
--type Fixed \
--pr 1234 \
--body "fix the thing — explain the user-visible change in one sentence"
在仓库根目录可直接使用 package.json 中暴露的 npm 脚本别名("changeset": "node scripts/changeset/new.cjs"):
npm run changeset -- --type Fixed --pr 1234 --body "your user-visible change"
参数说明
| 参数 | 必填 | 含义 | 校验规则(见 new.cjs 的 parseArgs) |
|---|---|---|---|
--type |
是 | 变更类型,须为 Keep a Changelog 六类之一 | 缺失或不在白名单直接报错(exit 2) |
--pr |
是 | 关联的 Pull Request 编号 | 会被 Number() 强转 |
--body |
是 | 一段面向用户的变更描述 | 缺失值或下一个 token 以 -- 开头即报错,防止标志位被误吞 |
--repo |
否 | 目标仓库根目录,默认 process.cwd() |
用于多仓库场景离线生成 |
三随机词命名:并发不冲突的工程细节
脚手架会写出 .changeset/<adjective>-<noun>-<noun>.md。三个随机词来自 new.cjs 内置的形容词、名词 A、动词性名词 B 三张词表,组合空间约 40 × 40 × 40 ≈ 6.4 万个不同文件名,因此并发 PR 之间"文件名恰好相同"的概率极低。
更关键的是源码里隐藏的两层保险:
- 原子创建:
fs.writeFileSync(target, content, { flag: 'wx' })——wx标志在目标文件已存在时直接抛出EEXIST,并发调用无法绕开existsSync的竞态窗口互相覆盖; - 碰撞重试:命中
EEXIST后自动重新随机抽取文件名,最多重试 16 次,耗尽预算才报错并提示排查.changeset/状态(见 new.cjs)。
写入前的类型消毒
type 值在写入 frontmatter 之前就会与白名单比对(new.cjs):一个包含换行符的 type 会污染 YAML frontmatter 结构,一个未知类型则会让后续 parse.cjs 给出令人困惑的报错。在写入边界提前拦截二者,是这套实现刻意设计的"防线前移"。
片段格式规范:frontmatter + body
片段文件由 frontmatter 与正文组成,规范格式为:
---
type: Fixed
pr: 1234
---
**`/gsd-foo` no longer drops trailing slashes** — explain the user-visible change.
type: 允许值与 Keep a Changelog
允许的 type: 遵循 Keep a Changelog 约定:Added、Changed、Deprecated、Removed、Fixed、Security。该白名单在三个模块中以同一枚举维护,保证"写入端能通过的,消费端一定接受":
- new.cjs —— 写入时消毒;
- parse.cjs —— 解析时校验;
- render.cjs 的
SECTION_ORDER与 github-release-notes.cjs —— 决定小节输出顺序。
body 的写法要点
从仓库大量真实片段(如 CHANGELOG.md 中的渲染结果与 .changeset/ 原稿)可以提炼出两个惯例:
- 开头加粗命令名或模块名,例如
**\/gsd-foo` ...**`,使条目在分组浏览时一眼可定位; - 一句话说清"用户可见的变化"——即读者升级后实际会感知到的行为差异,而非内部实现细节。
解析时的严格校验
parse.cjs 会把文本解析为类型化记录 { type, pr, body, docsExempt },任何不满足条件都会返回结构化错误码(冻结枚举 FRAGMENT_ERROR,测试断言稳定码而非自由文本):
| 错误码 | 触发条件 |
|---|---|
missing_frontmatter |
缺少 --- ... --- frontmatter 块 |
missing_type / invalid_type |
type 缺失或不在六类白名单 |
missing_pr / invalid_pr |
pr 缺失,或 Number(pr) 不是正整数 |
empty_body |
正文为空(含仅剩豁免标记的情况) |
docs-exempt 豁免标记
对确实不需要文档配套改动的片段,parse.cjs 支持行内独立的 HTML 标记:
<!-- docs-exempt: internal test refactor only — no user-facing surface changed -->
解析时该标记会被从正文剥离(并做 CRLF 感知的空白清理),确保它不会泄漏进 CHANGELOG 或 release note;reason 字段则是必须提供的"人工审计痕迹"——无理由的裸标记会被拒绝。真实用法可参考 .changeset/3740-consolidate-phase-tests.md,其中以"internal test refactor only"为由声明豁免。
CI 门禁:什么时候必须写片段,什么时候可以跳过
原文档指出:"PRs that legitimately have no user-facing impact can add the no-changelog label. CI honors it."这条规则的实际执行者是 lint.cjs(package.json 中对应 npm run lint:changeset)。
evaluateLint({ changedFiles, labels }) 的判定顺序如下(见 lint.cjs):
- 改动集中包含片段文件(
.changeset/<name>.md,排除README.md)→ 通过(ok_fragment_present); - 否则若 PR 带
no-changelog标签 → 通过(ok_opt_out_label); - 否则若改动不触及任何 user-facing 路径 → 通过(
ok_no_user_facing_changes); - 其余情况 → 拦截(
fail_missing_fragment)。
"user-facing"由 lint.cjs 中的前缀白名单决定:
- 目录前缀:
bin/、get-shit-done/、agents/、commands/、hooks/、sdk/src/、sdk/prompts/; - 精确匹配文件:
CHANGELOG.md(直接编辑顶层 CHANGELOG 绕过新工作流的旁路被显式封死)。
而 tests/、docs/、CI 配置、锁文件等改动不在此列,会自动放行。需要补充的是 CI 端如何取得数据:CLI 包装器读取 GITHUB_EVENT_PATH 中的 PR labels,并执行 git diff --name-only origin/<base>...HEAD 计算改动集(使用 execFileSync 传 argv 数组、不经 shell,避免 GITHUB_BASE_REF 注入 shell 语法)。对应测试见 tests/changeset-lint.test.cjs。
一个实操建议:拿不准时一律添加片段。额外片段在渲染时被正确分组,几乎无成本;而漏写被门禁拦截则需要在 CI 上补一轮。
发布时刻:render 将碎片折叠进 CHANGELOG
发布流程是整套机制的"收获期",原文档给出的命令为:
node scripts/changeset/cli.cjs render --version vX.Y.Z --date YYYY-MM-DD
也等价于 npm 别名("changelog:render": "node scripts/changeset/cli.cjs render"):
npm run changelog:render -- --version vX.Y.Z --date YYYY-MM-DD
render 的六步流水线
cli.cjs 的 cmdRender 完整执行:
- 全量解析:读取
.changeset/下全部.md(排除README.md),逐个经parseFragment解析;任一失败即整体中止(exit 1),不会带病发布; - 切分旧文档:
splitChangelog保留# Changelog标题区(lead),剥离旧的## [Unreleased]占位块,其余历史内容作为priorChangelog; - 纯函数归并:
renderChangelog(见 render.cjs)将片段按type分组,生成无 I/O 的类型化中间表示(IR)——这是测试断言的对象; - 序列化:
serializeChangelog(见 serialize.cjs)按## [<version>] - <date>→### <type>→- <body> (#<pr>)输出每个 bullet,并把旧历史原样拼接在尾部; - 重建 Unreleased:在版本块上方写入一个全新的
## [Unreleased]占位块,供下一轮片段继续积累; - 删除已消费片段:每个成功归并的片段文件被
unlinkSync删除。
对照顶层 CHANGELOG.md 的既有渲染成果,可看到真实产物会在版本标题上附带的 compare 相对链接、并把最简序列化格式进一步美化为带粗体命令名的条目——渲染管线的核心契约(type 分组、(#NNNN) PR 后缀、版本日期头)与源码实现完全一致。
删除失败的半失败语义
片段删除若失败(如文件被占用),CHANGELOG 已写入而碎片仍在磁盘,重跑会双重消费。因此 cli.cjs 将删除失败以 fail_fragment_delete 结构码返回并置 exit 1,提示操作者先手工清理再重跑。
--json 结构化报告与幂等性
CLI 支持 --json 输出结构化报告(consumed、failures、release 等字段),这是测试唯一断言的输出契约。幂等性体现在:若某版本已渲染、片段已被消费,重复执行 render 会因 fragments.length === 0 直接返回而不触碰任何文件——这正是原文档声称 "Idempotent" 的源码依据。
附赠能力:github-release-notes 子命令
cli.cjs 还提供了 github-release-notes --from REF --to REF 子命令:用 git diff 直接对比两个 ref 之间 .changeset/ 的片段变化(经 git show <ref>:<file> 按需读取),渲染成带分组标题的 GitHub Release notes 正文,甚至对 Fixed/Removed 片段按关键词做二次归类(见 github-release-notes.cjs)。这意味着发布说明几乎零成本地从同一批片段二次生成,片段一次编写、多处复用。
从源码设计反观:这套机制的工程要点
- 写入端与消费端共享同一枚举:
ALLOWED_TYPES在new.cjs、parse.cjs、render.cjs、github-release-notes.cjs间保持一致,杜绝"能生成却解析不了"的不一致态; - 纯函数与副作用分离:
renderChangelog无文件 I/O,只产出 IR;文件写入集中在 CLI 层。这使得单元测试可以不落地任何文件即可覆盖归并逻辑(对应 tests/changeset-render.test.cjs 与 tests/changeset-serialize.test.cjs); - 序列化与解析互为逆运算:
serializeChangelog/parseChangelog在同一文件内成对实现,测试以parse(serialize(ir))往返验证而非比对文本,规避了"禁止对测试输出做原文匹配"的仓库规范; - CRLF 感知:
parse.cjs与github-release-notes.cjs多处显式处理\r\n,保证 Windows 上编辑的片段与 LF 片段产出字节一致的结果。
给其他仓库的迁移建议
若想把该工作流复用到自己的项目,只需移植如下内容并保持目录契约不变:
- 复制
scripts/changeset/全部七个模块(cli、new、parse、render、serialize、lint、github-release-notes); - 在 CI 中把
node scripts/changeset/lint.cjs作为 PR 门禁,并让维护者可按需打no-changelog标签; - 发布日执行
node scripts/changeset/cli.cjs render --version <tag> --date <today>,随后提交 CHANGELOG 变更与片段删除; - 如需 GitHub Release notes,追加
github-release-notes --from vPrev --to vCur。
改动集判定中的 user-facing 前缀(USER_FACING_PREFIXES 与 USER_FACING_FILES,见 lint.cjs)需按目标仓库的实际产物目录调整——这是迁移时唯一必须定制的部分。全套行为已被仓库内 tests/changeset-*.test.cjs 覆盖,可作为移植后的回归基线。
小结
get-shit-done 的 changeset 机制用一个"写新文件而非编辑共享文件"的朴素思路,配合脚手架(原子写、随机名、类型消毒)、严格解析(六类错误码)、CI 门禁(user-facing 判定 + no-changelog 退出)与幂等渲染(render 六步流水线 + release notes 二次生成),把"发布记录"从最易冲突的共享资源变成了完全可并行的累积过程。无论你是 GSD 的贡献者,还是想在自研仓库引入碎片化 changelog 流水线,scripts/changeset/ 下的实现都值得直接参照。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00