Skip to content

Commit d36dc42

Browse files
committed
ci(ui): regenerate the SDUI manifest in Build & E2E UI and compare it with the committed bytes (#26137)
check-sdui-manifest judges freshness from committed per-package source digests, so it cannot see an edit to @objectstack/sdui-parser, to a package the registry imports, or two queued PRs whose effect only shows together. Build & E2E UI now runs scripts/gen-sdui-manifest-node.mjs after its @objectstack build and fails when sdui.manifest.json differs from what this tree generates. The 36 @objectstack tasks replay from Build UI Deps; the 27 @object-ui builds plus the enumeration cost 213 s cold on 4 cores. - ci.yml `ui` filter: turbo.json's globalDependencies and the manifest's own four files, so a diff that moves only those still runs the step. - Build & E2E UI ceiling re-derived 25 -> 35 (457 + 213 s), with the pin in ui/scripts/__tests__/turbo-cache-save-bound.test.ts. - lint.yml and check-sdui-manifest.mjs prose, and releases-maintenance.md, point at the new step. The digest check stays until this is green on main. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A6SEitNngAMugGbuJHWsDS
1 parent ff91895 commit d36dc42

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
@@ -454,6 +454,21 @@ jobs:
454454
- 'pnpm-workspace.yaml'
455455
- 'turbo.json'
456456
- '.github/workflows/ci.yml'
457+
# turbo.json's `globalDependencies`: every build task hashes
458+
# them, so they move what the ui jobs build, the SDUI manifest's
459+
# regeneration included (#26137).
460+
- 'tsconfig.json'
461+
- 'tsconfig.base.json'
462+
- 'tsconfig.test.base.json'
463+
- 'tsup.config.ts'
464+
# The committed SDUI manifest, its record, its producer and its
465+
# offline gate. Build & E2E UI regenerates the manifest and
466+
# compares it with the committed bytes (#26137), so a diff that
467+
# moves only one of these still runs that comparison.
468+
- 'sdui.manifest.json'
469+
- 'scripts/sdui-manifest.record.json'
470+
- 'scripts/gen-sdui-manifest-node.mjs'
471+
- 'scripts/check-sdui-manifest.mjs'
457472
# The seven packages objectui installs from this tree (the set its
458473
# `.objectstack-pin` used to pack). A change here compiles and builds
459474
# the Console in the same PR — the check the retired Spec Shape Gate
@@ -3716,6 +3731,10 @@ jobs:
37163731
# - Build & E2E UI: n=51, min 106 s, median 170 s, p95 423 s, max 457 s
37173732
# (7.6 min) => max(22.9, 22.6) => 25, down from objectui's 30 plus the
37183733
# 15 the port added for the in-job build, which no derivation backed.
3734+
# Re-derived for the SDUI manifest regeneration (objectstack#26137),
3735+
# which adds 213 s cold (the 27 @object-ui builds plus the enumeration,
3736+
# measured on main 81bba6c3f): 457 + 213 = 670 s (11.2 min)
3737+
# => max(33.5, 26.2) => 35.
37193738
# Re-derive when a job's work changes; ui/scripts/__tests__/
37203739
# turbo-cache-save-bound.test.ts holds both values.
37213740
lint-ui:
@@ -4032,7 +4051,7 @@ jobs:
40324051
if: ${{ !cancelled() && needs.filter.outputs.ui_tree != 'false' && (needs.filter.outputs.ui != 'false' || needs.filter.outputs.ui_consumers != 'false') }}
40334052
runs-on: ubuntu-latest
40344053
# Derived above Lint UI.
4035-
timeout-minutes: 25
4054+
timeout-minutes: 35
40364055
permissions:
40374056
contents: read
40384057
env:
@@ -4091,6 +4110,30 @@ jobs:
40914110
--filter=@objectstack/sdui-parser... \
40924111
--filter=@objectstack/types...
40934112
4113+
# The repo-root SDUI manifest, regenerated from this tree and compared
4114+
# with the committed bytes (#26137). The generator builds the registry
4115+
# with turbo: the 36 tasks of the step above replay, and the 27
4116+
# @object-ui packages build (3m25s cold, measured on 4 cores, on main
4117+
# 81bba6c3f), then it enumerates the registry under plain Node (~8 s; ~6 s
4118+
# for the whole run when nothing moved). It serialises through the
4119+
# @objectstack/sdui-parser the step above built, so a parser edit moves
4120+
# this verdict too, which no committed digest does. On merge_group it runs
4121+
# on the queue's combined tree, the one run that sees two entries' effect
4122+
# on the manifest together. Only the artefact is compared: the records the
4123+
# generator rewrites beside it are held by `check-sdui-manifest` in
4124+
# lint.yml. The Console build below reads the registry through its src/
4125+
# aliases, so the dist this step builds does not reach it.
4126+
- name: The committed SDUI manifest is the one this tree generates
4127+
run: |
4128+
node scripts/gen-sdui-manifest-node.mjs
4129+
if ! git diff --quiet -- sdui.manifest.json; then
4130+
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."
4131+
git diff --stat -- sdui.manifest.json
4132+
git diff -- sdui.manifest.json | head -n 200
4133+
exit 1
4134+
fi
4135+
echo "sdui.manifest.json is byte-identical to this tree's regeneration."
4136+
40944137
- name: Build the Console for E2E
40954138
env:
40964139
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)