diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 410835d114e..fe398c87d89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -472,6 +472,21 @@ jobs: - '.github/workflows/**' - '.github/prompts/**' - 'skills/objectui/**' + # turbo.json's `globalDependencies`: every build task hashes + # them, so they move what the ui jobs build, the SDUI manifest's + # regeneration included (#26137). + - 'tsconfig.json' + - 'tsconfig.base.json' + - 'tsconfig.test.base.json' + - 'tsup.config.ts' + # The committed SDUI manifest, its record, its producer and its + # offline gate. Build & E2E UI regenerates the manifest and + # compares it with the committed bytes (#26137), so a diff that + # moves only one of these still runs that comparison. + - 'sdui.manifest.json' + - 'scripts/sdui-manifest.record.json' + - 'scripts/gen-sdui-manifest-node.mjs' + - 'scripts/check-sdui-manifest.mjs' # The seven packages objectui installs from this tree (the set its # `.objectstack-pin` used to pack). A change here compiles and builds # the Console in the same PR — the check the retired Spec Shape Gate @@ -3734,6 +3749,10 @@ jobs: # - Build & E2E UI: n=51, min 106 s, median 170 s, p95 423 s, max 457 s # (7.6 min) => max(22.9, 22.6) => 25, down from objectui's 30 plus the # 15 the port added for the in-job build, which no derivation backed. + # Re-derived for the SDUI manifest regeneration (objectstack#26137), + # which adds 213 s cold (the 27 @object-ui builds plus the enumeration, + # measured on main 81bba6c3f): 457 + 213 = 670 s (11.2 min) + # => max(33.5, 26.2) => 35. # Re-derive when a job's work changes; ui/scripts/__tests__/ # turbo-cache-save-bound.test.ts holds both values. lint-ui: @@ -4063,7 +4082,7 @@ jobs: if: ${{ !cancelled() && needs.filter.outputs.ui_tree != 'false' && (needs.filter.outputs.ui != 'false' || needs.filter.outputs.ui_consumers != 'false') }} runs-on: ubuntu-latest # Derived above Lint UI. - timeout-minutes: 25 + timeout-minutes: 35 permissions: contents: read env: @@ -4122,6 +4141,30 @@ jobs: --filter=@objectstack/sdui-parser... \ --filter=@objectstack/types... + # The repo-root SDUI manifest, regenerated from this tree and compared + # with the committed bytes (#26137). The generator builds the registry + # with turbo: the 36 tasks of the step above replay, and the 27 + # @object-ui packages build (3m25s cold, measured on 4 cores, on main + # 81bba6c3f), then it enumerates the registry under plain Node (~8 s; ~6 s + # for the whole run when nothing moved). It serialises through the + # @objectstack/sdui-parser the step above built, so a parser edit moves + # this verdict too, which no committed digest does. On merge_group it runs + # on the queue's combined tree, the one run that sees two entries' effect + # on the manifest together. Only the artefact is compared: the records the + # generator rewrites beside it are held by `check-sdui-manifest` in + # lint.yml. The Console build below reads the registry through its src/ + # aliases, so the dist this step builds does not reach it. + - name: The committed SDUI manifest is the one this tree generates + run: | + node scripts/gen-sdui-manifest-node.mjs + if ! git diff --quiet -- sdui.manifest.json; then + 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." + git diff --stat -- sdui.manifest.json + git diff -- sdui.manifest.json | head -n 200 + exit 1 + fi + echo "sdui.manifest.json is byte-identical to this tree's regeneration." + - name: Build the Console for E2E env: VITE_BASE_PATH: /console/ diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e25555975e3..0acbfe56381 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -966,15 +966,13 @@ jobs: # repo-root `sdui.manifest.json` is absent or unreadable, so nothing in # the production path ever says "the gate lost its whitelist" — this step # is the thing that notices. Offline record checks only (existence, shape, - # sha256 vs the provenance record, record pin vs `.objectui-sha`): the - # regenerate-and-compare half runs where a regeneration can (the Node - # generator reads objectui's BUILT tree at the pin, which only - # `pnpm objectui:build` materialises — see - # scripts/gen-sdui-manifest-node.mjs), and a pin bump goes red HERE until - # it has been re-run — the same moment the lockstep step above forces the - # parser parity re-verification, with an objectui checkout guaranteed in - # hand. Unfiltered for the same reason as its neighbour: filtering to the - # artefact would go dormant on exactly the PR that moves the pin. + # sha256 vs the provenance record, and with no pin each registry + # package's source digest vs its record): the regenerate-and-compare half + # runs where the registry is built, as ci.yml's Build & E2E UI step "The + # committed SDUI manifest is the one this tree generates" (#26137), on + # pull_request and on the merge queue's combined tree. Unfiltered: + # filtering it to the artefact would go dormant on exactly the PR that + # moves the artefact's inputs and not the artefact. # Direct invocation per the GATE INVOCATION IDIOM at the top of this file. - name: SDUI manifest is present, intact and fresh at the objectui pin id: sdui-manifest-is-present-intact-and-fresh diff --git a/docs/releases-maintenance.md b/docs/releases-maintenance.md index 4b749328fdc..27068030a28 100644 --- a/docs/releases-maintenance.md +++ b/docs/releases-maintenance.md @@ -85,6 +85,16 @@ the same package, or a PR that changes the artefact, still conflict once, and th later one merges `main` and re-runs the command. ⛔ There is no npm install route to the manifest; that was deleted by ruling 丙 on #17735. +CI also regenerates the manifest and compares it with the committed bytes (#26137): +the `Build & E2E UI` step "The committed SDUI manifest is the one this tree +generates" runs the same command after the registry is built, on every PR that +touches `ui/`, one of the seven packages objectui consumes (the parser among them) +or the manifest's own files, and again on the merge queue's combined tree. A +difference fails the step with the diff and the command to run. That covers what +the source digests cannot see: an edit to `@objectstack/sdui-parser`, to a package +the registry imports, and two queued PRs whose effect on the manifest only shows +together. + **One producer, one artefact.** `packages/console/build-dist.sh` copies the tracked `sdui.manifest.json` into `packages/console/dist/`, so the published console carries the same bytes CI gates. diff --git a/scripts/check-sdui-manifest.mjs b/scripts/check-sdui-manifest.mjs index bd852cef52a..1759ae0dd34 100644 --- a/scripts/check-sdui-manifest.mjs +++ b/scripts/check-sdui-manifest.mjs @@ -85,10 +85,11 @@ * line, and so does a PR that moves the artefact (it re-records every * package against the new sha256): those still conflict, on purpose, * because only a regeneration on the merged tree can say what the - * manifest of the combination is. What is NOT forced is the case where - * two PRs on different packages each leave the artefact unchanged and - * their combination would not; the next regeneration of any registry - * package sees the combination. + * manifest of the combination is. What is NOT forced HERE is the case + * where two PRs on different packages each leave the artefact unchanged + * and their combination would not. The regeneration in ci.yml (below) + * runs on the merge queue's combined tree, and that is where it is + * caught (#26137). * 4. none. The in-tree `@object-ui/core` version moves with every release * of the one fixed version group while the registry does not, so the * record carries it as provenance and the success line prints it. @@ -98,14 +99,18 @@ * * ## What this deliberately does NOT do, and where that risk is held * - * No per-PR regeneration: that would put an objectui clone and build (network, - * minutes) inside a required lint job, a network dependency this repo - * declines in a lint job. Under an unchanged pin the pinned - * commit's registry is immutable, so content drift per-PR is not a live axis. - * The residual axis — this repo's OWN adapter (`manifestFromConfigs`) changing - * while the pin stands still — is held by `packages/sdui-parser`'s unit suite, and by regeneration being byte-deterministic (two runs - * over one built tree, identical bytes), so the remedy this gate prints always - * converges. + * No regeneration in this gate: it runs in a required lint job that builds + * nothing, and the registry takes minutes to build. The regeneration runs where + * the registry is built: ci.yml's Build & E2E UI step "The committed SDUI + * manifest is the one this tree generates" (#26137) runs the generator and + * compares the committed artefact with its output byte for byte, on + * pull_request and on the merge queue's combined tree. It serialises through + * the built `@objectstack/sdui-parser`, so an edit to this repo's OWN adapter + * (`manifestFromConfigs`) moves its verdict, which no digest here does. + * Regeneration is byte-deterministic (two runs over one built tree, identical + * bytes), so that comparison and the remedy this gate prints both converge. + * With a pin, the pinned commit's registry is immutable, so content drift + * per-PR is not a live axis there. * * Check 4 relates two fields of the record; it does not re-run the producer. * That the artefact describes the pinned SOURCE (rather than a tarball objectui diff --git a/ui/scripts/__tests__/turbo-cache-save-bound.test.ts b/ui/scripts/__tests__/turbo-cache-save-bound.test.ts index cfe971eff23..9cb04e7ebad 100644 --- a/ui/scripts/__tests__/turbo-cache-save-bound.test.ts +++ b/ui/scripts/__tests__/turbo-cache-save-bound.test.ts @@ -74,7 +74,7 @@ const CEILINGS: Record = { [DEPS_JOB]: 25, [UI_JOBS.typeCheck]: 40, [UI_JOBS.lint]: 40, - [UI_JOBS.buildE2e]: 25, + [UI_JOBS.buildE2e]: 35, }; function job(key: string): Job {