Skip to content

Commit e931621

Browse files
ci(sdui): retire the per-package SDUI manifest records; CI's regeneration holds freshness (#26137 PR B) (#26367)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoAqVfFyhsxKuFcEP9YTBj25)_ Closes #26137 (PR B of two: PR A #26315 put the regeneration in CI; this retires the per-package records it replaces). Tier H (`.github/workflows/**`). Part of objectstack#25677. Clause-②: no Metadata-protocol: none ## 维护者速读(草稿) - **改了什么:** 删掉 `scripts/sdui-manifest.sources/` 下 26 个按包记录的源码摘要,以及 `check-sdui-manifest` 里比对这些摘要的第 3 项检查。清单是否新鲜,现在只由 #26315 加的 CI 步骤判定:`Build & E2E UI` 重新生成 `sdui.manifest.json` 并逐字节比较,PR 上和合并队列的合并树上都跑。lint 里的检查保留存在性、形状和 sha256 防篡改。`AGENTS.md` 里仍在讲已退役 objectui 钉的那段改为现状(行数不变)。 - **为什么改:** 摘要记录是在 CI 不能重新生成清单时的替代品。它让两个改同一个 registry 包的 ui PR 在记录文件上冲突,哪怕谁都没改清单(`i18n` 最热,12 个开着的 ui PR 里有 5 个碰它)。本 PR 自己 rebase 到 `main` 时就撞上 7 个这样的冲突,而清单一个字节都没变。CI 重新生成后,PR 只在清单真的变了时冲突。 - **哪些原来能合的现在会被拒:** 没有。仍带 `inTree.registryRecords` 的旧记录会被拒,提示重新生成。 - **哪些原来被拒的现在能合:** 只改 registry 源码、不改清单的 PR 不用再提交记录文件。清单过期仍由 `Build & E2E UI` 拒绝,而且它看得见解析器、registry 引用的 `@objectstack` 包和两个排队 PR 合起来的效果,摘要看不见这些。 - **风险与回滚:** 只改 CI、脚本和文档,不发布任何包,清单字节不变。在本 PR 之前开的分支如果改过某个记录文件,rebase 时会遇到"修改/删除"冲突,删掉该文件即可。回滚 = revert 本 PR。 - **你要做的:** Tier H,需要 GOVERNED_APPROVERS 审批。 ## 席位核对 (left for the dispatch seat) ## Why `check-sdui-manifest` held freshness with a source digest per registry package, recorded one line per package under `scripts/sdui-manifest.sources/` (#26051). The digests existed because a PR author could not afford to regenerate the manifest on every push. They cost two things (#26137): 1. **Conflicts on records, not on the contract.** Two PRs that touched the same registry package's `src/` conflicted on that package's record even when neither changed `sdui.manifest.json`. 2. **A blind spot.** No digest saw `@objectstack/sdui-parser`, the `@objectstack` packages the registry imports, or two queued PRs whose effect on the manifest only shows together. #26315 made CI regenerate the manifest in `Build & E2E UI` and compare it with the committed bytes, on `pull_request` and on the merge queue's combined tree. It is green on `main`: the push run on `4ddf6df61` passed 50 of 53 check runs with 3 skipped and none failed, and `Build & E2E UI` ran the new step. The digests now only produce the conflicts. Rebasing this PR onto `main` hit seven of them: `components`, `core`, `i18n`, `plugin-detail`, `plugin-grid`, `react` and `types` had been re-recorded since, and the manifest's bytes had not moved. ## What changes ### `scripts/check-sdui-manifest.mjs` - **Check 3 in-tree is gone.** `registrySourceDigests`, `readRegistryRecords`, `registryRecordPath`, `registryRecordLine`, `REGISTRY_RECORDS` and the gate's copy of `REGISTRY_MODULES` go with it. - **The in-tree leg keeps** the artefact's presence and shape, the sha256 and component count against the record, and the `inTree` block (`modulesRoot`, `objectuiWorkspaceVersion`). - **A record that still carries `inTree.registryRecords` is RED** as a retired layout, the same way the single-digest fields from before #26051 already are. That catches a branch cut before this PR that regenerated with the old generator. - **`READ_PATHS` and `ROOT_WATCH_HINTS`** drop `ui/packages/**` and the records directory. The gate reads neither now, so a registry package edit no longer derives it. - **The success line** says what it checked and names the CI step for freshness. - **The pin mode is untouched.** ### Self-test: 29 rows → 21 The nine digest rows go with the code they tested: - a staged edit naming one package; - an unstaged edit; - tests and unreached packages; - a new workspace edge; - a new package; - a removed package; - a deleted record; - an other-artefact record; - a record out of its one-line form. Two rows pin the new boundary, and each was ablated to show it bites: | Row | Ablation | Result | |---|---|---| | a record from the per-package layout is RED with the in-tree command | drop `registryRecords` from the retired fields | row reds | | a producer run whose artefact did not move leaves the record as it was (replaces "re-records only the package whose sources moved") | stamp today's `generatedAt` | row reds | | same | re-read the `@object-ui/core` version instead of carrying it | row reds | The carry row matters more now: the record is the one shared file left, so a producer that rewrote it on every run would make every pair of ui PRs conflict on it again. `SELF_TEST_BATTERY_FLOOR` 29 → 21 matches the roster. The unused must-not-name column and the producer-list parity check (it existed only for the digest) are removed. ### `scripts/gen-sdui-manifest-node.mjs` - No digests, no refusal of unstaged registry bytes (that existed because the digest read the index), no per-package records. - It no longer imports the gate, and drops `git-env.mjs`. - The build, the enumeration, the record's carry rule and the pin mode are unchanged. - The record's `//` note now names the CI step. ### The records and the record - `scripts/sdui-manifest.sources/` (26 files) is deleted. - `scripts/sdui-manifest.record.json` is regenerated. It loses `inTree.registryRecords` and takes the new note. - `sdui.manifest.json` is **byte-identical** (sha256 `1f6e1ccb925a…`, 106 components), and `generatedAt` is carried. ### Readers - `scripts/check-changeset-no-major.mjs`: the comment, the real-build assertion and the record-only fixture name only the shared record. - `lint.yml` and `ci.yml`: the step comments. - `docs/releases-maintenance.md`: the manifest section, and the cut-rc step that said lint held freshness. - `AGENTS.md` (the dispatch seat asked for it here): the manifest paragraph still described the retired objectui pin. It said to run `pnpm objectui:build`, that `scripts/check-sdui-manifest.mjs` reds until you regenerate, and that `objectui:bump` and `objectui:refresh` print the step, but no `objectui:*` script exists since the cutover. It now names the in-tree command and the CI step. It is line-neutral: 5 lines stay 5, each ≤ 118 bytes, the file stays at 1112 lines, and `check:pm-skill-ratchet` and `check:pm-governed-prose` pass. `cut-rc.yml` and `release.yml` name only the tracked artefact, so they need no change. ### Widening or narrowing **For freshness, this is a narrowing of the lint gate, covered by a wider check that is already required.** Every case the digests refused is a case where the committed manifest differs from what the tree generates, or a case where it does not: - **The manifest differs:** a registry source change that moves the manifest, or a combination of PRs. `Build & E2E UI` refuses it on the PR or in the queue. - **The manifest does not differ:** a source change that does not move it, or a new package or edge that adds no component. Refusing those was the conflict cost, not protection. `Build & E2E UI` runs on `ui || ui_consumers`, and the registry's runtime closure is inside those filters (#26315's table), so no stale manifest that the digests caught reaches `main`. ## Merge order - **#26343** (2g PR 2) re-records `components`, `core`, `plugin-dashboard` and `types` and deletes `sdui-parser.json` under `scripts/sdui-manifest.sources/`. Whichever lands second rebases: - If #26343 lands first, this PR takes modify/delete conflicts on those records, and `git rm` resolves them. - If this PR lands first, #26343 drops its record edits the same way. - Its one hunk in `scripts/gen-sdui-manifest-node.mjs` (header lines 28–29) does not overlap this PR's hunks. It does not touch `sdui.manifest.json` or the record. - **Open branches** that changed a file under `scripts/sdui-manifest.sources/` get a modify/delete conflict on rebase. `git rm` the file, then re-run `node scripts/gen-sdui-manifest-node.mjs` if the PR changes the manifest. ## Verification Head `15f5e2777`, on `main` `77a52cfe8`. 34 files, +198 / −687, one commit. 26 of the files are the deleted records. Tier H: `check-governed-merges --test` exits 3 for `ci.yml`, `lint.yml` and `AGENTS.md`, and 0 for the other five. - **Regeneration:** on this head, `node scripts/gen-sdui-manifest-node.mjs` rebuilt the registry after `main` moved (467 s) and left `git status` clean. `sdui.manifest.json` was byte-identical, and the committed record is exactly what it writes ("left unchanged … its date and version are carried"). - **`check-sdui-manifest`:** `--self-test` 21 cases behave, and the production run is green: "present and intact (sha256 1f6e1ccb925a…, 106 components) … Not judged here: whether it is the manifest this tree generates." - **Ablations:** the three in the table above, each red, each restored. - **Before the rebase**, the same change stacked on PR A (`75d1ea668`, without the AGENTS.md paragraph) passed: - the derived sweep, 82 of 82 exit 0, with `--ran` reconciling 82 derived, 82 run, 0 NOT-MEASURED, 0 UNRUN; - the push battery, 2164 cases pass, 0 fail, EXIT 0. - **On this head**, the sweep and the battery are running and will be added here. - **CI:** pending. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01A6SEitNngAMugGbuJHWsDS Co-authored-by: Claude <noreply@anthropic.com>
1 parent 756220c commit e931621

34 files changed

Lines changed: 198 additions & 687 deletions

‎.github/workflows/ci.yml‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4148,12 +4148,12 @@ jobs:
41484148
# 81bba6c3f), then it enumerates the registry under plain Node (~8 s; ~6 s
41494149
# for the whole run when nothing moved). It serialises through the
41504150
# @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.
4151+
# this verdict too. On merge_group it runs on the queue's combined tree,
4152+
# the one run that sees two entries' effect on the manifest together.
4153+
# Only the artefact is compared: the record the generator rewrites beside
4154+
# it is held by `check-sdui-manifest` in lint.yml. The Console build below
4155+
# reads the registry through its src/ aliases, so the dist this step
4156+
# builds does not reach it.
41574157
- name: The committed SDUI manifest is the one this tree generates
41584158
run: |
41594159
node scripts/gen-sdui-manifest-node.mjs

‎.github/workflows/lint.yml‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -966,13 +966,11 @@ 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, 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.
969+
# sha256 vs the provenance record): freshness is decided where the
970+
# registry is built, by ci.yml's Build & E2E UI step "The committed SDUI
971+
# manifest is the one this tree generates" (#26137), on pull_request and
972+
# on the merge queue's combined tree. Unfiltered: it builds nothing and
973+
# runs in seconds, and an absent artefact is the failure it exists for.
976974
# Direct invocation per the GATE INVOCATION IDIOM at the top of this file.
977975
- name: SDUI manifest is present, intact and fresh at the objectui pin
978976
id: sdui-manifest-is-present-intact-and-fresh

‎AGENTS.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,11 @@ The Studio/Console UI lives in this workspace under `ui/` (`@object-ui/*`; the C
159159
and `ui/AGENTS.md` adds that tree's own rules to this file's. `pnpm console:build` builds the Console and copies its
160160
dist into `packages/console/` (`packages/console/build-dist.sh`). ⛔ Never hand-edit `packages/console/dist/`.
161161

162-
**Moving the pin has a second half: regenerate the committed manifest** — `pnpm objectui:build`, then
163-
`node scripts/gen-sdui-manifest-node.mjs`, which reads objectui's BUILT tree at the pin (never an npm install, whose
164-
version string names an older commit) and takes no arguments; `scripts/check-sdui-manifest.mjs` reds until you do.
165-
`objectui:bump` and `objectui:refresh` print the step, and the console build ships that same tracked file in
166-
`packages/console/dist/`. Full procedure: `docs/releases-maintenance.md`.
162+
**A registry change has a second half: regenerate the committed manifest** with
163+
`node scripts/gen-sdui-manifest-node.mjs`, which builds the registry under `ui/`, enumerates it from `ui/apps/console`
164+
(never an npm install) and takes no arguments; commit what it writes. CI's `Build & E2E UI` runs it too and fails
165+
when the committed `sdui.manifest.json` differs, the merge queue's combined tree included, and the console build
166+
ships that same tracked file in `packages/console/dist/`. Full procedure: `docs/releases-maintenance.md`.
167167

168168
**Fast iteration on `ui/` src (no rebuild loop):** run the Console dev server —
169169
`pnpm --filter @object-ui/console dev` (Vite on **:5180**, HMR). Its `/api` proxy targets

‎docs/releases-maintenance.md‎

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,11 @@ ships the Console from the same commit as the spec it builds against.
5555
> root `.changeset/` before the first joint Version PR is #25831. New frontend
5656
> changesets go in the root `.changeset/`.
5757
58-
#### After a registry change: regenerate the committed manifest (#12924, #17735, #25712, #26051)
58+
#### After a registry change: regenerate the committed manifest (#12924, #17735, #25712, #26137)
5959

6060
The repo-root `sdui.manifest.json` (ADR-0080, the Console's public-tier component
61-
manifest), its provenance record `scripts/sdui-manifest.record.json` and one record
62-
per registry package under `scripts/sdui-manifest.sources/` are generated, and one
63-
command writes them:
61+
manifest) and its provenance record `scripts/sdui-manifest.record.json` are
62+
generated, and one command writes them:
6463

6564
```bash
6665
node scripts/gen-sdui-manifest-node.mjs # no arguments
@@ -69,31 +68,26 @@ node scripts/gen-sdui-manifest-node.mjs # no arguments
6968
It builds the registry packages under `ui/packages/`, enumerates them from
7069
`ui/apps/console` under plain Node, and serializes them through
7170
`@objectstack/sdui-parser` (it needs `packages/sdui-parser/dist`:
72-
`pnpm --filter @objectstack/sdui-parser build`). Each package record is one line:
73-
a digest of that package's own `src/` and the `sha256` of the artefact it was
74-
verified against. A run rewrites only the records whose line changed, so a PR that
75-
edits one registry package re-records that package alone; the shared record moves
76-
only when the artefact does, and then every package record names the new `sha256`.
77-
78-
A required lint step reads the tracked files on **every PR**:
79-
`node scripts/check-sdui-manifest.mjs` checks the shape, the `sha256` against the
80-
record, and recomputes each package's source digest from the index. A PR that edits
81-
a registry package's source without regenerating goes red there, naming the package,
82-
and the failure text prints the regeneration call. Two PRs that edit different
83-
registry packages touch different record files and do not conflict; two that edit
84-
the same package, or a PR that changes the artefact, still conflict once, and the
85-
later one merges `main` and re-runs the command. ⛔ There is no npm install route to the manifest; that was
86-
deleted by ruling 丙 on #17735.
87-
88-
CI also regenerates the manifest and compares it with the committed bytes (#26137):
71+
`pnpm --filter @objectstack/sdui-parser build`). The record moves only when the
72+
artefact does, so a PR that does not change the manifest touches neither file.
73+
⛔ There is no npm install route to the manifest; that was deleted by ruling 丙 on
74+
#17735.
75+
76+
CI regenerates the manifest and compares it with the committed bytes (#26137):
8977
the `Build & E2E UI` step "The committed SDUI manifest is the one this tree
9078
generates" runs the same command after the registry is built, on every PR that
9179
touches `ui/`, one of the seven packages objectui consumes (the parser among them)
9280
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.
81+
difference fails the step with the diff and the command to run. Two PRs conflict
82+
on the manifest only when both change it, and the later one merges `main` and
83+
re-runs the command; two queued PRs whose effect on the manifest only shows
84+
together fail in the queue.
85+
86+
A required lint step, `node scripts/check-sdui-manifest.mjs`, reads the tracked
87+
files on **every PR**: the artefact is present and is a component manifest, and
88+
its `sha256` and component count are the record's. It builds nothing, so it does
89+
not judge freshness. (The per-package source records under
90+
`scripts/sdui-manifest.sources/` that it used to recompute, #26051, are retired.)
9791

9892
**One producer, one artefact.** `packages/console/build-dist.sh` copies the tracked
9993
`sdui.manifest.json` into `packages/console/dist/`, so the published console carries the
@@ -234,9 +228,9 @@ What it does, in order:
234228
re-reads `main`, so **`main` may keep moving for the whole run**.
235229
2. Builds the workspace, both scopes, then the Console (`pnpm run build`, then
236230
`pnpm run console:build`) from that snapshot. The dist carries the tracked
237-
`sdui.manifest.json`, whose freshness the required lint job already held on the
238-
PR that changed it. A Console that cannot build stops the cut before anything is
239-
written.
231+
`sdui.manifest.json`, whose freshness the required `Build & E2E UI` job already
232+
held on the PR that changed it and in the merge queue. A Console that cannot build
233+
stops the cut before anything is written.
240234
3. Runs the gates that read `.changeset/*`, before versioning consumes it.
241235
4. Runs `pnpm run version` — the repo script, never a bare `changeset version` —
242236
and **fails unless the computed version equals the one you typed**.

‎scripts/check-changeset-no-major.mjs‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,9 +1365,8 @@ export function publishedSourceOwners(path, manifestOf) {
13651365
// packed output — one path — and never which files it copies: the day the
13661366
// console build ships a second tracked file, this leg sees it with no edit
13671367
// here, and the day it stops copying the manifest, the leg stops claiming it.
1368-
// `scripts/sdui-manifest.record.json` and the per-package records under
1369-
// `scripts/sdui-manifest.sources/` are measured OUT for that reason rather than
1370-
// by choice: the build does not copy them, so they are not part of the shipped
1368+
// `scripts/sdui-manifest.record.json` is measured OUT for that reason rather
1369+
// than by choice: the build does not copy it, so it is not part of the shipped
13711370
// artefact.
13721371
//
13731372
// ⚠️ Residual, stated rather than implied: the reader understands the plain
@@ -4876,8 +4875,8 @@ function selfTest() {
48764875
`this repository's ${PACKED_COPY_BUILD_SCRIPTS[0]} copies the repo-root sdui.manifest.json into packages/console/dist/ as read by this leg — got ${JSON.stringify(realPairs)}`,
48774876
);
48784877
assert(
4879-
!realPairs.some((p) => p.source === 'scripts/sdui-manifest.record.json' || p.source.startsWith('scripts/sdui-manifest.sources/')),
4880-
`measured, not chosen: the real build ships neither the manifest record nor a package record — got ${JSON.stringify(realPairs)}`,
4878+
!realPairs.some((p) => p.source === 'scripts/sdui-manifest.record.json'),
4879+
`measured, not chosen: the real build does not ship the manifest record — got ${JSON.stringify(realPairs)}`,
48814880
);
48824881

48834882
// ── The owner of a destination ────────────────────────────────────────
@@ -4957,21 +4956,19 @@ function selfTest() {
49574956
const no = levelOf(copyRepo({ head: { ...PR22812, '.changeset/console-bbbb.md': changeset([[CONSOLE, 'patch']]) } }), declaredNo);
49584957
assert(no.result.verdict === 'none-clean' && no.exitCode === 0, `the manifest moved under \`Clause-②: no\` + \`none\` is unaffected — got ${no.result.verdict} / ${no.exitCode}`);
49594958

4960-
// The records move with regeneration but ship nothing: the shared one, and
4961-
// the one-line record of the registry package a ui PR re-recorded.
4959+
// The record moves with regeneration but ships nothing.
49624960
const recordOnly = levelOf(
49634961
copyRepo({
49644962
head: {
49654963
'scripts/sdui-manifest.record.json': '{"objectuiSha":"bbbb"}\n',
4966-
'scripts/sdui-manifest.sources/plugin-grid.json': '{"package":"ui/packages/plugin-grid"}\n',
49674964
'.changeset/console-bbbb.md': changeset([[CONSOLE, 'patch']]),
49684965
},
49694966
}),
49704967
yesWidening,
49714968
);
49724969
assert(
49734970
recordOnly.touched.packages.length === 0 && recordOnly.touched.unreadable.length === 0 && recordOnly.result.verdict === 'clean',
4974-
`control: the records alone move no published file — got ${JSON.stringify(recordOnly.touched)} / ${recordOnly.result.verdict}`,
4971+
`control: the record alone moves no published file — got ${JSON.stringify(recordOnly.touched)} / ${recordOnly.result.verdict}`,
49754972
);
49764973

49774974
// #4690 on this leg: a build script that is THERE but yields no copy is

0 commit comments

Comments
 (0)