Skip to content

fix(ci): TypeScript Type Check 走 merge base,补上缺失的 fetch-depth: 0 (#6359) - #6459

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-6359-typecheck-fetch-depth
Aug 7, 2026
Merged

fix(ci): TypeScript Type Check 走 merge base,补上缺失的 fetch-depth: 0 (#6359)#6459
hotlong merged 1 commit into
mainfrom
claude/issue-6359-typecheck-fetch-depth

Conversation

@hotlong

@hotlong hotlong commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #6359

.github/workflows/lint.ymltypecheck job 用 actions/checkout@v7默认 fetch-depth: 1,于是 resolveSurfaceBase()packages/spec/scripts/build-schemas.ts)里的 merge-base HEAD origin/main 在这个 job 里永远走不通,每次都落到 origin/main 的 tip。合并 ref 随 main 前进而陈旧 ⇒ tip ≠ merge base ⇒ 「main 上新增的键」被读成「本 PR 删掉的键」。同文件的 ESLint job(lint.yml:32-38)早有同一句 fetch-depth: 0⚠️ 失效方向不同:那道门 shallow 时降级为不校验(假绿),这道门 shallow 时降级为误报红


1. 根因复现 —— checkout 深度与门内 --depth=1 的相互作用(实测)

这是本单最容易「看起来修了实际没修」的地方:门自己有一句

git('fetch', '--quiet', '--depth=1', 'origin', '+refs/heads/main:refs/remotes/origin/main');

它会不会抵消 checkout 的 fetch-depth: 0?在隔离 sandbox 里按 resolveSurfaceBase() 的 git 序列逐字复现(⛔ 没有在 worktree 里跑 --depth=1 —— 那会把共享 .git 弄成 shallow):

=== A. fetch-depth: 0 等价(全量 clone,origin/main 引用存在)===
is-shallow-repository        : false
gate probe exit (0 => skip)  : 0        <- 探针成功
  => 门的 --depth=1 fetch 被这个探针守着;这里根本不执行
merge-base HEAD origin/main  : exit=0 rev=84453479a18e   (== 真实分叉点)
is-shallow AFTER the gate ran: false

=== B. fetch-depth: 1 等价(无分支引用的 shallow checkout,即今天的 CI)===
gate probe exit (!=0 => fetch): 1
gate probe exit after fetch   : 0
is-shallow-repository         : true
merge-base HEAD origin/main   : exit=1 out=      <- 走不通
  => 落到 tip 70fcefe22698 —— #6359 的假红锚点

=== C. 如果那句 --depth=1 真的跑了,会不会毁掉全量 checkout?===
is-shallow before             : false
is-shallow after forced fetch : true             <- 会!
merge-base HEAD origin/main   : exit=1 out=
  => 是 A 里的探针守卫,才使 fetch-depth: 0 不被抵消

结论(实测):答案是有条件的否,而条件恰恰是本 PR 依赖的那一条。C 证明这句 --depth=1 确实有能力把一个全量仓库变成 shallow 并弄坏 merge-base;A 证明它git rev-parse --verify --quiet origin/main^{commit} 这个探针守着,而 fetch-depth: 0 恰好保证该引用存在 ⇒ 那句 fetch 一次都不会执行。所以本 PR 不是白做,但这个守卫是承重的 —— 已在 resolveSurfaceBase() 上方把这一点写成注释,免得下一个人「顺手」把它改成无条件 fetch 或把深度提上去。

本地对照:本容器的 checkout 自身就是 shallow(git rev-parse --is-shallow-repositorytrue),但本分支直接坐在 origin/main 之上,所需的 walk 是平凡的,所以 merge-base 仍成功、check:authorable-surface EXIT=0 且没有打印 shallow 那行。CI 里 HEAD 是合并 ref,depth=1 一刀就切在祖先上 —— 这就是差别。

2. 范围 (2) 的评估结论:拆出 #6452,本 PR 只留纯诊断的一半

立单人建议一并把 resolveSurfaceBase() 的 tip fallback 显式化。评估后拆开,依据是量到的爆炸半径:

  1. 调用 resolveSurfaceBase() 的是 build-schemas.ts顶层裸块,没有 if (CHECK) 守卫(CHECK 常量在 :109);
  2. 违规分支以 process.exit(1) 结束(:1907),同样不受 CHECK 守卫
  3. gen:schema@objectstack/specbuild 的第一步(package.json:185)。

⇒ 每一个 shallow checkout 且会构建 spec 的 job 都走这条 fallback:ci.ymlbuild-core / test-gate / temporal-conformance / dogfood*ci.yml:150 那个 fetch-depth: 0 只属于 test job)、docker-publish / release / publish-smoke / showcase-smoke / scaffold-e2e / coverage-nightly / spec-liveness-check 等。

于是 issue 建议的两条处置各自撞上一条硬约束,不是「成本高」而是「方向错」:

处置 失败方向
(a) 拿不到 merge base 就只报 ℹ️ 未验证 在上述所有 shallow job 里放宽 #4650 删除门 —— 本单最重的禁项。resolveSurfaceBase() 自己的文档注释早已判它死刑:「a deletion check that silently skips is the #4650 bypass with extra steps」
(b) 拿不到 merge base 直接报错 上述那批 job 第一天全红 —— 用一个更大的假红换掉一个假红

真正稳妥的第三条路(改锚到 in-tree authorable-surface.base.jsonbaseRev,而不是改判)需要重排 verifyCommittedSurfaceBase() 的验证互动 —— 否则会走进 rev === resolved.rev 快路径拿文件验文件,比现状更弱。那是一道有 #4650 / #5235 / #5358 / #5370 / #5847 / #5898 六单历史的门的设计决定,不该由一张「加一行 fetch-depth」的卡顺手拍 ⇒ 按 PD#10 立 #6452(未认领,交分诊;正文带上面这份爆炸半径测量)。

本 PR 保留的是纯诊断的一半:shallow 那行日志从「静默降级说明」改成点名方向的诊断。判决逻辑一行未动 ⇒ 行为零变更 ⇒ 爆炸半径为零。实测渲染(截自下面 B-before 那次真实运行,位置恰在假红正上方):

   (shallow history — no merge base is walkable here, so this run anchors on the
    origin/main TIP ebac7eac9b7a instead. ⚠️  Under a tip anchor a key that main ADDED
    after this branch forked is indistinguishable from a key this branch DELETED. If a
    deletion is reported below for a file you did not touch, check that first — and if
    this is CI, the job's checkout step needs `fetch-depth: 0` (#6359).)

3. checkout 耗时 before/after(实测)

最干净的对照来自同一个 run 的两个 job(同一 commit、同一时刻、同一 runner 池),差别只有 fetch-depth

run 31219895689 checkout 步 耗时 job 总时长
ESLint(已有 fetch-depth: 0 21:24:15 → 21:24:26 11 s 3 min 19 s
TypeScript Type Check(改前,默认 depth 1) 21:24:20 → 21:24:24 4 s 14 min 55 s

⇒ 预期增幅约 +7 s,落在一个 ~15 分钟的 job 上约 +0.8%,远低于 60 s 的折中阈值。本 PR 自己 run 上的实测值见下方「CI 实测」一节(本 PR 就是该 job 的活体测试)。⛔ 未改用有界 fetch-depth50 之类)—— 那是把「永远走不通」换成「偶尔走不通」,是更难诊断的同类缺陷。

4. 反向验证 A / B / C(先申报、后执行)

三条断言的极性先写明:A 是否定式(结构上不会红:它断言某行日志不再出现)、B 是否定式 + 一个肯定式对照C 是肯定式(必须红且点名)。

声明 A —— merge base 真的走得通

  • 申报:改后在 CI 的等价条件下 merge-base HEAD origin/main 返回真实分叉点而非退化到 tip;最有判别力的证据是门自陈的那行日志:改前打印 shallow 锚点行,改后不再打印
  • 实测:sandbox 段 A(上)—— 探针 exit 0、门内 fetch 不执行、merge-base exit=0 且等于真实分叉点、跑完仍 is-shallow=false。本地 check:authorable-surface EXIT=0 且 grep -c "shallow history|no merge base is walkable" = 0。本 PR run 上的真实 job 日志见「CI 实测」。
  • ⚠️ 空绿自查:这条是否定式,「日志没出现」本身可以因为「门根本没跑」而为真。所以它不单独成立,由 C(同一条代码路径上的肯定式红)承担「门确实在跑」的举证;且本地那次 check:authorable-surface 有完整的 1613 schema 生成输出与 merge base 字样,不是空跑。

声明 B —— 假红场景不再发生

  • 申报:构造「分叉点落后于某个新增 authorable key 的提交」⇒ 改前该门误报删除、改后不报。

  • 实测(端到端,不是论证):用 build-schemas-check-mode.test.ts 既有的 git 沙箱 harness(写 .git/shallow 造截断),跑真实的门

  • ⚠️ 空绿自查:B-after 是否定式,所以它由 B-before 的肯定式红配对 —— 同一 fixture、同一棵树,唯一变量是 .git/shallow 的有无。两次都跑到了门并产生了完整输出。

  • 说明:这四个探针用例是临时加入该文件跑完即撤的,未提交 —— 见下面「不在本 PR 里」的理由。

声明 C —— 真删除仍被抓(⛔ 最重要:本单绝不放宽删除门)

  • 申报:构造一个真的删除 authorable key 的改动 ⇒ 门必须仍然红且点名。肯定式,必须实测。
  • 实测status=1,点名:
    ❌ 1 authorable baseline line(s) were deleted without proof (#4650):
         - data/Object:zzReallyDeleted6359 — def reachable from the metadata-type roots;
           the entry at 87f526fc1c7b was LIVE (never tombstoned).
    
  • ⚠️ 空绿自查(C-control):同一 fixture 去掉那条被删的 line ⇒ status=0。这证明 C 的红是那条删除造成的,而不是环境里的既有红。
  • 另外:改动落地后 @objectstack/spec 全量 339 个测试文件 / 8641 个用例通过,其中 build-schemas-check-mode.test.ts 的 55 个用例(含多条 deleted without proof肯定式红断言)全绿 —— 删除门的判决面一行未动。

5. 门禁 EXIT 表(均在 git add 之后跑)

门禁 EXIT
pnpm check:workflow-status-functions(改 workflow 必跑) 0 — 22 workflow / 41 job / 24 job 级 if: 全部合规
YAML 解析校验(全部 22 个 workflow,并断言解析值) 0typechecklint 两个 job 解析出的 with 都是 {"fetch-depth":0}
pnpm check:nul-bytes 0 — 6070 个 tracked 文本文件,无裸控制字节
控制字节自扫 grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'(三个改动文件) 1(无匹配 = 干净)
pnpm --filter @objectstack/spec typecheck 0
packages/spec build-schemas-check-mode.test.ts(55 用例) 0
pnpm --filter @objectstack/spec check:authorable-surface(即 CI 那一步) 0,且无 shallow 日志行

6. 不在本 PR 里


Generated by Claude Code

`lint.yml` 的 `typecheck` job 用 `actions/checkout@v7` 的默认 `fetch-depth: 1`,
于是 `resolveSurfaceBase()` 里的 `merge-base HEAD origin/main` 在这个 job 里
永远走不通,每次都落到 origin/main 的 tip。而合并 ref 随 main 前进而陈旧,
tip != merge base,「main 新增的键」就被读成「本 PR 删掉的键」——方向恰好反了。

同文件的 ESLint job 已有同一句 `fetch-depth: 0`,但失效方向不同:那道门
shallow 时降级为不校验(假绿),这道门 shallow 时降级为误报红。

一并把 shallow 那行日志从「静默降级说明」改成点名方向的诊断:tip 锚下
main 新增 == 本分支删除,且若在 CI 则该 job 的 checkout 需要 fetch-depth: 0。
判决逻辑一行未动;把 fallback 本身显式化的那一半爆炸半径过大,拆到 #6452。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 7, 2026 10:04pm

Request Review

@hotlong hotlong added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 7, 2026 — with Claude
@github-actions github-actions Bot added the size/s label Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

No hand-written docs reference the 0 changed package(s). ✅

@hotlong
hotlong marked this pull request as ready for review August 7, 2026 22:34
@hotlong
hotlong added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit 4c9a85e Aug 7, 2026
29 checks passed
@hotlong
hotlong deleted the claude/issue-6359-typecheck-fetch-depth branch August 7, 2026 22:49
os-project-manager added a commit that referenced this pull request Aug 7, 2026
#6459 (#6359's stop-gap) landed the shallow diagnostic this PR's fallback
replaces. Resolution keeps the re-anchor branch and hands #6359's wording to the
one arm it still describes — the tip fallback, reached only when no upstream
anchor is usable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZgRyPVwi1jLb1mNNuUQ9o
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants