Skip to content

Commit 963e504

Browse files
ci(ui-merge): the seven @objectstack packages build once per run and the ui jobs replay them; every ui cache save is bounded (#25963)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6Jao5KRZX2oEdTBVQdTu3b2xR7)_ Fixes #25869 Part of #25677 (post-cutover CI speed, item 3; rows F1, F2, F11 and F13). Clause-②: no Metadata-protocol: none ## What changed `.github/workflows/ci.yml`, the ui jobs only: - **New job `ui-deps` (`Build UI Deps`).** It builds the seven @objectstack packages objectui consumes ONCE per run, with the same `turbo run build` command the ui jobs use, then prunes `.turbo/cache` to that build's tasks (`scripts/ci/prune-turbo-cache.mjs`) and uploads those entries as the artifact `ui-deps-turbo-cache` (36 tasks, ~27 MB, stored uncompressed because the entries are zstd already, kept one day). Its `if:` is the union of the four ui jobs' conditions. The build carries Build Core's four `TURBO_*` lines verbatim, followed by `assert-turbo-remote-cache.mjs`, so a queue or `main` run replays a closure that was already built from the remote cache. A `pull_request` run still gets no remote credentials. - **The four ui jobs** (`type-check-ui`, `test-ui`, `lint-ui`, `build-e2e-ui`) now `need` `ui-deps` and download the artifact into `.turbo/cache` before their unchanged `Build the @objectstack packages objectui consumes` step. That step then replays every task, measured locally at 1.2 s for 36 of 36 cached. The download is `continue-on-error`. Every job's `if:` is byte-identical and starts with `!cancelled()`, so a red, cancelled or skipped `ui-deps` never skips a ui check: with no artifact, the job builds cold exactly as before. Nothing the ui jobs test changed. - **Why cache entries and not `dist/`.** `type-check-ui` and `lint-ui` run further turbo tasks that depend on `^build`, and only a cache hit stops turbo rebuilding the seven packages there. Turbo still hashes every task, so an entry that does not match the tree gets rebuilt rather than trusted. `turbo run build --dry=json` gives the same 36 task hashes with and without the env the jobs differ in (`TURBO_RUN_SUMMARY`, `NODE_OPTIONS`, `TURBO_CACHE`). - **F1 + F11: every ui cache save is bounded.** Each one is a main-phase `actions/cache/save` with `timeout-minutes: 5` and `continue-on-error: true`, placed after the job's last checking step and guarded on its restore's `cache-hit`. No ui job uses the combined `actions/cache` any more. The pnpm store is restored by every ui job and saved by `ui-deps` alone. `test-ui` and `build-e2e-ui` drop their own turbo seed, since the seven-package build is their only turbo run. `type-check-ui` and `lint-ui` keep theirs. - **F2: decided, the root jobs do not run `ui/scripts/report-turbo-cache.mjs`.** The prune before each seed save is objectstack's `scripts/ci/prune-turbo-cache.mjs` (the same summary-driven prune, fed by job-level `TURBO_RUN_SUMMARY`). The hit/miss report is `assert-turbo-remote-cache.mjs`'s step-summary table on `Build UI Deps`. The script retires with its last callers, the nested `ui/.github/workflows/` (#25712); `report-turbo-cache.test.ts`'s retirement note now says so. - **F13: the ceilings are derived and pinned** in a comment above `lint-ui`, using objectui's rule (smallest round number that is ≥ 3× the slowest successful run and ≥ that run + 15 min) over every successful job since the cutover. Lint UI: n=42, max 558 s, which gives **30** (unchanged). Build & E2E UI: n=51, max 457 s, which gives **25** (was 45; nothing backed the extra 15). `ui-deps` is 25 by the same rule. - Build Core's remote-cache comment lists `Build UI Deps` among the carriers. `ui/scripts/__tests__/turbo-cache-save-bound.test.ts` is restored, retargeted to the root jobs by path through `helpers/root-ci.ts`. It brings back the six pins #25879 retired, now over all five jobs: main-phase saves, bounded and non-fatal, ceiling at least twice the bound, saves after the last command, the same key with a `cache-hit` guard, and ceilings not raised. It adds the hand-off pins: `needs: ui-deps` with an `!cancelled()` `if:`, a fail-open download before the build step, and a build command identical to `ui-deps`'s. ⛔ Not touched: the lint-ui gate steps #25902 adds. That PR edits lines next to two of this PR's `lint-ui` hunks (the `Install dependencies` step and the steps before `Save Turbo cache`). Whichever lands second takes a small textual merge. Re-derive Lint UI's ceiling when #25902's gates land, because they add a full ui-package build to that job. ## Measured before (the card's "before") The 51 CI runs that scheduled ui jobs between the cutover (#25825) and 2026-10-26T05:30Z, step and job times taken from the Actions jobs endpoint: | | runs | seven-package build per ui job | runner-min on that step | |---|---|---|---| | warm: every ui job replays from its own seed | 24 | 0–2 s | 0.3 per run | | cold: every ui job builds | 18 full-lane + 9 two-lane | 86–288 s, median 201 s | 33–77 per full-lane run, 7–8.5 per two-lane run | | **total** | **51** | | **1191** | A cold seed is the common case. The seeds are saved per job on `main` pushes, so a PR or queue entry whose closure differs from the newest seed's misses in all twenty jobs at once. Examples: queue entry 40271867662 (#25940, ui test files only) 70.2 runner-min; PR run 40266487909 74.0; push 40268844566 62.0. Expected after (ui-deps builds once): a cold full-lane run spends one build (~4 min) instead of ~70 runner-min. A warm run gains ~1.5 runner-min (one more job's setup and twenty ~27 MB downloads). On this window's mix that is roughly −900 of 1191 runner-min. **The cost is wall clock:** a ui job now starts after `ui-deps`, about one job-setup plus one upload (~1–1.5 min) later than it used to start its own build. On a warm run that is pure wall-clock loss; on a cold run it largely offsets the ~3–4 min build each job no longer runs. This PR's own CI run (all lanes, since `ci.yml` is in the `ui` filter) gives the first after-reading; the card gets the per-job before/after table once comparable PRs have run. Option (b) of the card (a `pull_request` run reads the signed remote cache) is not taken. Turbo reads the remote with the same token that writes it, and Build Core's "Turbo remote cache (#21186)" comment withholds both secrets from PR code on purpose. Giving PRs a read would reverse that ruling, which is the maintainer's call and not a seat's. ## Verification | Run | Result | |---|---| | `ui/` `pnpm exec vitest run scripts/__tests__/turbo-cache-save-bound.test.ts` at this head | 44 passed | | the same file against `main`'s `ci.yml` (`17c5e68b`), restored with `git checkout HEAD --` | 38 failed, 6 passed (exit 1) | | local seven-package build into a fresh cache dir, `--summarize` | 36 tasks, 0 cached, 4m36s; cache dir 27 MB | | the same build after deleting `packages/spec/dist`, from that cache dir | 36 of 36 cached, `FULL TURBO`, 1.2 s, `dist/` restored | | `turbo run build --dry=json` with and without `TURBO_RUN_SUMMARY` / `NODE_OPTIONS` / `TURBO_CACHE` | 36 identical task hashes | Derived gates (`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, 3 paths, at `cd0ae004`): 63 families. 62 exit 0. `pnpm check:dual-build-cjs-loads` exits 3, PREREQUISITE NOT MET: it reads the `dist/` of every package, and this box has built only the ui closure. CI's Lint & Repo Gates runs it on a full build. `--ran` reconciles: "63 derived famil(ies) accounted for — 62 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)". The slowest, on a shared box: `pnpm check:type-check-debt` (410s), `pnpm check:pm-dispatch-gates` (346s), `pnpm check:query-options-erasure` (286s), `node scripts/check-comment-mask-corpus.mjs` (161s). Push-environment battery (`GITHUB_ACTIONS=true GITHUB_EVENT_NAME=push node scripts/pm/check-dispatch-gates.mjs`, both tiers): it runs after this body was written, and its verdict line goes in the card report. ## Acceptance notes - `ui/scripts/report-turbo-cache.mjs` has no root caller now. It retires with its last callers, the nested workflows (#25712, 2d-3), not here: those workflows still call it, and `render-budget-comment.test.ts` and `turbo-lint-inputs.test.ts` still read it. - `type-check-ui`'s 40-minute ceiling is not derived here (F13 named Lint UI and Build & E2E UI). Over the same population its max is 1194 s; objectui's rule would give 60. It is pinned at 40 as "does not rise". ## 维护者速读(草稿) **改了什么:** ui 的四个 CI 作业以前各自构建同样的 7 个 @objectstack 包。现在新增一个 `Build UI Deps` 作业,每次 CI 只构建一次,再把 turbo 缓存条目交给这四个作业,它们约 1 秒就能回放出构建结果。同时,所有 ui 缓存保存都加了时限,保存失败也不会让作业变红。Build & E2E UI 的超时按实测推导,从 45 分钟降到 25 分钟。 **为什么改:** 合并后的 51 次运行里,有 27 次是每个 ui 作业各自冷构建一遍(全量运行每次 33–77 runner-min),这一步合计 1191 runner-min。缓存保存没有时限,上传一旦卡住,已经通过的检查会被取消,合并队列会把它当成失败(objectui#6577 出过这个事故)。 **风险与代价(含回滚):** ui 作业要等 `Build UI Deps` 跑完才开始,墙钟大约多 1–1.5 分钟。冷构建时这部分基本被省下的构建时间抵消,热缓存时就是净增。下载失败时作业会自己冷构建,和现在一样,所以不会因此跳过或变红任何必需检查。回滚就是 revert 这个 PR。 **席位意见:** **你要做的:** 这是 Tier H(`.github/workflows/ci.yml`),需要 os-zhuang 或 hotlong 批准。 --- _Generated by [Claude Code](https://claude.ai/code/session_01XpG2CKzNjLujzcMmSxKCHy)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ef1fc78 commit 963e504

3 files changed

Lines changed: 573 additions & 59 deletions

File tree

0 commit comments

Comments
 (0)