Skip to content

Commit 4ddf6df

Browse files
ci(ui): regenerate the SDUI manifest in Build & E2E UI and compare it with the committed bytes (#26137 PR A) (#26315)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoAqVfFyhsxKuFcEP9YTBj25)_ Part of #26137 (PR A of two: the CI comparison; PR B retires the per-package records once this is green on `main`). Tier H (`.github/workflows/**`). Part of objectstack#25677. Clause-②: no Metadata-protocol: none ## 维护者速读(草稿) - **改了什么:** `Build & E2E UI` 多了一步:用 `node scripts/gen-sdui-manifest-node.mjs` 从本树重新生成 `sdui.manifest.json`,再和提交的文件逐字节比较,不一致就红,并打印差异和修复命令。`ci.yml` 的 `ui` 过滤器加了 turbo 的四个全局依赖文件,以及清单自己的四个文件。原来 lint 里按源码摘要判新鲜度的检查保留不动。 - **为什么改:** 摘要只读各 registry 包的 `src/`,看不见三类变化:解析器 `@objectstack/sdui-parser` 的改动、registry 运行时引用的 `@objectstack` 包的改动、两个排队 PR 合起来的效果。实测:在 `main` 上改 `manifestFromConfigs` 一行,现有检查仍是绿的,而重新生成的清单差了 87 行。现在 PR 和合并队列的合并树上都会重新生成并比较。 - **哪些原来能合的现在会被拒:** 只有清单确实过期的 PR。今天 `main` 重新生成与提交的字节完全一致。 - **代价:** 每次跑 ui 的 run 多约 3.6 分钟 runner 时间(27 个 `@object-ui` 包冷构建 3m25s,枚举 8s;36 个 `@objectstack` 任务从 Build UI Deps 回放)。今天 22 次绿 run 里 ui 尾部都比它长,所以墙钟时间增加 0。`Build & E2E UI` 超时按规则重算为 35 分钟。 - **风险与回滚:** 只改 CI 和文档,不发布任何包。回滚 = revert 本 PR。 - **你要做的:** Tier H,需要 GOVERNED_APPROVERS 审批。 ## 席位核对 (left for the dispatch seat) ## Why `check-sdui-manifest` (lint, every PR) judges the committed `sdui.manifest.json` fresh when each registry package's `src/` digest equals its one-line record under `scripts/sdui-manifest.sources/` (#26051, #26120). That digest is blind to everything outside those `src/` trees that moves the artefact: - **the serialiser itself**, `@objectstack/sdui-parser`, which the generator imports from `packages/sdui-parser/dist`. Since the lockstep gate retired (#26221) nothing ties a parser edit to the records, and after 2g PR 2 deletes `ui/packages/sdui-parser` the records stop reaching any parser at all (#26137 comment 6420796812); - **the `@objectstack` packages the registry imports at runtime** (`spec`, `client`, `core`, `formula`, `types`), and any `package.json` or lockfile change; - **two queued PRs** that each leave the artefact unchanged and change it together, the residual the maintainer accepted for #26120. ### Before: the gap on `main`, measured On `main` 81bba6c, in `packages/sdui-parser/src/index.ts` `manifestFromConfigs`, `isContainer: c.isContainer` → `isContainer: c.isContainer ?? false`: - `node scripts/check-sdui-manifest.mjs`: **exit 0**, "present, intact … each package's digest current in its record"; - `node scripts/gen-sdui-manifest-node.mjs`: `sdui.manifest.json` **1 file changed, 87 insertions(+)**. So a stale manifest passes today. With this PR, the same edit reds `Build & E2E UI` on the PR, because `packages/sdui-parser/**` is in `ui_consumers` and the step serialises through the parser the job just built. ## What changes ### `ci.yml`, `Build & E2E UI`: the comparison A new step, **"The committed SDUI manifest is the one this tree generates"**, after "Build the @objectstack packages objectui consumes" and before the Console build: ```bash node scripts/gen-sdui-manifest-node.mjs if ! git diff --quiet -- sdui.manifest.json; then echo "::error file=sdui.manifest.json::…Run 'node scripts/gen-sdui-manifest-node.mjs' and commit what it writes." git diff --stat -- sdui.manifest.json git diff -- sdui.manifest.json | head -n 200 exit 1 fi ``` - **What it reuses:** the generator's turbo run is 63 build tasks. 36 are exactly the set `Build UI Deps` builds and hands over, and they replay from the downloaded `ui-deps-turbo-cache`. The other 27 are the `@object-ui` registry closure. - **What it adds:** on 4 cores, the 27 build in 3m25s cold and the enumeration takes 8s; with nothing moved the whole run takes 6s. The output on `main` is byte-identical to the committed file (sha256 `1f6e1ccb925a…`, 106 components), and two runs agree. - **Why this job:** it is required already, already replays the 36, and is the shortest ui job (1.9–2.9 min across 22 green runs on 2026-10-26: 14 `pull_request`, 6 `merge_group`, 2 `push`). The ui lane's tail after `Build UI Deps` was 8.4–14.2 min in the same runs, so the added wall time measured 0 in all of them. Lint UI builds the 27 too, but it runs on the `ui` output only, so a PR that changes only the parser or one of the five packages would skip it (#26137 comment 6424273287). - **What it compares:** only the artefact. The records the generator rewrites beside it stay with `check-sdui-manifest` until PR B. - **The Console build is unaffected:** `ui/apps/console/vite.config.ts` aliases every workspace package to its `src/`, so the dist this step builds does not reach it. ### `ci.yml`, the `ui` filter | Added | Why | |---|---| | `tsconfig.json`, `tsconfig.base.json`, `tsconfig.test.base.json`, `tsup.config.ts` | `turbo.json`'s `globalDependencies`. Every build task hashes them, so they move what the ui jobs build. The digest never saw them either. | | `sdui.manifest.json`, `scripts/sdui-manifest.record.json`, `scripts/gen-sdui-manifest-node.mjs`, `scripts/check-sdui-manifest.mjs` | The artefact, its record, its producer and its offline gate. A hand edit of the artefact, or a producer edit, now runs the comparison. | The registry's runtime closure, walked over `dependencies` and `peerDependencies` from the 17 seeds, is the 27 ui packages plus `spec`, `client`, `core`, `formula` and `types`. All of them were under `ui/**` or `ui_consumers` already. Cost of the additions: in the 30 days to 2026-10-26, 2 merged PRs moved one of the eight paths and no other ui path (#26120, #25833); each would have run the ui lane once more. ### `ci.yml`, the ceiling `Build & E2E UI` is re-derived by the rule in the comment above Lint UI. The slowest successful run was 457 s; adding 213 s gives 670 s (11.2 min), so `max(33.5, 26.2)` ⇒ **35**, up from 25. The pin in `ui/scripts/__tests__/turbo-cache-save-bound.test.ts` moves with it. ### Prose - **`lint.yml`:** the `check-sdui-manifest` step comment says where the regeneration now runs. The stale reference to the retired lockstep neighbour goes. - **`scripts/check-sdui-manifest.mjs` header:** "What this deliberately does NOT do" and check 3's "What is NOT forced" now name the CI step that does it. - **`docs/releases-maintenance.md`:** one paragraph on the CI comparison. ### Widening or narrowing **This is a widening.** It adds a check, and no case that the current check refuses passes now: the current check stays on, unchanged. The step goes red only when the committed manifest differs from this tree's regeneration. That is exactly a stale manifest, so nothing an author can write is refused that is not stale. ## What PR B does, after this is green on `main` It retires check 3 in-tree (the per-package digests) and `scripts/sdui-manifest.sources/`. It also updates their readers: - `scripts/gen-sdui-manifest-node.mjs`; - `scripts/check-changeset-no-major.mjs` (L1368–1371, L4879, L4966); - `docs/releases-maintenance.md`; - `cut-rc.yml` and `release.yml` prose. After that, PRs conflict only when the manifest's bytes really change. Whichever of PR B and 2g PR 2 lands second rebases on `scripts/sdui-manifest.sources/sdui-parser.json`. ## Verification Head `d36dc4280`, on `main` `ff918954e`. 5 files, +79 / −23, one commit. Tier H: `check-governed-merges --test` exits 3 for `.github/workflows/ci.yml` and `.github/workflows/lint.yml` and 0 for the other three paths. - **The regeneration on this head:** `main` here already carries #26211's parser. On `d36dc4280`, `node scripts/gen-sdui-manifest-node.mjs` wrote **byte-identical** bytes: sha256 `1f6e1ccb925a…`, 106 components, `git status` clean, no record rewritten. So the new step should be green on this PR. With main moved, that run rebuilt 62 of 63 tasks locally in 7m25s; in CI, `Build UI Deps` supplies 36 of them. - **The gap, before:** see "Before" above (main 81bba6c: gate exit 0, regeneration +87 lines). - **`check-sdui-manifest`:** `--self-test` 29 cases behave; the production run is green. - **Tests that read the job:** `ui/scripts/__tests__/turbo-cache-save-bound.test.ts` and `ensure-chromium-ready.test.ts` pass 52/52, with the 35-minute pin. - **Workflows parse:** `ci.yml` and `lint.yml` both parse (14 jobs each). `build-e2e-ui` reads `timeout-minutes: 35`, and the new step sits between "Build the @objectstack packages objectui consumes" and "Build the Console for E2E". - **Gate sweep:** `node scripts/pm/dispatch-gates.mjs --commands` derives 79 gates. They ran on `468450e19`, this commit before its rebase from `4f7e0414a` to `ff918954e`; nothing on `main` between those touched the five files. 78 exited 0. `check:dual-build-cjs-loads` exited 3 (prerequisite: no workspace `dist`), then exited 0 after `pnpm build`. `--ran` reconciles 79 derived, 79 run, 0 NOT-MEASURED, 0 UNRUN. - **Push battery** (`GITHUB_ACTIONS=true GITHUB_EVENT_NAME=push node scripts/pm/check-dispatch-gates.mjs`) on `468450e19`: 2164 cases pass, 0 fail, EXIT 0 (1586 s). - **CI:** pending on `d36dc4280`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01A6SEitNngAMugGbuJHWsDS Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8b39b77 commit 4ddf6df

5 files changed

Lines changed: 79 additions & 23 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,21 @@ jobs:
472472
- '.github/workflows/**'
473473
- '.github/prompts/**'
474474
- 'skills/objectui/**'
475+
# turbo.json's `globalDependencies`: every build task hashes
476+
# them, so they move what the ui jobs build, the SDUI manifest's
477+
# regeneration included (#26137).
478+
- 'tsconfig.json'
479+
- 'tsconfig.base.json'
480+
- 'tsconfig.test.base.json'
481+
- 'tsup.config.ts'
482+
# The committed SDUI manifest, its record, its producer and its
483+
# offline gate. Build & E2E UI regenerates the manifest and
484+
# compares it with the committed bytes (#26137), so a diff that
485+
# moves only one of these still runs that comparison.
486+
- 'sdui.manifest.json'
487+
- 'scripts/sdui-manifest.record.json'
488+
- 'scripts/gen-sdui-manifest-node.mjs'
489+
- 'scripts/check-sdui-manifest.mjs'
475490
# The seven packages objectui installs from this tree (the set its
476491
# `.objectstack-pin` used to pack). A change here compiles and builds
477492
# the Console in the same PR — the check the retired Spec Shape Gate
@@ -3734,6 +3749,10 @@ jobs:
37343749
# - Build & E2E UI: n=51, min 106 s, median 170 s, p95 423 s, max 457 s
37353750
# (7.6 min) => max(22.9, 22.6) => 25, down from objectui's 30 plus the
37363751
# 15 the port added for the in-job build, which no derivation backed.
3752+
# Re-derived for the SDUI manifest regeneration (objectstack#26137),
3753+
# which adds 213 s cold (the 27 @object-ui builds plus the enumeration,
3754+
# measured on main 81bba6c3f): 457 + 213 = 670 s (11.2 min)
3755+
# => max(33.5, 26.2) => 35.
37373756
# Re-derive when a job's work changes; ui/scripts/__tests__/
37383757
# turbo-cache-save-bound.test.ts holds both values.
37393758
lint-ui:
@@ -4063,7 +4082,7 @@ jobs:
40634082
if: ${{ !cancelled() && needs.filter.outputs.ui_tree != 'false' && (needs.filter.outputs.ui != 'false' || needs.filter.outputs.ui_consumers != 'false') }}
40644083
runs-on: ubuntu-latest
40654084
# Derived above Lint UI.
4066-
timeout-minutes: 25
4085+
timeout-minutes: 35
40674086
permissions:
40684087
contents: read
40694088
env:
@@ -4122,6 +4141,30 @@ jobs:
41224141
--filter=@objectstack/sdui-parser... \
41234142
--filter=@objectstack/types...
41244143
4144+
# The repo-root SDUI manifest, regenerated from this tree and compared
4145+
# with the committed bytes (#26137). The generator builds the registry
4146+
# with turbo: the 36 tasks of the step above replay, and the 27
4147+
# @object-ui packages build (3m25s cold, measured on 4 cores, on main
4148+
# 81bba6c3f), then it enumerates the registry under plain Node (~8 s; ~6 s
4149+
# for the whole run when nothing moved). It serialises through the
4150+
# @objectstack/sdui-parser the step above built, so a parser edit moves
4151+
# this verdict too, which no committed digest does. On merge_group it runs
4152+
# on the queue's combined tree, the one run that sees two entries' effect
4153+
# on the manifest together. Only the artefact is compared: the records the
4154+
# generator rewrites beside it are held by `check-sdui-manifest` in
4155+
# lint.yml. The Console build below reads the registry through its src/
4156+
# aliases, so the dist this step builds does not reach it.
4157+
- name: The committed SDUI manifest is the one this tree generates
4158+
run: |
4159+
node scripts/gen-sdui-manifest-node.mjs
4160+
if ! git diff --quiet -- sdui.manifest.json; then
4161+
echo "::error file=sdui.manifest.json::sdui.manifest.json is not the manifest this tree generates. Run 'node scripts/gen-sdui-manifest-node.mjs' and commit what it writes."
4162+
git diff --stat -- sdui.manifest.json
4163+
git diff -- sdui.manifest.json | head -n 200
4164+
exit 1
4165+
fi
4166+
echo "sdui.manifest.json is byte-identical to this tree's regeneration."
4167+
41254168
- name: Build the Console for E2E
41264169
env:
41274170
VITE_BASE_PATH: /console/

‎.github/workflows/lint.yml‎

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -966,15 +966,13 @@ jobs:
966966
# repo-root `sdui.manifest.json` is absent or unreadable, so nothing in
967967
# the production path ever says "the gate lost its whitelist" — this step
968968
# is the thing that notices. Offline record checks only (existence, shape,
969-
# sha256 vs the provenance record, record pin vs `.objectui-sha`): the
970-
# regenerate-and-compare half runs where a regeneration can (the Node
971-
# generator reads objectui's BUILT tree at the pin, which only
972-
# `pnpm objectui:build` materialises — see
973-
# scripts/gen-sdui-manifest-node.mjs), and a pin bump goes red HERE until
974-
# it has been re-run — the same moment the lockstep step above forces the
975-
# parser parity re-verification, with an objectui checkout guaranteed in
976-
# hand. Unfiltered for the same reason as its neighbour: filtering to the
977-
# artefact would go dormant on exactly the PR that moves the pin.
969+
# sha256 vs the provenance record, and with no pin each registry
970+
# package's source digest vs its record): the regenerate-and-compare half
971+
# runs where the registry is built, as ci.yml's Build & E2E UI step "The
972+
# committed SDUI manifest is the one this tree generates" (#26137), on
973+
# pull_request and on the merge queue's combined tree. Unfiltered:
974+
# filtering it to the artefact would go dormant on exactly the PR that
975+
# moves the artefact's inputs and not the artefact.
978976
# Direct invocation per the GATE INVOCATION IDIOM at the top of this file.
979977
- name: SDUI manifest is present, intact and fresh at the objectui pin
980978
id: sdui-manifest-is-present-intact-and-fresh

‎docs/releases-maintenance.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,16 @@ the same package, or a PR that changes the artefact, still conflict once, and th
8585
later one merges `main` and re-runs the command. ⛔ There is no npm install route to the manifest; that was
8686
deleted by ruling 丙 on #17735.
8787

88+
CI also regenerates the manifest and compares it with the committed bytes (#26137):
89+
the `Build & E2E UI` step "The committed SDUI manifest is the one this tree
90+
generates" runs the same command after the registry is built, on every PR that
91+
touches `ui/`, one of the seven packages objectui consumes (the parser among them)
92+
or the manifest's own files, and again on the merge queue's combined tree. A
93+
difference fails the step with the diff and the command to run. That covers what
94+
the source digests cannot see: an edit to `@objectstack/sdui-parser`, to a package
95+
the registry imports, and two queued PRs whose effect on the manifest only shows
96+
together.
97+
8898
**One producer, one artefact.** `packages/console/build-dist.sh` copies the tracked
8999
`sdui.manifest.json` into `packages/console/dist/`, so the published console carries the
90100
same bytes CI gates.

‎scripts/check-sdui-manifest.mjs‎

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,11 @@
8585
* line, and so does a PR that moves the artefact (it re-records every
8686
* package against the new sha256): those still conflict, on purpose,
8787
* because only a regeneration on the merged tree can say what the
88-
* manifest of the combination is. What is NOT forced is the case where
89-
* two PRs on different packages each leave the artefact unchanged and
90-
* their combination would not; the next regeneration of any registry
91-
* package sees the combination.
88+
* manifest of the combination is. What is NOT forced HERE is the case
89+
* where two PRs on different packages each leave the artefact unchanged
90+
* and their combination would not. The regeneration in ci.yml (below)
91+
* runs on the merge queue's combined tree, and that is where it is
92+
* caught (#26137).
9293
* 4. none. The in-tree `@object-ui/core` version moves with every release
9394
* of the one fixed version group while the registry does not, so the
9495
* record carries it as provenance and the success line prints it.
@@ -98,14 +99,18 @@
9899
*
99100
* ## What this deliberately does NOT do, and where that risk is held
100101
*
101-
* No per-PR regeneration: that would put an objectui clone and build (network,
102-
* minutes) inside a required lint job, a network dependency this repo
103-
* declines in a lint job. Under an unchanged pin the pinned
104-
* commit's registry is immutable, so content drift per-PR is not a live axis.
105-
* The residual axis — this repo's OWN adapter (`manifestFromConfigs`) changing
106-
* while the pin stands still — is held by `packages/sdui-parser`'s unit suite, and by regeneration being byte-deterministic (two runs
107-
* over one built tree, identical bytes), so the remedy this gate prints always
108-
* converges.
102+
* No regeneration in this gate: it runs in a required lint job that builds
103+
* nothing, and the registry takes minutes to build. The regeneration runs where
104+
* the registry is built: ci.yml's Build & E2E UI step "The committed SDUI
105+
* manifest is the one this tree generates" (#26137) runs the generator and
106+
* compares the committed artefact with its output byte for byte, on
107+
* pull_request and on the merge queue's combined tree. It serialises through
108+
* the built `@objectstack/sdui-parser`, so an edit to this repo's OWN adapter
109+
* (`manifestFromConfigs`) moves its verdict, which no digest here does.
110+
* Regeneration is byte-deterministic (two runs over one built tree, identical
111+
* bytes), so that comparison and the remedy this gate prints both converge.
112+
* With a pin, the pinned commit's registry is immutable, so content drift
113+
* per-PR is not a live axis there.
109114
*
110115
* Check 4 relates two fields of the record; it does not re-run the producer.
111116
* That the artefact describes the pinned SOURCE (rather than a tarball objectui

‎ui/scripts/__tests__/turbo-cache-save-bound.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ const CEILINGS: Record<string, number> = {
7474
[DEPS_JOB]: 25,
7575
[UI_JOBS.typeCheck]: 40,
7676
[UI_JOBS.lint]: 40,
77-
[UI_JOBS.buildE2e]: 25,
77+
[UI_JOBS.buildE2e]: 35,
7878
};
7979

8080
function job(key: string): Job {

0 commit comments

Comments
 (0)