首页
/ get-shit-done 的 Changeset 片段工作流:用 per-PR CHANGELOG 碎片机制消除发布记录合并冲突

get-shit-done 的 Changeset 片段工作流:用 per-PR CHANGELOG 碎片机制消除发布记录合并冲突

2026-09-07 16:10:23作者:秋阔奎Evelyn

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.md3740-consolidate-phase-tests.mddynamic-routing.md),并且对应测试从 tests/changeset-parse.test.cjstests/changeset-github-release-notes.test.cjs 一应俱全,是一条"已被 CI 完整约束过"的生产流水线。

.changeset/ 目录结构与真实片段示例

目录顶层组织如下:

  • .changeset/README.md —— 本工作流的规范文档;
  • .changeset/*.md —— 待消费的片段本体,可并行积累多个版本周期;
  • 顶层 CHANGELOG.md —— 发布时折叠后的最终产物。

仓库中的真实片段通常有两类命名风格:

  1. 由脚手架生成的三随机词风格,如 .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.
  1. 带 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.cjsparseArgs
--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 约定:AddedChangedDeprecatedRemovedFixedSecurity。该白名单在三个模块中以同一枚举维护,保证"写入端能通过的,消费端一定接受":

body 的写法要点

从仓库大量真实片段(如 CHANGELOG.md 中的渲染结果与 .changeset/ 原稿)可以提炼出两个惯例:

  1. 开头加粗命令名或模块名,例如 **\/gsd-foo` ...**`,使条目在分组浏览时一眼可定位;
  2. 一句话说清"用户可见的变化"——即读者升级后实际会感知到的行为差异,而非内部实现细节。

解析时的严格校验

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):

  1. 改动集中包含片段文件.changeset/<name>.md,排除 README.md)→ 通过(ok_fragment_present);
  2. 否则若 PR 带 no-changelog 标签 → 通过(ok_opt_out_label);
  3. 否则若改动不触及任何 user-facing 路径 → 通过(ok_no_user_facing_changes);
  4. 其余情况 → 拦截(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.cjscmdRender 完整执行:

  1. 全量解析:读取 .changeset/ 下全部 .md(排除 README.md),逐个经 parseFragment 解析;任一失败即整体中止(exit 1),不会带病发布;
  2. 切分旧文档splitChangelog 保留 # Changelog 标题区(lead),剥离旧的 ## [Unreleased] 占位块,其余历史内容作为 priorChangelog
  3. 纯函数归并renderChangelog(见 render.cjs)将片段按 type 分组,生成无 I/O 的类型化中间表示(IR)——这是测试断言的对象;
  4. 序列化serializeChangelog(见 serialize.cjs)按 ## [<version>] - <date>### <type>- <body> (#<pr>) 输出每个 bullet,并把旧历史原样拼接在尾部;
  5. 重建 Unreleased:在版本块上方写入一个全新的 ## [Unreleased] 占位块,供下一轮片段继续积累;
  6. 删除已消费片段:每个成功归并的片段文件被 unlinkSync 删除。

对照顶层 CHANGELOG.md 的既有渲染成果,可看到真实产物会在版本标题上附带的 compare 相对链接、并把最简序列化格式进一步美化为带粗体命令名的条目——渲染管线的核心契约(type 分组、(#NNNN) PR 后缀、版本日期头)与源码实现完全一致。

删除失败的半失败语义

片段删除若失败(如文件被占用),CHANGELOG 已写入而碎片仍在磁盘,重跑会双重消费。因此 cli.cjs 将删除失败以 fail_fragment_delete 结构码返回并置 exit 1,提示操作者先手工清理再重跑。

--json 结构化报告与幂等性

CLI 支持 --json 输出结构化报告(consumedfailuresrelease 等字段),这是测试唯一断言的输出契约。幂等性体现在:若某版本已渲染、片段已被消费,重复执行 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_TYPESnew.cjsparse.cjsrender.cjsgithub-release-notes.cjs 间保持一致,杜绝"能生成却解析不了"的不一致态;
  • 纯函数与副作用分离renderChangelog 无文件 I/O,只产出 IR;文件写入集中在 CLI 层。这使得单元测试可以不落地任何文件即可覆盖归并逻辑(对应 tests/changeset-render.test.cjstests/changeset-serialize.test.cjs);
  • 序列化与解析互为逆运算serializeChangelog / parseChangelog 在同一文件内成对实现,测试以 parse(serialize(ir)) 往返验证而非比对文本,规避了"禁止对测试输出做原文匹配"的仓库规范;
  • CRLF 感知parse.cjsgithub-release-notes.cjs 多处显式处理 \r\n,保证 Windows 上编辑的片段与 LF 片段产出字节一致的结果。

给其他仓库的迁移建议

若想把该工作流复用到自己的项目,只需移植如下内容并保持目录契约不变:

  1. 复制 scripts/changeset/ 全部七个模块(clinewparserenderserializelintgithub-release-notes);
  2. 在 CI 中把 node scripts/changeset/lint.cjs 作为 PR 门禁,并让维护者可按需打 no-changelog 标签;
  3. 发布日执行 node scripts/changeset/cli.cjs render --version <tag> --date <today>,随后提交 CHANGELOG 变更与片段删除;
  4. 如需 GitHub Release notes,追加 github-release-notes --from vPrev --to vCur

改动集判定中的 user-facing 前缀(USER_FACING_PREFIXESUSER_FACING_FILES,见 lint.cjs)需按目标仓库的实际产物目录调整——这是迁移时唯一必须定制的部分。全套行为已被仓库内 tests/changeset-*.test.cjs 覆盖,可作为移植后的回归基线。

小结

get-shit-done 的 changeset 机制用一个"写新文件而非编辑共享文件"的朴素思路,配合脚手架(原子写、随机名、类型消毒)、严格解析(六类错误码)、CI 门禁(user-facing 判定 + no-changelog 退出)与幂等渲染(render 六步流水线 + release notes 二次生成),把"发布记录"从最易冲突的共享资源变成了完全可并行的累积过程。无论你是 GSD 的贡献者,还是想在自研仓库引入碎片化 changelog 流水线,scripts/changeset/ 下的实现都值得直接参照。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389