Skip to content

resolveSurfaceBase() 的 tip fallback 是静默的正确性降级 —— 拿不到 merge base 时它照样把「删除」判成违规并 exit 1 #6452

Description

@hotlong

背景

#6359 拆出。#6359 的止血(给 lint.ymltypecheck job 加 fetch-depth: 0)只修好了一个 job;resolveSurfaceBase() 里那条 fallback 本身没动,本单记录的是它。

#6359 立单人建议「把假设变成断言」并建议一并做。实现时量到这条 fallback 的爆炸半径远大于一个 job,两条被建议的处置各自撞上一条硬约束,所以按 PD#10 拆出来交分诊 —— 拆开的理由(下面第三节的测量)是本单最值钱的部分,不要只当成「fallback 应显式化」。

现状

packages/spec/scripts/build-schemas.tsresolveSurfaceBase()

const mergeBase = git('merge-base', 'HEAD', tip);
const rev = mergeBase.status === 0 ? mergeBase.stdout.trim() : tip;

merge-base 走不通时静默落到 origin/main 的 tip。而 tip 锚下「main 在分叉后新增的键」与「本分支删掉的键」是同一个事实,门会把前者报成后者 —— #6359 实测:PR #6356 一行 packages/spec 没碰,被判「删除了 ui/BulkActionDef:requiredPermissions」,而那个键是 main 刚加的。

爆炸半径(实测,origin/main @ 26b72e0)

这三条是本单区别于「一个 job 的配置疏漏」的关键:

  1. 这段代码不受 --check 保护。 CHECK 常量在 build-schemas.ts:109,而调用 resolveSurfaceBase() 的块是顶层裸块(build-schemas.ts:1760 附近,{ const base = resolveSurfaceBase(); … }),没有任何 if (CHECK) 守卫。
  2. 判决是无条件致命的。 违规分支以 process.exit(1) 结束(build-schemas.ts:1907),同样不受 CHECK 守卫 —— 也就是说这不是「--check 模式下的一个门」,而是任何一次 gen:schema 都可能据此让构建红掉
  3. gen:schemabuild 的一部分。 packages/spec/package.json:185"build": "pnpm gen:schema && pnpm gen:openapi && tsup …"。于是每一个 shallow checkout 且会构建 @objectstack/spec 的 job 都走这条 fallback。仓库里 checkout 不带 fetch-depth: 0(即默认 1)的 workflow:ci.ymlbuild-core / test-gate / temporal-conformance / dogfood* 各 job(ci.yml:150 那个 fetch-depth: 0 只属于 test job)、docker-publish.ymlrelease.ymlpublish-smoke.ymlshowcase-smoke.ymlscaffold-e2e.ymlcoverage-nightly.ymlspec-liveness-check.ymlcodeql.ymlcheck-links.ymlvalidate-deps.yml

一处诚实的限定(未实测,留给接单人核)turbo.jsonbuild 声明为可缓存(outputs: ["dist/**", "json-schema/**", …]),所以在 packages/spec 未被改动的 PR 上 spec 的 build 很可能是缓存命中、gen:schema 根本不执行 —— 这大概率就是 #6356 只在 TypeScript Type Check 上红、Build Core 没红的原因。若成立,这条 fallback 的实际触发面是「改了 packages/spec 的 PR + 冷缓存的 job(docker/release/nightly)」。⚠️ 注意这个相关性是反的:它专挑改了 spec 的 PR 下手,而那正是「你删了一个 authorable 键」这句话最可信、也最费时间去自证清白的场合。

为什么 #6359 里没有顺手改掉它

#6359 建议的两条处置,在上面这个半径下各自撞墙:

两条都不是「成本高」,是「方向错」,所以不是在两个坏选项里挑一个的问题。

建议的第三条路(未实现,需设计裁决)

改锚,而不是改判merge-base 走不通、但 in-tree 锚 packages/spec/authorable-surface.base.json 存在时,锚到该文件的 baseRev(而不是 tip)。

⚠️ 但这需要重排 verifyCommittedSurfaceBase() 的验证互动:若基线的 keys 直接取自锚文件本身,会走进 rev === resolved.rev 的快路径而自我验证(拿文件验文件),比现状更弱;正确形态应是「rev 取 baseRev,keys 用 --depth=1 取回该 commit 后从 git 读」。这是一道有 #4650 / #5235 / #5358 / #5370 / #5847 / #5898 六单历史的门的设计决定,不该由一张「加一行 fetch-depth」的卡顺手拍。

已经落地的部分(#6359 的 PR 里)

只做了纯诊断的一半:shallow 那行日志现在点名方向(「tip 锚下 main 新增 == 本分支删除」)并指出「若这是 CI,该 job 的 checkout 需要 fetch-depth: 0」。零行为变更、零爆炸半径。判决逻辑一行未动 —— 那就是本单。

验收建议

  • 选定处置(建议第三条路)并说明它在 shallow 环境下不放宽门的依据;
  • packages/spec/scripts/build-schemas-check-mode.test.ts 已有 git 沙箱 harness(写 .git/shallow 即可造截断),新行为应在那里被钉住:同一棵树,shallow 下不再误报 main 新增的键,真删除仍然红
  • ⛔ 不要用有界 fetch-depth(50 之类)绕过 —— 那是把「永远走不通」换成「偶尔走不通」,更难诊断。

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions