From e8b9fd659e30d8ee3affcabbf197462e2765859c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 22:01:30 +0000 Subject: [PATCH] =?UTF-8?q?fix(ci):=20TypeScript=20Type=20Check=20?= =?UTF-8?q?=E8=B5=B0=20merge=20base=EF=BC=8C=E8=A1=A5=E4=B8=8A=E7=BC=BA?= =?UTF-8?q?=E5=A4=B1=E7=9A=84=20fetch-depth:=200=20(#6359)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3 --- .github/workflows/lint.yml | 16 ++++++++ .../scripts/build-schemas-check-mode.test.ts | 4 +- packages/spec/scripts/build-schemas.ts | 37 +++++++++++++++++-- 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f639db14e0..d4f2e5e8c9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -489,6 +489,22 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v7 + with: + # The authorable-surface deletion gate (#4650) below anchors on the + # MERGE BASE of HEAD with origin/main — that is the only anchor under + # which "a key this PR deleted" and "a key main gained since the fork + # point" are different facts. A shallow clone has no walkable + # ancestry, so `merge-base` fails and the gate falls back to + # origin/main's TIP, where those two facts collapse into one and the + # SECOND one is reported as the first: #6359 had PR #6356 (which + # touches no spec file at all) go red for "deleting" + # ui/BulkActionDef:requiredPermissions — a key main had just ADDED. + # + # Same line as the ESLint job above, opposite failure mode, and that + # is why it is spelled out here rather than cross-referenced: shallow + # degrades the slot-lookup ratchet to "not verified" (a false GREEN), + # and degrades this gate to a false RED on an innocent PR. + fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v7 diff --git a/packages/spec/scripts/build-schemas-check-mode.test.ts b/packages/spec/scripts/build-schemas-check-mode.test.ts index c069e8316f..95520f3676 100644 --- a/packages/spec/scripts/build-schemas-check-mode.test.ts +++ b/packages/spec/scripts/build-schemas-check-mode.test.ts @@ -1517,7 +1517,9 @@ describe('build-schemas.ts — the drift notice names the direction it measured // Truncation moves TWO things here, and the second was a surprise worth // writing down: `merge-base HEAD origin/main` itself fails once the walk is // cut, so `resolveSurfaceBase` falls back to origin/main's TIP as the - // baseline (it says so — "using origin/main tip … as the baseline anchor"). + // baseline (it says so — "no merge base is walkable here, so this run + // anchors on the origin/main TIP …", the line #6359 reworded to name what + // that anchor then MISJUDGES). // The pair being compared is therefore anchor-at-`tip` vs baseline-at- // `mainTip`, not the fork point at all. And the ancestry between them is // exactly what a grafted history cannot answer: `mainTip` is its own shallow diff --git a/packages/spec/scripts/build-schemas.ts b/packages/spec/scripts/build-schemas.ts index fff00ba113..2cb47576aa 100644 --- a/packages/spec/scripts/build-schemas.ts +++ b/packages/spec/scripts/build-schemas.ts @@ -1509,8 +1509,17 @@ function resolveSurfaceBase(): SurfaceBaseResolution | null { const git = gitInPackage; const committed = readCommittedSurfaceBase(); - // CI's typecheck job checks out shallow with no branch refs, so fetch the - // one ref this check needs (depth 1 — a single snapshot) before giving up. + // A checkout with no branch refs (any `fetch-depth: 1` job) cannot name + // origin/main at all, so fetch the one ref this check needs before giving up. + // + // This fetch is GUARDED by the probe above and that is load-bearing (#6359): + // where the ref already resolves — every full clone, and every CI job that + // checks out `fetch-depth: 0` — it does not run, so it cannot undo the depth + // its job asked for. Where it does run, it is `--depth=1` because the only + // thing it is trying to buy is the ability to NAME origin/main; deepening it + // here would silently make every shallow build pay for a full history it was + // configured not to want. The job that needs walkable ancestry declares that + // in its checkout step, which is where the cost is visible. let tipProbe = git('rev-parse', '--verify', '--quiet', 'origin/main^{commit}'); if (tipProbe.status !== 0) { git('fetch', '--quiet', '--depth=1', 'origin', '+refs/heads/main:refs/remotes/origin/main'); @@ -1526,7 +1535,29 @@ function resolveSurfaceBase(): SurfaceBaseResolution | null { const mergeBase = git('merge-base', 'HEAD', tip); const rev = mergeBase.status === 0 ? mergeBase.stdout.trim() : tip; if (mergeBase.status !== 0) { - console.log(` (shallow history — using origin/main tip ${tip.slice(0, 12)} as the baseline anchor)`); + // That "…is the merge base anyway" holds only while the merge ref is + // fresh. It is generated when the PR opens or updates and goes STALE as + // main advances, so on a branch that forked earlier the tip anchor + // carries keys the fork point never had — and this gate reads every one + // of them as a line THIS commit deleted. The direction is worth spelling + // out because the verdict it produces ("deleted without proof") reads + // like a severe spec violation and costs far more to diagnose than to + // fix: #6359 was one CI job missing `fetch-depth: 0`, and the PR it + // reddened (#6356) had not touched packages/spec at all. + // + // Diagnostic only — the verdict below is unchanged. Making this route + // stop MISJUDGING rather than merely announcing itself is a separate + // decision with a much wider blast radius: this block is top-level, so + // every `gen:schema` runs it, which means every shallow job that builds + // @objectstack/spec (ci.yml `build-core`, docker-publish, release, …) + // takes this path whenever that build is a cache miss. Tracked in #6452. + console.log( + ` (shallow history — no merge base is walkable here, so this run anchors on the\n` + + ` origin/main TIP ${tip.slice(0, 12)} instead. ⚠️ Under a tip anchor a key that main ADDED\n` + + ` after this branch forked is indistinguishable from a key this branch DELETED. If a\n` + + ` deletion is reported below for a file you did not touch, check that first — and if\n` + + ` this is CI, the job's checkout step needs \`fetch-depth: 0\` (#6359).)`, + ); } const baseline = readSurfaceKeysAtRev( git,