Skip to content

CI: TypeScript Type Check 缺 fetch-depth: 0,authorable-surface 删除门把「main 新增的键」误判成「本 PR 删了键」 #6359

Description

@os-zhuang

现象

2026-08-07 15:02,PR #6356(只改 driver-memory / driver-mongodb 的类型签名,一行 packages/spec 都没碰)在 TypeScript Type Check 上红:

❌ 1 authorable baseline line(s) were deleted without proof (#4650):
   - ui/BulkActionDef:requiredPermissions — def reachable from the metadata-type roots;
     the entry at d8e8d9cbc892 was LIVE (never tombstoned).

同一 job 在上面几行已经自陈了原因:

(shallow history — using origin/main tip d8e8d9cbc892 as the baseline anchor)

归因(已坐实,非猜测)

d8e8d9cbc 是 main 当前 tip,提交标题逐字是 feat(spec): declare requiredPermissions on BulkActionDefSchema (#6257) (#6332) —— 它新增了这个 authorable key。PR #6356 的分叉点是 4d552af3f,落后 main 三个提交:

d8e8d9cbc feat(spec): declare requiredPermissions on BulkActionDefSchema (#6257) (#6332)
cfb549db8 feat(runtime): standalone stack dispatches mysql:// ... (#6344)
fb363b20e docs(lint): 按实测改正 `normalized` 输入层的三条依据 ... (#6340)
4d552af3f ← #6356 / #6355 的 base

于是「main 上新增的键」在分支侧看起来就是「本 PR 删掉的键」。方向恰好反了。

根因是一行 CI 配置

resolveSurfaceBase()(packages/spec/scripts/build-schemas.ts:1024-1031)的逻辑本身是对的,注释也写明了意图:

// Merge base, so a branch behind origin/main is compared against what it
// FORKED from (keys added on main since then are not "deleted" here). In a
// shallow clone there is no walkable ancestry — fall back to the tip, which
// on a PR's synthetic merge commit is the merge base anyway.
const mergeBase = git('merge-base', 'HEAD', tip);
const rev = mergeBase.status === 0 ? mergeBase.stdout.trim() : tip;

括号里那句假设正是失效的一环。而问题在于 fallback 不是罕见降级,它是这个 job 的常态路径:

.github/workflows/lint.yml:378 的 typecheck job 用的是 actions/checkout@v7 默认 fetch-depth: 1。加上门自己那句 git fetch --quiet --depth=1,merge-base HEAD origin/main 在这个 job 里永远走不通 —— 每一次运行都落到 tip 分支。

而 tip == merge base 这个假设只在「合并 ref 是对着 main 当前 tip 生成的」时成立。合并 ref 是 PR 打开/更新时生成、随 main 前进而变陈旧的;本例中合并 ref 建于 4d552af3f,--depth=1 却抓到了 d8e8d9cbc,两者不等,门就看见了幽灵删除。

同一个文件里的兄弟 job 已经踩过并修好了同一个坑(.github/workflows/lint.yml:32-38,ESLint job):

      - name: Checkout repository
        uses: actions/checkout@v7
        with:
          # The slot-lookup ratchet compares the baseline against its state at
          # the merge base with main — the only way to see a file being ADDED
          # to the grandfather list. A shallow clone has no merge base, and the
          # check would degrade to "not verified" on every run.
          fetch-depth: 0

typecheck job 需要的是同一句话,只是失效方式更糟:ESLint 那道门 shallow 时降级为不校验,这道门 shallow 时降级为误报红。

影响面

任何分叉点早于「某个新增 authorable key 的提交」、且此后跑过 TypeScript Type Check 的开放 PR,都会在一个自己从未碰过的文件上红。#6332 于本日 15:00 前后合入,此刻 #6356、#6355 均命中。这是一个假红发生器:它按 main 的合并节奏周期性地扫过所有在飞 PR,而报错文案(「删除了 authorable 键」「未经证明」)指向的是一个严重的规范违规,读起来完全不像环境问题 —— 排查成本远高于修复成本。

建议修法

  1. 一行修复:给 lint.yml 的 typecheck job 加 fetch-depth: 0,注释比照兄弟 job 写明「这道门要走 merge base」。
  2. 把假设变成断言(建议一并做):resolveSurfaceBase() 的 tip fallback 目前是静默正确性降级。既然 tip ≠ merge base 会产生假红而非假绿,这一路不该悄悄执行 —— 要么在 rev !== tip 无法判定时明确拒绝把「删除」判成违规(只报 ℹ️ 未验证),要么把「拿不到 merge base」直接变成对 CI 配置的显式报错。这样下次某个 job 忘了 fetch-depth: 0,红的是配置本身,而不是无辜 PR 的规范合规性。

现场处置

两个 PR 均已 update branch 合入 main 重跑,问题消失 —— 但那是绕开,不是修复;本单记录的是 CI 配置本身。

按 PD#10 只记录不修,未认领。

Activity

  1. self-assigned this
    on Aug 7, 2026
  2. hotlong commented on Aug 7, 2026

    @hotlong
    Contributor

    PM 定级 + 认领(pm-dispatch devx 座位) — 摘 finding,直接入队并派发。

    为什么由 PM 定级而不等分诊轮:立单人已把归因坐实到一行 CI 配置(不是猜测:给出了 d8e8d9cbc 的提交标题、分叉点 4d552af3f、中间三个提交,以及门自陈的 shallow history — using origin/main tip … as the baseline anchor),修法有名有姓,而且同一个文件里的兄弟 job 已有现成先例(lint.yml:32-38 的 ESLint job 带 fetch-depth: 0 并注释了理由)。证据面完整、无需拍板。

    为什么优先:它是一个假红发生器,按 main 的合并节奏周期性扫过所有在飞 PR;而报错文案(「删除了 authorable 键」「未经证明」)指向的是严重规范违规,读起来完全不像环境问题 —— 排查成本远高于修复成本。立单人已实测 #6356 / #6355 双双命中。⚠️ 更值得注意的是失效方向:ESLint 那道门 shallow 时降级为不校验,这道门 shallow 时降级为误报红。

    • Session: session_01BDmDsu2575gDxeMCxXhDE3
    • 分支: claude/issue-6359-typecheck-fetch-depth
    • 文件面: .github/workflows/lint.yml(typecheck job 的 checkout 步)+ 视评估连带 packages/spec/scripts/build-schemas.ts 的 resolveSurfaceBase()。

    范围两条

    1. 必做(止血):给 typecheck job 加 fetch-depth: 0,注释比照兄弟 ESLint job 写明「这道门要走 merge base」。⚠️ 顺带核实门自己那句 git fetch --quiet --depth=1 是否会抵消 checkout 的深度 —— 只改 checkout 而 fetch 仍 --depth=1 的话可能白做,这正是本单最容易「看起来修了实际没修」的地方。
    2. 评估后决定(立单人建议一并做):把 resolveSurfaceBase() 的 tip fallback 从静默正确性降级改成显式的 —— 拿不到 merge base 时,要么明确拒绝把「删除」判成违规(只报 ℹ️ 未验证),要么直接对 CI 配置报错。⇒ 由 dev 评估成本:小改则同批做(它才是防复发的那一半:下次某个 job 忘了 fetch-depth: 0,红的是配置本身而非无辜 PR);若牵动 packages/spec 的其他消费面而变大,⛔ 不要硬塞,按 Prime Directive chore: version packages #10 另立单并在 PR 正文说明。

    必须量的一件事

    fetch-depth: 0 会拉全量历史 ⇒ checkout 变慢,而这是两个 required job 之一所在的 workflow。请实测修前/修后该 job 的 checkout 步耗时并写进 PR 正文;若增幅显著(例如 >60s),如实报出,由 PM 判断是否需要折中(例如 fetch-depth: 50 之类的有界深度)⚠️ 但⛔ 不要自行改用有界深度 —— 那会把「永远走不通」换成「偶尔走不通」,是更难诊断的同类缺陷。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions