首页
/ get-shit-done(GSD)hotfix 自动发布管线:3621 如何修复 cherry-pick 丢失测试夹具导致的 CI 红灯

get-shit-done(GSD)hotfix 自动发布管线:3621 如何修复 cherry-pick 丢失测试夹具导致的 CI 红灯

2026-09-04 12:01:20作者:明树来

GSD(get-shit-done)的 release-sdk.yml 工作流提供了一条全自动 hotfix 路径:从最近的稳定 tag 切出 hotfix/X.YY.Z 分支,自动从 main cherry-pick 修复提交并发布到 npm。本文围绕 changeset #3621 展开,讲清这次修复解决的核心问题——为什么"生产修复被选中、配套测试夹具没被选中"会让 hotfix 分支 CI 直接变红——以及两半修复(commit 前缀过滤器加入 test:、shipped-paths 分类器新增 CI-gating 规则)如何在源码层面协同生效。读完本文,你能理解 GSD hotfix 管线中"三级筛选漏斗"的完整决策链,并能自行解读运行摘要中 Included / NON_SHIPPED_SKIPPED / CONFLICT_SKIPPED / POLICY_SKIPPED 四个分桶的含义。

背景:release-sdk hotfix 管线与"三级筛选漏斗"

release-sdk.yml 中,prepare job 的 "Prepare hotfix branch" 步骤实现了 hotfix 自动化。它的工作方式是:

  1. 从目标版本 X.YY.Z 反推 base tag(用 sort -V 做语义化版本比较,避免 v1.27.10v1.27.9 这类多位补丁号的字典序错排);
  2. git cherry HEAD origin/main 找出尚未进入 base tag 的 main 提交,作为候选集;
  3. 对每个候选提交依次过三道筛子,只有通过者才会真正 git cherry-pick

这三道筛子就是理解 #3621 的关键:

筛子 位置 规则 落桶
前缀过滤器 工作流内联正则 提交标题匹配 ^(fix|chore|test)(\([^)]+\))?!?: 不匹配 → POLICY_SKIPPED
shipped-paths 分类器 scripts/diff-touches-shipped-paths.cjs diff 路径与 package.jsonfiles 白名单(含 package.json 本身)取交集 无交集 → NON_SHIPPED_SKIPPED
cherry-pick 冲突处理 工作流内 冲突则跳过并分类原因(context absent / merge conflict) CONFLICT_SKIPPED

其中 shipped-paths 分类器被独立成脚本而不是内联在 YAML 里,源码注释给出了明确理由:"rules … are unit-testable"(bug #2980)。它从 stdin 读入 git diff-tree --no-commit-id --name-only -r <SHA> 的路径列表,用退出码表达三种结果:

  • 0 — 至少一个路径是 shipped(对 npm 包行为有意义),应 cherry-pick;
  • 1 — 没有任何 shipped 路径,hotfix 循环跳过该提交;
  • 2 — 分类器自身出错(package.json 缺失、I/O 失败、未捕获异常),工作流必须 fail-fast,而不能当作跳过处理。

退出码 2 的区分是 bug #2983 的产物:Node 未捕获异常的默认退出码是 1,若不显式区分,工具故障会被误当成"无 shipped 路径"而静默跳过提交。

事故复盘:v1.42.3 hotfix 为什么 CI 变红

#3621 的直接起因是一次真实的生产事故。回归测试 tests/bug-3621-cherry-pick-test-fixtures.test.cjs 的文件头注释完整记录了故障形态(v1.42.3 hotfix,run 25949422676):

  • 提交 36534059fix(3562) 前缀)被选中。它修改了安装器,使其在 Codex skills 目录下物化 gsd- 命名的 SKILL.md 文件——这是 docs/RELEASE-v1.42.3.md 中"routable skills"修复的核心代码;
  • 提交 08848df8docs(3562) 前缀)被前缀过滤器判为 POLICY_SKIPPED问题在于这个提交捆绑了配套的测试夹具修正——删除了 tests/install-minimal-all-runtimes.test.cjs 中一段 if (runtime === 'codex') return new Set() 的短路逻辑;
  • 结果:hotfix 分支同时具备"新的生产行为 + 过期的测试断言",3 个测试失败,CI 红灯。

这就是"捆绑失败模式"(bundling failure mode):生产修复与其测试对齐被拆在两个提交里,而 hotfix 过滤器只能看到提交标题,看不到语义上的依赖关系。测试文件头明确指出:只改其中一半,这个失败模式依然存在——必须两半修复同时落地。

修复上半:前缀过滤器放行 test: 提交

第一处改动在 release-sdk.yml 的候选循环中。修改后的正则(工作流内 "Prepare hotfix branch" 步骤):

if echo "$SUBJECT" | grep -qE '^(fix|chore|test)(\([^)]+\))?!?: '; then

变化点是把 test 加入前缀集合,使 test(3562): 这类夹具对齐提交能与 fix(3562): 生产修复一起进入候选集。正则保留了对可选 scope(如 (3562))和 breaking ! 标记的容忍,与原有 fix: / chore: 行为一致。

同时,这条正则刻意没有加入 feat:docs:。回归测试对这两点做了负向断言:

  • fix|chore 不能被加 test: 时"顺手"丢掉(must keep fix and chore as before);
  • 不能悄悄加入 feat:——特性提交不属于 hotfix 范畴;
  • 不能悄悄加入 docs:——纯文档提交仍应保持 POLICY_SKIPPED

值得注意的是,v1.42.3 事故中那条夹具提交的前缀是 docs(3562),而不是 test:。这意味着本次修复解决的是同类问题的未来形态:只要团队约定夹具对齐用 test: 前缀提交,管线就能把它和生产修复成对 pick 进来。changeset 原文也确认了这一点——"当生产修复捆绑在一个 fix: 提交里、而对应的测试夹具对齐落在另一个 test: 提交时,两者现在会被一起 pick"。

修复下半:shipped-paths 分类器新增 CI-gating 判定

仅放行 test: 提交还不够。一个只碰测试文件的 test: 提交,其 diff 路径全部落在 tests/ 下,而 tests/ 并不在 package.jsonfiles 白名单里(当前白名单为 bincommandsget-shit-doneagentshooksscriptssdk/srcsdk/sharedsdk/promptssdk/dist 等)。如果分类器仍只认"shipped"语义,test: 提交会在 shipped-paths 这道筛子被二次拦截,问题依旧。

因此 scripts/diff-touches-shipped-paths.cjs 新增了 isCiGating 判定:

// #3621: paths that gate hotfix-branch CI even though they don't appear
// in the npm tarball.
function isCiGating(diffPath) {
  if (diffPath.startsWith('tests/')) return true;
  // SDK vitest specs live next to source. Production source ships via
  // sdk/dist/ (already in package.json `files`); the test files are what's
  // missing from that surface.
  if (diffPath.startsWith('sdk/src/') && /\.(test|spec)\.(ts|cjs|mjs|js)$/.test(diffPath)) return true;
  return false;
}

规则覆盖两类路径:

  • tests/<任意路径> — 仓库根部的 node:test 测试目录(含 tests/helpers.cjstests/fixtures/** 等夹具);
  • sdk/src/<任意层级>/<名字>.test.<ts|cjs|mjs|js>.spec. 变体 — SDK 的 vitest 规格与源码同目录存放,生产代码经 sdk/dist/ 发版,而测试文件不在 npm tarball 里,正是需要"补票"的那部分。

分类逻辑的判定顺序(main() 的 stdin end 回调)是刻意的:

// #2980 still wins over #3621: any commit touching .github/workflows/*
// is unpickable regardless of other content because the push step
// fails on workflow scope rejection. Check this first.
if (paths.some(isPushBlocking)) {
  process.exit(EXIT_NOT_SHIPPED);
}
if (paths.some((p) => isShipped(p, shipPrefixes))) {
  process.exit(EXIT_SHIPPED);
}
// #3621: a commit whose only relevant paths are CI-gating tests is
// still pickable — it can change whether the hotfix CI passes even
// though it doesn't change what the npm tarball ships.
if (paths.some(isCiGating)) {
  process.exit(EXIT_SHIPPED);
}
process.exit(EXIT_NOT_SHIPPED);

语义上,tests/sdk/src/** 的测试规格现在被视为 CI-gating-equivalent(等价于 shipped):它们不进入 npm tarball,但决定 hotfix 分支测试 job 的生死。一个与已 cherry-pick 生产修复配套的夹具对齐提交必须可被 pick,否则 hotfix 运行必然失败——这正是 #3621 的头注释所总结的"根因"。

与 #2980 的优先级关系:workflow 文件仍是"一票否决"

#3621 扩展了可 pick 集合,但它不能也不得侵蚀 #2980 的 push-blocking 护栏。#2980 的背景是:默认 GITHUB_TOKEN 没有 workflow 权限,任何触碰 .github/workflows/* 的提交一旦被 pick 进 hotfix 分支,git push 步骤会被 GitHub 拒绝(v1.39.1 曾因此炸掉,run 25232010071)。

在分类器里,这条规则由 isPushBlocking 表达,且先于 isShippedisCiGating 判定:

function isPushBlocking(diffPath) {
  return diffPath.replace(/\\/g, '/').startsWith('.github/workflows/');
}

也就是说,一个 fix(release-sdk): 提交即使同时修改了 workflow 文件和配套的回归测试(例如 tests/bug-2980-hotfix-only-picks-shipping-changes.test.cjs),在 #3621 之后,测试路径虽然单独满足 CI-gating 检查,整个提交仍会被跳过。回归测试专门锁定了这一不变式:

test('push-blocking guard wins: workflow + test bundle classifies as NOT_SHIPPED (preserves #2980)', () => {
  // ... stdin: .github/workflows/release-sdk.yml + tests/bug-2980-*.test.cjs + CHANGELOG.md
  assert.equal(result.status, 1,
    '#2980 preservation: bundle with .github/workflows/* must skip regardless of test paths in the same commit');
});

这与 changeset 最后一句的声明完全对应:".github/workflows/<file> 的 push-blocking 护栏被保留(#2980):触碰 workflow 文件的 bundle 仍然整体跳过,不管其中还含有什么。"

回归测试:行为矩阵如何锁定修复

tests/bug-3621-cherry-pick-test-fixtures.test.cjs 从两个层面锁定这次修复:

层面一:工作流文本契约。 测试直接读取 release-sdk.yml 并提取含 grep -qE '^( 的正则行,断言其匹配 ^\(fix\|chore\|test\),同时负向断言不包含 feat|docs|。测试文件里有一条自证注释值得留意:

// allow-test-rule: source-text-is-the-product
// release-sdk.yml IS the product for hotfix automation; this test reads
// the workflow's prefix-filter regex line directly because the regex IS
// the behavior contract — there is no runtime that consumes it.

对于 CI 自动化工作流而言,YAML 本身就是"产品",前缀正则就是行为契约——没有运行时消费它,因此对源码文本做断言是合理且必要的。

层面二:分类器行为矩阵。 通过 spawnSync 在临时目录(写入最小 package.jsonfiles: ["bin", "sdk/dist"])中运行分类器,覆盖了完整的退出码矩阵:

输入 diff 路径 期望退出码 依据
tests/bug-foo.test.cjstests/helpers.cjstests/fixtures/... 0 tests/ 前缀即 CI-gating
sdk/src/query/init.test.tsinit.spec.tsgolden.integration.test.ts 0 SDK vitest 规格即 CI-gating
sdk/src/query/init.tssdk/src/config.ts 1(isCiGating 层面为 false) 非测试的 sdk/src 路径走 shipped 规则,本身不在最小 files
docs/test-strategy.mdbin/install-test-helper.jsdocs/install.test.md false 防止"路径名里含 test"的误报;.test.md 不是被识别的规格扩展名
tests/install-minimal-all-runtimes.test.cjs(v1.42.3 情形) 0 核心回归点
README.md + docs/CONFIGURATION.md + 两个 tests 文件(v1.42.3 提交形态) 0 混合 diff 只要含一条 CI-gating 路径即通过
纯文档 diff(README.md + docs/CONFIGURATION.md 1 文档不是 CI-gating,保持 NOT_SHIPPED
bin/install.js(正常 shipped 路径) 0 修复前的行为不得回归
.github/workflows/release-sdk.yml 1 工作流文件既非 shipped 也非 CI-gating
workflow + test + CHANGELOG 混合 1 #2980 护栏优先

对操作者意味着什么

对触发 hotfix 发布的人而言,#3621 之后运行摘要(GITHUB_STEP_SUMMARY)中的分桶语义有一处实质性变化:"Skipped — touches no shipped paths (informational)" 一节现在明确写着:被跳过的 fix/chore/test 提交既未触碰 npm tarball 的 files 白名单(或 package.json),也未触碰 tests/sdk/src/** 下的 CI-gating 测试(#3621)。CI / 文档 / 规划类变更属于 main,不属于 hotfix——无需操作。

实践上的直接结论:

  1. 夹具对齐提交请使用 test: 前缀(可带 scope,如 test(3562):)。这是它能进入 hotfix 候选集的唯一入口;docs: / feat: 前缀的夹具修正仍会被前缀过滤掉。
  2. 测试路径的落点有讲究:仓库根 tests/ 下任何路径,以及 sdk/src/ 下符合 .test. / .spec. + ts|cjs|mjs|js 后缀的文件都会被识别;其他位置(如 sdk/prompts/、自定义目录)不享受 CI-gating 待遇。
  3. 不要把 workflow 文件改动混进修复提交。即便同提交里带了合法的生产路径或测试路径,isPushBlocking 的一票否决仍会让整个提交被跳过——这是 #2980 保留的护栏,#3621 明确不推翻它。

小结

#3621 是一次典型的"自动化管线语义对齐"修复:hotfix 自动 cherry-pick 的目标不只是"把能改 npm 包内容的提交 pick 进来",还包括"把能让 hotfix 分支 CI 通过的提交 pick 进来"。为此它改了两处且必须成对生效——release-sdk.yml 的前缀正则从 fix|chore 扩为 fix|chore|testscripts/diff-touches-shipped-paths.cjs 的分类逻辑新增 isCiGating 判定(tests/**sdk/src/** vitest 规格视为 CI-gating 等价路径);同时 isPushBlocking.github/workflows/* 的一票否决保持最高优先级。两半修复的必要性由 tests/bug-3621-cherry-pick-test-fixtures.test.cjs 的完整行为矩阵锁定,与 tests/bug-2980-hotfix-only-picks-shipping-changes.test.cjs 共同构成 hotfix 管线的回归防线。这套"前缀过滤 → shipped/CI-gating 分类 → 冲突分桶"的三段式设计,对任何需要维护自动 hotfix 管线的 npm 项目都有直接参考价值。

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

项目优选

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