dsh-pre-push-checks
push 之前该跑哪些检查——挑最小的、能覆盖这次 diff 的那一组,而不是反射式地跑全套。
这是 .agents/skills/dsh-pre-push-checks/SKILL.md 的中文精读。它回答的问题是:这次 push 之前,到底该跑哪些检查?答案是「能覆盖这次 diff 的最小集合」,而不是反射式地把全仓套件跑一遍。
什么时候触发
原文 frontmatter 的触发条件:在 push、force-push、标记 ready for review、或声称「检查已通过」之前,以及 gh stack sync 发布完重写过的分支之后立刻使用——目的是为即将发出(或刚发出)的 diff 选出最小的测试与检查组合,而不是条件反射式地跑全仓套件。
Git hooks 被刻意做得很窄:pre-commit 只修暂存的 lint、查空白符、守 vendored 源码的元数据;pre-push 只跑增量 typecheck。穷尽覆盖和平台矩阵归 CI 管。
唯一的顺序例外是 gh stack sync——它会把层叠 rebase 直接发布出去,本地来不及插进验证。所以 sync 之后立刻补验,证据没齐之前不合并。
一、先看清楚这次要推什么
$ git status --short --branch $ git rev-parse --show-toplevel $ pnpm --silent run change-scope --base <verified-base-ref>
change-scope 从不猜、也不替你 fetch base——base 必须由你从当前远端或栈状态里核实后传入;要看非 HEAD 的提交就加 --head。它输出带版本号的 JSON:已提交路径(相对 merge base)、暂存 / 未暂存 / 未跟踪路径。合进了一个变化过的 base 之后,要重跑一次,重新评估合并后的范围能影响什么行为,只重跑被这次合并作废掉的检查。
二、按 diff 选证据
除了 hooks 之外,不存在「全仓通用基线」。每个行为变化都需要最窄的、回归时能变红的那个测试或检查;只有 diff 真的够到的面,才加更宽的检查:
| 动了什么 | 跑什么 |
|---|---|
| 包或脚本的行为 | 归属它的 Vitest 文件或具体测试名。共享契约变了才加相邻包的测试;全仓覆盖留给 CI |
| 文档、Agent Note、目录、文档关联注释 | pnpm run doc-sync;文档工作流要求时再跑全量 lint |
| 模型 / 编辑器 / CLI / 终端可见的输出 | 归属该输出的 keyless 快照或可运行示例场景 |
| 包清单、公共导出、构建配置、worker/bin 入口、构建产物路径 | pnpm run build + 相关 hygiene 检查 + 归属的构建产物冒烟 |
| 真实 provider 或 agent 行为 | 有凭证时跑对应的 pnpm run test:e2e 目标;绝不打印密钥 |
不要仅仅因为接下来要 commit 或 push,就手动重跑一个已经通过的检查——尤其不要在 push 前再跑一次 typecheck 去重复 pre-push hook 已经做过的事。
把覆盖率聚焦到受影响的源码上
选测试和选覆盖率是两件事。Vitest 的文件过滤器决定跑哪些测试,而仓库配置默认会统计所有 packages/*/*/src/**/*.ts。需要覆盖率证据时,两个都要指明:
$ pnpm exec vitest run packages/<group>/<package>/tests/<behavior>.spec.ts \
--coverage \
--coverage.include='packages/<group>/<package>/src/**/*.ts'
行为确实只关一个模块时就用精确文件;多个文件或包就重复 --coverage.include。选定范围内,逐文件 100% 的阈值依然生效。
拿不准归属测试时,用 Vitest 的依赖图找候选,但选定后要亲自看过这些测试再把这次运行当证据:
$ pnpm exec vitest related packages/<group>/<package>/src/<changed>.ts \
--run --coverage \
--coverage.include='packages/<group>/<package>/src/<changed>.ts'
vitest related 的盲区只通过配置、动态加载、子进程、worker、构建产物或外部 provider 才够得到的行为,related 发现不了——这些测试必须显式点名。也不要用 --passWithNoTests、降低覆盖率阈值、或收窄 --coverage.include 来把一个没覆盖到的受影响文件藏起来。
三、什么时候才允许全量本地彩排
只有三种情况:用户明确要求、诊断 CI 失败、或者改动横跨全仓到「任何更窄的组合都不可信」。用当前的工作流和 package scripts 作为清单;不要重建已经被移除的 check:pre-push 聚合。
四、保护改写历史的 push
独立分支和栈式 PR 分支都允许 rebase(review 之后也行)。但改写历史之前,先 fetch 当前远端分支并记下它的精确 OID,发布时用租约:
$ git push --force-with-lease=<branch>:<observed-oid>
这样远端被别人推进过时 push 会中止。gh stack push / gh stack sync 对它们管理的分支自带租约保护。裸 --force 永远不允许。重写后的 push 之后,重新 fetch 活跃 heads,重新审计未解决的 review 线程、approval、可合并性和检查——重写前的 commit hash 和行内评论锚点都不再是有效证据。
sync 后的验证序列
gh stack sync 把 fetch、级联 rebase、push 合成一步,本地验证插不进去。所以它返回之后:
- 重新查询每个分支头和 GitHub 官方栈顺序;
- 对照各自的 live PR base,检查每个被重写层的变化范围;
- 为每个受影响层跑本 skill 选出的相关证据;
- 所有检查通过之前,每个 PR 都保持未合并状态,汇报「验证进行中」。
sync 后验证失败,就把租约保护的已发布 heads 留在原地,修复、验证修复、再发布修正。不要因为命令本身成功就声称栈已就绪。
五、失败怎么办
普通 push 前有相关检查失败:停下,修复或解释阻塞点,不要 push 上去赌 CI 不一样。失败看起来是环境特有的?那就证明它:
- 记下精确命令、失败的测试、平台相关的差异;
- 确认与平台无关的那部分证据是绿的;
- 检查是必需的,就优先修跨平台的不确定性;
- 绕过本地 hook 仅限用户明确要求或同意,并且要汇报清楚到底什么失败了、为什么 CI 预期会不同。
六、push 流程
普通 push 和独立分支的 rebase push:
- 把选好的检查跑一遍;
- 正常 commit,并检查 pre-commit fixer 动过的文件;
- 正常 push(授权的重写分支用精确租约),让增量 typecheck hook 跑起来;
- 验证远端 ref 与本地
HEAD一致。
$ git rev-parse HEAD origin/$(git branch --show-current) $ gh pr checks # GitHub PR:push 后看远端 CI
pending 就如实报 pending;失败要先看原因再归因于分支或环境。
当 gh pr checks 说没有检查、/actions/runs?head_sha=<sha> 返回 total_count: 0 时,先读可合并性:
gh pr view <number> --json mergeable,mergeStateStatus
PR 处于 CONFLICTING/DIRTY 时 GitHub 根本不创建 pull_request 工作流运行——缺失的信号就是冲突本身,不是基础设施坏了。唯一修复是解决冲突;空 commit、--allow-empty push、draft/ready 来回切、revert 再恢复,全都只会让 total_count 停在 0 并留下垃圾历史。本地还合不了时,用 git merge-tree --write-tree HEAD origin/<base> 确认冲突路径。
它把「push 前跑全套」这个最常见的浪费,换成了一套可执行的判断:证据要配得上 diff,而不是配得上焦虑。而「CI 拥有穷尽覆盖」这条分工,让「本地少跑」从偷懒变成了制度。