Repository navigation
Commit 963e504
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
- .github/workflows
- ui/scripts/__tests__
0 commit comments