Repository navigation
fix(ci): the Lint turbo cache replays, and every turbo cache restores only its own job's (objectui#12276) - #12290
Conversation
…ository (objectui#12276) `//#lint:root` declared no inputs, so turbo hashed every tracked file for it, and every package's `lint` dependsOn it: one untracked docs/ file moved all 46 package lint hashes and the restored cache could replay nothing. Its inputs are now the extensions ESLint lints at that scope minus the four trees the `lint:root` script ignores, which is what that script walks. Claude-Session: https://claude.ai/code/session_011oKFDZMRoPGDbStvxQa172 Co-authored-by: Claude <noreply@anthropic.com>
…hes under explicit globs (objectui#12276) Measured: with explicit inputs turbo 2.10.9 hashes git-ignored files too (64008 inputs, 63598 of them under the root node_modules). Claude-Session: https://claude.ai/code/session_011oKFDZMRoPGDbStvxQa172 Co-authored-by: Claude <noreply@anthropic.com>
…ned and reported (objectui#12276) `Lint`, the `docs` job, the release job and `Bundle Analysis` restored with a bare `turbo-<os>-` fallback, and three of them saved under one shared `turbo-<os>-<sha>` key, so each restored whichever job's cache reached the ref first and saved it again. They now take the `type-check` job's shape (objectui#12222): a per-job prefix and no other restore key, pull-request and queue runs restore and never save, the saving runs prune to what they used, and every run reports what the cache replayed and which key it restored. Pins: `//#lint:root`'s inputs against ESLint's own walk at that scope, and the five turbo caches' keys, save events, prune and report as a population. Claude-Session: https://claude.ai/code/session_011oKFDZMRoPGDbStvxQa172 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oKFDZMRoPGDbStvxQa172 Co-authored-by: Claude <noreply@anthropic.com>
…d its contract pins scope to their steps (objectui#12276) `render-budget-comment.test.ts` holds that every script the job runs is in its `paths:` trigger closure, counts the job's `!cancelled()` steps, and reads the renderer's env contract; the report step added one script, one condition and one `steps.*` env var outside the render step. Claude-Session: https://claude.ai/code/session_011oKFDZMRoPGDbStvxQa172 Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
维护者速读(终稿)席位 改了什么:
为什么改:原来任何一个文件变化都会让 46 个包的 lint 全部重跑,缓存从未命中过(实测:一个 docs 文件就改动 46/46 个哈希)。docs job 本次还读到一份 2.9 GB 用不上的旧缓存。 风险与代价(含回滚):
席位意见:可合。CI 全绿,本 PR 自己的运行已显示根任务只哈希 410 个文件。dev 的测量推翻了「push 运行多半被取消、存不上缓存」的假设(20 次里 14 次成功),所以没加定时任务。合并后由本席核对验收:接下来三个 PR 的 你要做的:批准本 PR( Generated by Claude Code |
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Fixes #12276
Clause-②: no
The
Lintjob's turbo cache could not replay anything, and four jobs could restore each other's caches. This PR changes two things:turbo.json://#lint:rootnow declares its owninputs. They are the extensions ESLint lints at that scope, minus the four trees thelint:rootscript passes as--ignore-pattern, plus!**/node_modules/**. Before this, the task had no inputs, so turbo hashed every file in the repository for it. Every package'slintdepends on it, so any change anywhere moved every lint hash.Lint,Build Docs,Changeset Release,Bundle Analysis) now copy the shape of thetype-checkjob (objectui#12222). Each job gets its own key prefix (lint,docs,release,perf) and restores by that prefix only. Pull-request and queue runs restore but never save. The runs that do save prune the cache to what they used, and every run posts oneTurbo cachenotice saying what it replayed and which key it restored.Changeset: an empty-frontmatter declaration. No published package's files change, and
check-changeset-presence.mjsagrees: "no changeset is owed".Before reading (taken before any change)
Run linterin theLintjob, last 20pull_requestruns where the step ran (window 2026-10-12T15:02Z to 2026-10-13T00:57Z, Actions jobs API, stepcompleted_atminusstarted_at): median 276.5 s, min 179 s, max 296 s. Measuring the after value onmainis the PM's job once this lands.Restore Turbo Cachein the same 20 runs: median 2 s, but 8 of 20 took between 12 and 23 s. The latestmainpush run took 29 s (run 38329504101). The lint cache itself holds only logs, so a restore that slow means some other job's build cache was pulled in. Job logs answer 403 to this container, so the restored key cannot be read directly. The new notice reports it on every run.What was measured on
origin/mainbefore the change (74964ad6)H1 (confirmed, re-measured).
turbo run lint --dry=json://#lint:roothashed 8652 input files. One untracked file underdocs/moved 46 of 46 packagelinthashes.H3 (confirmed). The four bare-fallback restores and their saves sit where the dispatch said (lint.yml, the ci.yml
docsjob, changeset-release.yml, performance-budget.yml).Lint,Bundle Analysisand the release job also saved under the sameturbo-OS-SHAkey, and the bareturbo-OS-fallback also matched thedocsandtype-checkprefixes.H5 (falsified). Most
mainpush runs do complete:Lint: 14 of the 20 newestmainpush runs succeeded and 6 were cancelled (window 2026-10-12T13:39Z to 2026-10-13T00:53Z).Bundle Analysis: 16 succeeded, 3 were cancelled and 1 was still running.scheduleand on publish pushes, all onmain.So a push-only save does warm the cache. No
schedulewas added tolint.yml.A turbo mechanism found while doing this
With explicit
inputs, turbo 2.10.9 also hashes git-ignored files, which$TURBO_DEFAULT$never does. The first version of the inputs had 64008 inputs, 63598 of them under the rootnode_modules. A git-ignoreddist/probe file at the root was hashed too. That is why!**/node_modules/**is in the list. The new pin's docblock records the remaining limit: a local git-ignored output dir with linted extensions outside the four trees would also be hashed, and none exists whenLintrunspnpm lint.Proof that the inputs are right (H2, done three ways and then more)
The linted set comes from ESLint itself, not from the card:
eslint . --ignore-pattern ...(thelint:rootscript) with--format jsonreported 407 files (34 undere2e/, 23 undereslint-rules/, 335 underscripts/, 15 root files).turbo run lint --dry=jsonafter the change://#lint:roothas 410 inputs:public/mockServiceWorker.js, which ESLint ignores through**/public;package.jsonandturbo.json, which turbo adds to every root task by itself.Probes, each compared with the after baseline taken at
a3ea39d0(the sameturbo.jsonas the head):linthashes moved//#lint:roothashdocs/file.changeset/filecontent/filepackages/core/src/scripts/module (positive control)scripts/invoked-as.mjs(positive control)e2e/speceslint.config.jspackage.jsonReplay, run for real (
TURBO_RUN_SUMMARY=true turbo run lint --filter=@object-ui/types, three runs under the verify lock):Cached: 0 cached, 2 total;1 cached, 2 total;docs/file: still1 cached, 2 total.//#lint:rootstays uncached and re-runs every time. Its stable hash is what lets the package task replay. The report script's notice on that summary:lint 1/1 replayed · uncached //#lint:root (410 input files) · cache directory absent · restored nothing (cold). The script took 87 ms, which is where each new step'stimeout-minutes: 1comes from. "Cache directory absent" is a worktree effect: in a linked git worktree, turbo writes its cache under the main checkout's.turbo/cache. CI checkouts are not worktrees.What a pull request can expect afterwards (a reading of the last 200 merges up to
74964ad6, not a live count)://#lint:rootinput (scripts/,e2e/, a root.ts/.js/.mjs, the rootpackage.jsonorturbo.json). Those still re-lint all 46 packages.eslint.config.js,eslint-rules/or the lockfile.Pins (new and changed)
turbo-lint-inputs.test.ts, new block "//#lint:roothashes the files it lints". It checks that://#lint:rootdeclares inputs and$TURBO_DEFAULT$is not one of them;!inputs equal thelint:rootscript's--ignore-patternvalues plus**/node_modules/**(read frompackage.json, not copied);ESLint#isPathIgnoredthroughcheck-lint-rule-coverage.mjs'swalkedFiles);report-turbo-cache.test.ts, new population block covering all five.turbo/cachejobs across.github/workflows/. It checks:turbo-OS-TAG-SHA, with exactly one restore key, its ownturbo-OS-TAG-;!cancelled()andRESTORED_CACHE_KEYfrom the restore step;TURBO_RUN_SUMMARYset on a step between the restore and the report.turbo-task-guard-coverage.test.ts: docblock only.//#lint:roothas inputs now and is still on the uncached side of the partition.render-budget-comment.test.ts: it pins that every scriptBundle Analysisruns is in itspaths:trigger closure, counts the job's!cancelled()steps, and reads the renderer's env contract. The report step added one script, one condition and onesteps.*env var, so:scripts/report-turbo-cache.mjsjoins the perfpaths:lists andTRIGGER_CLOSURE;This pin was not on the dispatch's H6 list. The full
scripts/__tests__run caught it.content/docs/guide/ci-cd-pipeline.md:ci-cd-pipeline-doc.test.tspairs every first-party command a job runs with its doc section, so the report script is named in thedocsrow and in the Lint, Performance Budget and Changeset Release sections.Ablations (one-time; mutation through
ablation-replace.mjs, restore proven by blob hash equal to HEAD and an emptygit diff HEAD)//#lint:rootinputskey renamed awayturbo-lint-inputs.test.ts!**/node_modules/**renamedturbo-OS-restore key re-added in lint.ymlgithub.event_name == 'push'Build SitelosesTURBO_RUN_SUMMARYturbo-OS-SHAlint-The first attempts at A2 through A5 were refused by the tool: the replacement was a substring of the anchor, or the anchor was ambiguous, so nothing was measured. They were re-run with valid anchors as A2c to A5b.
Local verification at
741484ae(the head)Every exit code was captured before any pipe:
pnpm exec vitest run --maxWorkers=2 --shard=1/2 scripts/__tests__Test Files 93 passed, 1 skipped (94),Tests 3228 passed, 1 skippedpnpm exec vitest run --maxWorkers=2 --shard=2/2 scripts/__tests__Test Files 94 passed, 1 skipped (95),Tests 2403 passed, 1 skippedpnpm type-check:scriptsVERDICT command-exit 0;--listFilesOnlylists all four changed test filespnpm exec eslinton the four changed test filespnpm check:control-bytesOK (scanned 8568 tracked text file(s); skipped 85 binary)pnpm check:new-line-citations0 new citation(s), enforcement report-only -> exit 0pnpm check:action-ref-convention109 action reference(s) in 29 workflow file(s) ... 0 off-conventionnode scripts/check-pre-install-import-graph.mjs --self-test, then the real run16 cases pass;39 pre-install step(s) in 19 job(s) ... every non-relative leaf a node builtinnode scripts/check-shell-escape-residue.mjsOK (5/5 root(s) resolved ...)node scripts/check-doc-links.mjsLinks are valid across 17 scan roots.node scripts/check-doc-fence-languages.mjs --self-test, then the real run26 cases pass;every TypeScript block in 227 document(s) is fenced ...node scripts/check-changeset-presence.mjsno changeset is owednode scripts/check-changeset-no-major.mjsNo changeset declares a major bump.node scripts/check-governed-queue-guard.mjs --test(all 11 paths)NOT GOVERNED. objectui#12255 lands the workflow row in parallel; the seat treats this PR as Tier HNot run locally, left to CI:
pnpm lint;Lint,Build Docsthroughturbo.jsonandci.ymlin its pathspec, andBundle Analysisthrough its own file; the release job runs only onmain).Acceptance notes (observations, not filed)
package.json/turbo.jsoncoupling remains. Turbo adds both to every root task's inputs, and every package'slintdepends on//#lint:root. So an edit to either, or to anythinglint:rootlints, still re-lints all 46 packages (28 of the last 200 merges). Removing that would mean changing thelinttask'sdependsOn, and the card fences that task off.Lintonpushreplays withoutTURBO_FORCE. Thetype-checkfull battery forces; this job does not. It trusts the inputs that the derived guard (turbo-lint-inputs.test.ts) pins. I considered forcing it and did not, because the card does not ask for it.//#type-check:e2einpute2e/**has the same git-ignored-files behaviour described above, but only on a local tree. CI checks out fresh.scripts/report-turbo-cache.mjsheader still describes only theci.ymltype-check use. It is still true and now incomplete. It was left alone to keep the surface small; the next edit of that script can carry it.changeset-release.ymlin parallel. This diff stays off them (its nearest change is theBuild packagesstep's newenv:, below the comment block that card edits).维护者速读(草稿)
改了什么: 四个 CI 任务(Lint、Build Docs、Changeset Release、Bundle Analysis)的 turbo 缓存改为各用各的 key 前缀,只由
main上的运行写入,写入前裁剪到本次实际用到的条目,每次运行发一条注解说明复用了什么、恢复的是哪个 key。另外turbo.json里根任务//#lint:root只按它真正检查的文件计算 hash。为什么改: 以前仓库里任何一个文件变动都会让 46 个包的 lint 缓存全部失效,每个 PR 的 Lint 都要全量跑(实测中位数约 4.6 分钟);而且几个任务会互相恢复对方的大缓存,白白花时间、占缓存配额。
风险与代价(含回滚): 落地后第一次运行是冷缓存。PR 不再保存缓存,同一个 PR 再次推送时也从
main读。改动scripts/、e2e/、根package.json的 PR 仍会全量 lint(过去 200 次合并里约 14%)。不改变任何门禁的判定,只影响耗时。回滚:直接 revert 本 PR。席位意见:
你要做的: 本 PR 改了
.github/workflows/**,按「两仓加治理行」作为需人工批准的受管改动保持 draft;请审阅后批准,其余由席位落地。Generated by Claude Code