Skip to content

ci(lint): Type Check · workspace type-checks the affected packages on PR and queue runs; the whole workspace on push and schedule - #23043

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-23017-typecheck-cache-affected
Oct 12, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-23017-typecheck-cache-affected

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #23017
Clause-②: no

Type Check · workspace (the typecheck-workspace lane behind the required TypeScript Type Check context) now type-checks only the packages a pull_request / merge_group diff affects; schedule, push and workflow_dispatch keep the whole workspace (the full battery, #22998). The job name, the TypeScript Type Check aggregate and its roster are unchanged. One file changes: .github/workflows/lint.yml, one step (Type check workspace packages) and the comment above it.

The card's mechanism, measured first, and refuted

The card's hypothesis was that the turbo cache never restores on PR and queue runs because the restore keys are scoped to github.ref_name. On main at edecd44 the restore step carries FOUR restore keys; the third is the bare prefix ${{ runner.os }}-turbo-typecheck-, which matches the newest main-push save on every event. Measured on the jobs API and the job logs:

  • PR run 38239402273 (claude/issue-22992-showcase-boot-splash): restore 10 s, Cached: 138 cached, 139 total, Type check workspace packages 19 s. The cache restores.
  • The same PR's merge group, run 38242582445, 29 minutes later: Cached: 64 cached, 139 total. All 64 hits are build tasks; 0 of 74 typecheck tasks replayed; step 512 s (turbo Time: 8m31.023s).
  • Every task hash differed between the two runs (132 of 132 common task ids). @objectstack/types#typecheck computed locally is 88ca22ad9987a868 at 3071d3e, 5d35a004a079ced5 at 2acff87, and 30eade7d14ef0536 from c7d6037 through edecd44, the last equal to the hash the merge group logged. 2acff87 and c7d6037 both touch packages/spec. Nearly every package depends on spec, and a typecheck hash includes its dependencies' build hashes.

So the restore-keys fix the card proposes (a -typecheck-main- line) would change nothing: the bare prefix already restores main's seed, and on PR runs main's saves are the only typecheck saves that exist. The seed misses because of hash churn. A merge group is built on entries that have not reached main yet, so its seed is behind by construction. Commit (1) of the suggested route was therefore not made. H4 (the build step misses for the same key reason) is refuted the same way: on pull_request the build step reads the remote cache (remote:r) and its misses are the PR's own affected packages, which nobody has written yet; on merge_group the build step is a remote:rw writer and its median is 114 s.

What changed

The step's run:, with the comment above it explaining each rule:

  • pull_request / merge_group: scripts/ci/select-shard-packages.sh, ci.yml Test Core's own selector, called with the same event fields. Both jobs therefore read one rule for where a diff starts: merge-base with origin/main on a PR, the group's base_sha in the queue, and the whole list with a ::warning:: when neither resolves. The selection includes dependents (turbo ls --affected). It also unions in packages whose tests read the changed files across a package boundary (check-cross-package-test-inputs.mjs --union-into), because their typecheck programs compile those same tests. For example, lint's and cli's tests import examples/app-showcase, which no ^build edge reaches. The selection is intersected with the unchanged universe (./packages/*, ./packages/*/*, ./apps/*) and passed to turbo run typecheck as one --filter=NAME flag per package.
  • A root-level change means the whole universe. The workflow states this rule itself instead of leaving it to turbo. Measured on turbo 2.11.5 at edecd44, one file changed per probe: tsconfig.json, tsup.config.ts (the globalDependencies) and turbo.json select all 81 packages under turbo ls --affected. Root package.json, pnpm-workspace.yaml, .npmrc, and a change to the ROOT importer's typescript version in pnpm-lock.yaml select ZERO. So any changed repo-root file (a path with no /) means the whole universe, except Markdown, because no tsc program reads a root *.md. A changed scripts/ file that a package's own package.json scripts run also means the whole universe. For example, scripts/check-test-typecheck.mts is run by the typecheck of 72 of the universe's 76 packages. That set is found with git grep at run time, not kept as a list.
  • An empty selection is an honest green and says so in the log. It exits before turbo run typecheck, because that command with no --filter runs the whole workspace. Every doubt falls back to the whole universe: no changed-file list, a failed union, or a base that cannot be resolved.
  • schedule, push, workflow_dispatch and any other event: the whole universe, as before. The push-only cache save is unchanged, so main's seed stays complete.

turbo.json is unchanged. H2 found no input that moves on every commit: the typecheck hash stays the same across commits that do not touch its dependency closure (measured above).

Measurements

Before. The card (29 runs): step median 486 s, p90 627 s; job wall median 11.1 min, p90 14.2 min. Re-read over 300 lint.yml runs (2026-10-11T20:38Z to 2026-10-12T11:35Z), counting only successful Type Check · workspace jobs:

event n step median / p90 job wall median / p90
pull_request 117 479 s / 641 s 11.7 / 17.8 min
merge_group 93 510 s / 633 s 11.7 / 15.0 min
push 30 417 s / 621 s 8.2 / 11.8 min
schedule 14 1 s / 429 s 1.3 / 8.2 min

Selection replay. The final step script was run on the last 60 first-parent commits of main ending at edecd44, each as a merge group on its parent. turbo ls and the union were real; turbo run typecheck was stubbed. Results, out of a 76-package universe:

packages selected commits
none 4
1–5 20
6–20 6
21–50 8
51–72 (spec-touching) 16
whole universe 6

The 6 whole-universe runs: 3 objectui pin bumps that also touch packages/spec, 1 lockfile change, 1 .gitattributes change and 1 change to scripts/sync-scaffold-emission-policy.mjs.

Local timing. On a 4-core / 15 GB box (the ubuntu-latest shape), with a fresh turbo cache dir and the dependency closure built beforehand, every selected typecheck task is a cold miss. A one-line diff in packages/plugins/plugin-email/src/index.ts selects 11 packages:

  • The 6 packages the dependency graph reaches type-check in 1m19s.
  • All 11, including the union's five (spec, core, types, objectql, runtime), type-check in 2m32s.

Compare that with the whole universe's 486 s median on the runner. With the seed restored, any of the 11 whose own hash is in the seed replays instead of running.

This PR's own run: run 38252457300 on 06f9521d — Type Check · workspace job wall 56 s, Type check workspace packages 2 s (log: No workspace package is affected by this diff -- nothing to type-check.); TypeScript Type Check and Lint & Repo Gates green. Every turbo restore key missed on that run (Cache not found for input keys: Linux-turbo-typecheck-23043/merge-..., Linux-turbo-typecheck-, Linux-turbo-build-core-...), so the run also shows the step is independent of main's seed.

Cache hit count: the dispatch asked for the turbo summary's hits on untouched packages. With this change, PR and queue runs do not schedule untouched packages at all, so there is no hit count to quote. The pre-change PR run 38239402273 above (138 of 139) shows that the restore already reaches main.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 43 commands from this diff. All 43 ran on 06f9521d and exited 0, reconciled with --ran (43 run, 0 NOT-MEASURED (a DERIVED zero ...)). They include check:required-contexts, check-aggregator-roster, check:type-check-coverage, check:type-check-debt, check:cross-package-test-inputs, scheduled-full-run --self-test, check:pm-dispatch-gates (fast tier, its off-CI default) and check:nul-bytes. No pin script flagged anything (H5): the step keeps its name and its turbo run typecheck invocation.

The YAML was parsed with yaml, and the extracted run: passes bash -n. The script was also replayed against synthetic merge-group and PR diffs:

diff selection
leaf package 11 packages
root package.json whole universe
root-importer lockfile whole universe
scripts/check-test-typecheck.mts whole universe
workflow file only none
examples/app-showcase cli, dogfood, lint, spec
push / schedule event whole universe
unresolvable base_sha whole universe, with the selector's warnings

Acceptance notes

  • The card and the claim call .github/workflows/** Tier H, but GOVERNED_SURFACES in scripts/pm/check-governed-merges.mjs has no workflows row. This PR stays a draft as dispatched either way.

  • typecheck's turbo inputs are package-local. A package scheduled through the union or the root rule still replays a cached green when its own hash is in the seed, on every event including schedule. This PR does not change that, and the step comment says so. Declaring $TURBO_ROOT$ inputs for typecheck (as test has them) would be the fix. It is not attempted here, because it changes hash semantics for every package.

  • The union is a superset for tsc: it also names files that tests only read, so spec, core, types, objectql and runtime join most package diffs (+73 s cold in the local timing above). A tsc-only radius would need its own declaration and gate.

  • Root Markdown is exempt because AGENTS.md alone was 2 of the last 30 commits. Any other root file, including .objectui-sha, .gitattributes and sdui.manifest.json, means the whole universe.

  • lint.yml is at GitHub's workflow-file size ceiling. This branch at 513,738 bytes (0bd33ce6, d4265926, b39592f3) produced a failed zero-job workflow-file run on each push (38245964547, 38249293618, 38250844690) and the PR got no Lint & Type Check run at all; 06f9521d at 510,016 bytes is valid; main is 505,924 bytes. The step comment was compacted for that reason, and this body carries the rationale. The ceiling itself is carded by the seat (a gate on the file's size and a split plan).

维护者速读(草稿)

改了什么:Type Check · workspace 这条类型检查车道,在 PR 和合并队列上只检查本次改动影响到的包(含依赖它们的包);每小时定时、推到 main 和手动触发仍然全量检查。作业名、必需检查 TypeScript Type Check 及其成员都不变。只改了 .github/workflows/lint.yml 里的一个步骤。

为什么改:这一步的中位耗时约 8 分钟,是合并队列里最长的环节之一。卡片原先以为是缓存键写错,导致 PR 恢复不到 main 的缓存。实测缓存其实能恢复(某个 PR 的 139 个任务命中了 138 个)。真正的原因是几乎所有包都依赖 spec:只要 spec 一变,缓存里的检查结果就全部作废;而队列里的条目总比 main 新,所以几乎每次都全量重跑。按改动范围挑包才能根治。

风险与代价(含回滚):改动仓库根目录的文件(锁文件、根 package.json、tsconfig 等,Markdown 除外),或改动各包类型检查共同调用的脚本时,一律全量检查,不交给 turbo 判断。实测 turbo 对根 package.json 的变化、以及锁文件里 TypeScript 版本的变化,都会判为"没有包受影响"。被其他包的测试跨包引用的包(例如示例应用被 lint/cli 的测试引用)也会被并入检查。代价是:普通的包改动会顺带检查 spec 等五个大包,冷启动时约多 70 秒。改动了 spec 的提交(约占三分之一)仍然接近全量。回滚只需还原这一个步骤,不涉及数据或产物迁移。

席位意见:

你要做的:审阅后决定是否批准这个草稿 PR。落地后,由 PM 读取连续三个合并队列和三个 PR 的实测耗时。

Body amended by the director seat from the dev's report 6124930963 (three stale spots named there: the gates sha, this PR's own run, the size-ceiling note), 2026-10-12T13:13Z.


Generated by Claude Code

… PR and queue runs

pull_request and merge_group select the packages their diff affects (plus
dependents, plus the declared cross-package test radius) through ci.yml's own
selector; schedule, push and workflow_dispatch keep the whole universe. A
changed repo-root file, or a scripts/ file a package's own scripts run, means
the whole universe: turbo ls --affected selects zero packages for a root
package.json, pnpm-workspace.yaml, .npmrc or root-importer lockfile change.

Claude-Session: https://claude.ai/code/session_01LqNgdPZB2Jnr5HpGbaqLeB
Co-authored-by: Claude <noreply@anthropic.com>
…union is a stated superset

Claude-Session: https://claude.ai/code/session_01LqNgdPZB2Jnr5HpGbaqLeB
Co-authored-by: Claude <noreply@anthropic.com>
… what the cache still decides

Claude-Session: https://claude.ai/code/session_01LqNgdPZB2Jnr5HpGbaqLeB
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 12, 2026
@github-actions github-actions Bot added the ci/cd label Oct 12, 2026
…w-file size ceiling

The three previous pushes of lint.yml (513,738 bytes) each produced a failed
workflow-file run with zero jobs, and the pull request got no Lint & Type
Check run at all; main's copy is 505,924 bytes. The rationale lives in the
pull request body.

Claude-Session: https://claude.ai/code/session_01LqNgdPZB2Jnr5HpGbaqLeB
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读(终稿)· PR #23043 · 席位已对照 diff 逐行校正,2026-10-12T13:14Z

改了什么:Type Check · workspace 这条类型检查车道,在 PR 和合并队列上只检查本次改动影响到的包(含依赖它们的包,以及被其他包测试跨包引用的包);每小时定时、推到 main 和手动触发仍然全量检查。作业名、必需检查 TypeScript Type Check 及其成员都不变。只改了 .github/workflows/lint.yml 里的一个步骤(+69/−1)。

为什么改:这一步中位约 8 分钟,是合并队列里最长的环节之一。卡上原以为是缓存键写错、PR 恢复不到 main 的缓存;dev 实测缓存能恢复(某 PR 139 个任务命中 138 个),真正原因是几乎所有包都依赖 spec:spec 一变,缓存里的结果全部作废,而队列里的条目总比 main 新,所以几乎每次都全量重跑。按改动范围挑包才是根治,缓存键那一步因此没改(改了也是空操作)。

风险与代价(含回滚):改仓库根目录文件(锁文件、根 package.json、tsconfig 等,Markdown 除外)或各包脚本共同调用的 scripts/ 文件时一律全量,不交给 turbo 判断(实测 turbo 对根 package.json 与锁文件里 TypeScript 版本的变化会判"没有包受影响")。普通包改动会顺带检查 spec 等五个大包,冷启动约多 70 秒;改了 spec 的提交(约三分之一)仍接近全量。本 PR 自己的 run:该步 2 秒、作业 56 秒(只改 workflow,选中零个包);本地 4 核冷跑一个叶子包的改动约 79–152 秒。回滚只需还原这一个步骤。另一个发现:lint.yml 已贴近 GitHub 的 workflow 文件大小上限(main 505,924 字节,513,738 字节时整个文件失效、两个必需检查都不报),席位已为此立卡。

席位意见:建议批准。队列的耗时主因被实测查清,方案不删不减任何检查(全量仍在 main 与每小时跑),合并队列上预计从约 8 分钟降到 1–3 分钟,落地后由席位读三次队列与三次 PR 实测回填。

你要做的:在 PR 上 Approve;席位随后翻正式并入队。

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

Projects

None yet

2 participants