Skip to content

ci(ui-merge): port the four objectui gates that guard published @object-ui packages into root CI - #26854

Merged
os-zhuang merged 5 commits into
mainfrom
claude/project-thread-s71sz8
Oct 27, 2026
Merged

os-zhuang merged 5 commits into
mainfrom
claude/project-thread-s71sz8

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread

Part of #26717 (the four "port" rows: 23, 21, 7 test:dist, 22. The five port-or-retire rows 19, 18, 7-docs, 17 and 26 are measured below and left for the next card.)

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:把保护已发布 @object-ui/* 包的四个 objectui 检查接回根 CI。Build & E2E UI 多跑两步:Console 首屏体积预算(check:eager-closure)和 components 的构建产物测试(test:dist),都复用这个 job 已有的构建。新增一个只在 main 的 push/定时批次跑的 job「UI Published Artifacts」:先跑一次和发布相同的 pnpm build,再跑两个检查,一个查发布包里有没有测试文件,一个查每个 ESM 入口能不能被 Node 直接加载。scripts/release-publish.sh 在 changeset publish 之前先扫一遍,扫出问题就拒绝发布。
  • 为什么改:objectui 并入后,这四个检查没有在任何地方跑过。18.2.0 发 @object-ui/* 时也没有经过发布前的那道扫描。
  • 风险与回滚:Build & E2E UI 是必需检查,多出的两步会挡 PR。这两步在 main 上实测是绿的,首屏体积还有 481 KB 余量。新 job 不是必需检查,PR 和合并队列都不跑它。但发布审批前读的全量批次里会有它,它红了算 main-red。回滚就是 revert 本 PR。
  • 先看:.github/workflows/ci.yml 末尾新增的 ui-published-artifacts job;scripts/release-publish.sh 新增的拒发段;ui/scripts/__tests__/check-published-dist-tooling.test.ts 改写的接线测试(这一处要你拍板,见决策卡)。
  • 两分钟验证:node scripts/release-verify-npm.mjs --self-test,预期 169 例全过,其中第 16 组有 12 例,包括「扫出测试文件就拒绝发布、changeset publish 不会运行」。
  • 需要你做的:批准本 PR(Tier H),并在线程里回答行 23 的位置问题(推荐 B,本 PR 就是按 B 做的)。

What changes

Rows of the objectstack#25697 workflow roster, ported from the objectui workflows deleted by #26058. Invocations were read at that PR's parent be789779 (published-dist-gate.yml, node-esm-load-gate.yml, performance-budget.yml, ci.yml "Test (dist pins)").

Row Gate Where it runs now Step name
22 check:eager-closure Build & E2E UI, over the Console build the job already makes. On a pull request, a step before it resolves the base branch's ceiling constants (objectui#6245 freshness half), ported from performance-budget.yml. check:eager-closure — resolve the base-branch ceiling constants, check:eager-closure — weigh the Console's eager closure against its budget
7 test:dist on @object-ui/components Build & E2E UI. The root turbo task's dependsOn: ["build"] replays the registry build the SDUI manifest step already ran. test:dist — run the built-artifact pins of @object-ui/components
23 check:published-dist New full-battery job UI Published Artifacts, after pnpm build, with --no-build. Also scripts/release-publish.sh, before npm's record starts, refusing on a non-zero exit. check:published-dist — scan every published @object-ui tarball for tooling files
21 check:node-esm-load (load leg) Same job, same build, --no-build. It runs whatever the scan above said. check:node-esm-load — import every published ESM entry under plain Node
  • ui-published-artifacts (UI Published Artifacts): if: is Build & E2E UI's condition plus github.event_name != 'pull_request' && github.event_name != 'merge_group', the full-battery spelling. So a pull request and a queue entry skip it, main's push and 4-hourly schedule runs carry it, and scheduled-full-run.mjs --release-gate reads it before a publish approval. It is not a required context. Its setup is spelled out (Node, pnpm, a restore-only store cache, pnpm install --frozen-lockfile) rather than the setup-workspace composite, because check-pre-install-import-graph.mjs reads a job with no literal pnpm install as running entirely before node_modules exists, and both gates import typescript. It restores the build-core / ui-deps turbo seeds (restore-only). timeout-minutes: 60, objectui's bound for each of the two builds it replaces.
  • Why pnpm build + --no-build. The gate's own build mode is not the release's build. check:published-dist builds turbo run build --filter=!@object-ui/site from ui/, which under the monorepo is 128 tasks including @objectstack/docs; the release builds pnpm build (no docs). check:node-esm-load's build mode runs pnpm build in ui/, which has no build script (measured: ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command "build" not found, exit 254). Building once with the release's command and grading with --no-build is the order the publish path runs the scan in. --no-build cannot pass vacuously: a package with nothing under its build output is no-build-output, both gates assert a floor, and the ESM gate's provenance leg still refuses an artifact whose turbo log names another tree.
  • scripts/release-publish.sh runs node ui/scripts/check-published-dist-tooling.mjs --no-build after the npm_config_globalconfig unset and before --start-npm-record, so the scan's npm pack --dry-run runs never land in the record release-verify-npm.mjs reads back. A non-zero exit prints an ::error:: and exits 1 before changeset publish.
  • THE TWO SPEEDS header in ci.yml now names four full-battery jobs.

Forced test edits

  • scripts/release-verify-npm.mjs battery 16 (floor 8 → 12). Its run of release-publish.sh would otherwise run the real scan over an unbuilt tree. It now stubs npm on PATH (answering npm pack --dry-run --json with one build-output file, plus a dist/__tests__/*.test.js when asked; the stub writes a debug log into npm_config_logs_dir like real npm). New cases: the scan packs every published package before changeset; npm's record holds only the publish's log; a tarball with tooling material exits 1 with Refusing to publish; and then changeset and git are never called. The existing call-order case filters out the npm pack lines.
  • ui/scripts/__tests__/check-published-dist-tooling.test.ts (own commit, option B on the thread's decision card). The pin is NOT wired into any per-PR workflow read the ruling (objectui#4846 comment 5307574139: no per-PR full-repo build) per workflow FILE, so no job of ci.yml (which subscribes pull_request) could carry the scan. It now reads per JOB: in any workflow that subscribes pull_request or merge_group, a job that runs the scan must deny both events in its own if:, and the loop must find at least one carrier. The retired runs nightly pin comes back as runs in ci.yml's full battery, after the release's build, with --no-build. If option A or C is chosen instead, this commit is dropped and the job moves or loses its first step.

Why

The roster kept these four as "kept, follow-up port", and #26058 deleted their only runners. Since then nothing has run them: 18.2.0 published @object-ui/* without the published-dist scan, and a Console first-load regression would land unmeasured. Card #26717 has the business case.

Tests

Measured on head f0665932 (the final head 8090d1fa differs on this PR's paths only in the new job's setup steps, spelled out instead of the composite; the gate commands, scripts and release-publish.sh are unchanged) in this worktree, after a cold pnpm build (116 tasks, 13m50s under os-verify-lock, shared box):

The four gates as the CI steps spell them (all exit 0)
  • pnpm --dir ui run check:published-dist --no-build: exit 0, 241 s. Inspected 36 published package(s): 5799 tarball file(s), 5654 of them build output, 0 tooling artifact(s) in build output.
  • pnpm --dir ui run check:node-esm-load --no-build: exit 0, 81 s. Provenance leg: 35 of 35 gradable entries were built by this tree. Load leg: 33 of 36 published ESM entries imported and evaluated. (create-plugin is bin-only; plugin-dashboard and plugin-map are the ruled .css boundary.)
  • VITE_BASE_PATH=/console/ pnpm --dir ui/apps/console exec vite build, then pnpm --dir ui run check:eager-closure: exit 0. Console eager closure is 1950.2 KB gzipped across 89 of 2392 chunks (budget: 2431.6 KB, headroom: 481.4 KB). Per-chunk, membership and sensitivity halves all ✅; freshness not applicable off a pull request.
  • pnpm exec turbo run test:dist --filter=@object-ui/components: exit 0, Test Files 2 passed (2), Tests 8 passed (8).
  • PR freshness step rehearsed: its run: extracted from this ci.yml, run under bash -e with PR_BASE_SHA=8b276385, BASE_REF=main: exit 0, then the checker with GITHUB_EVENT_NAME=pull_request and those five variables: exit 0, Ceiling freshness: all 3 ceiling constants are unchanged on main since this checkout's base.
  • node scripts/release-verify-npm.mjs --self-test: OK … 169 cases pass across 16 batteries.
  • pnpm --dir ui exec vitest run scripts/__tests__/check-published-dist-tooling.test.ts: 33 passed.
Counter-probes (each defect turned its step red, then was restored)

One locked script, every mutation restored by trap and checked by sha256 or git diff HEAD:

  • Row 23: ui/packages/core/dist/probe-26717.test.d.ts written into a built dist. check:published-dist --no-build exit 1: @object-ui/core [tooling-in-published-output] packages/core/dist/probe-26717.test.d.ts. The release-path spelling node ui/scripts/check-published-dist-tooling.mjs --no-build exit 1 too. Restored (file absent). Battery 16's tooling case is the same refusal through release-publish.sh.
  • Row 21: export * from './probe26717extless' appended to built ui/packages/react/dist/index.js. check:node-esm-load --no-build exit 1: ✗ @object-ui/components (via @object-ui/react): ERR_MODULE_NOT_FOUND …/react/dist/probe26717extless, 21 lines naming it. Restored, sha matches.
  • Row 22: eager-closure.json grown by 700,000 gzip bytes. check:eager-closure exit 1: ❌ Console eager closure is 2633.8 KB gzipped across 89 chunks — 202.2 KB over the 2431.6 KB budget. Restored, sha matches; re-run exit 0.
  • Row 7: through scripts/ablation-replace.mjs, namespace: 'page' → namespace: 'probe26717' on page:header's registration in ui/packages/components/src/renderers/layout/containers.tsx. turbo run test:dist --filter=@object-ui/components rebuilt the package and exited 1: × registers page:header from the BUILT bundle. Restored (git diff HEAD empty); re-run exit 0.
  • Release path, mutation-free: battery 16's a tarball carrying tooling material fails the script and changeset publish never runs cases.

CI run ids per gate go on the card once a main push run and this PR's run exist: this PR's run covers the two Build & E2E UI steps; UI Published Artifacts first runs on the push that lands this.

Acceptance notes

  • The five rows left for the next card, measured on origin/main 8b276385: no root workflow runs any of them.
  • The rest of objectui's budget step is not ported. It also held the entry chunk to 350 KB gzipped, ran check-eager-locale-catalogues.mjs (alias check:eager-locale-catalogues), check:sdui-registration-pins, a package size report and a PR comment. None of them runs in root CI today. The card names check:eager-closure only; the rest is next-card material.
  • Found, not fixed here: pnpm --dir ui run check:node-esm-load without --no-build exits 1 in the monorepo (ui/package.json has no build script). And check:published-dist's own build mode builds @objectstack/docs, unlike the release it claims to match. Both are ui script defects; this PR routes around neither: CI and the release call the gates with --no-build after the release's own build. Fixing the scripts' build modes is a ui-lane card (and ui/package.json is held by open chore(deps)(deps-dev): bump the development-dependencies group across 1 directory with 22 updates #26556).
  • release.yml's comment over the publish step still describes release-publish.sh as changeset publish followed by one tag push; it now runs the scan first. Not edited here (outside the claimed surface).
  • The check-node-esm-load.mjs docblock says --no-build "is wired into nothing"; this PR wires it. Not edited (ui script, outside the surface).
  • No new required context. Build & E2E UI (required) gains three steps, about 1 to 2 minutes on top of its measured 312 s run (schedule run 40537439467) against its 35-minute bound.

Verification

Final head 8090d1fa (the branch with origin/main 0ea90c7 merged in; no overlapping files). After a fresh pnpm build (13m22s), the sweep ran every command node scripts/pm/dispatch-gates.mjs --commands derives for this change set (4 paths) under os-verify-lock (VERDICT command-exit 0, 44m35s).

Gate sweep and reconciliation
  • 71 derived commands, 71 run, all exit 0. Among them: pnpm --dir ui exec vitest run scripts/ (178 files, 5066 tests passed, 2 skipped), node scripts/release-verify-npm.mjs --self-test (169 cases across 16 batteries), pnpm check:published-files, and check:pm-dispatch-gates (fast tier, 1935 cases).
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran <ledger>: ✓ 71 derived famil(ies) accounted for — 71 run, 0 NOT-MEASURED, 0 UNRUN.
  • Not run locally: pnpm lint and the full test suites, which are CI's run.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (no formatter ran; pnpm lint is CI's run)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset. (nothing publishes: workflow, root scripts and a ui test; skip-changeset)
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block is the repo's Chinese brief).
  • The Before Submitting list in CONTRIBUTING.md is done. (the local subset per the dispatch contract; the full suite is CI's)

🤖 Generated with Claude Code

https://claude.ai/code/session_01ADHkmNiJfZmoXAbfcJSAfx

…ct-ui packages into root CI

- Build & E2E UI runs check:eager-closure (with the base-branch ceiling
  freshness step on pull requests) over the Console build it already makes,
  and test:dist for @object-ui/components over the registry build the SDUI
  manifest step already ran.
- A full-battery job, UI Published Artifacts (push/schedule only), builds what
  the release builds and runs check:published-dist and the check:node-esm-load
  load leg over it with --no-build.
- scripts/release-publish.sh runs check-published-dist-tooling --no-build
  before npm's record starts and refuses to publish on a non-zero exit.
  release-verify-npm battery 16 stubs npm and pins the order, the clean
  record and the refusal.

Claude-Session: https://claude.ai/code/session_01ADHkmNiJfZmoXAbfcJSAfx
Co-authored-by: Claude <noreply@anthropic.com>
…in the full-battery carrier

The ruling on objectui#4846 forbade a per-PR full-repo build. The pin read it
per workflow FILE, which a full-battery job in ci.yml (a file that subscribes
pull_request) cannot satisfy although it costs a pull request nothing. It now
reads each job of a per-PR or queue workflow: a job that runs the scan must
deny both events in its own if:. The retired "runs nightly" pin comes back
against the root job that schedules the scan.

Claude-Session: https://claude.ai/code/session_01ADHkmNiJfZmoXAbfcJSAfx
Co-authored-by: Claude <noreply@anthropic.com>
…stall graph sees it

check-pre-install-import-graph finds a job's install by its `pnpm install`
line and reads every step of a job without one as running before
node_modules exists; both gates import typescript. The ui jobs spell their
setup out for the same reason.

Claude-Session: https://claude.ai/code/session_01ADHkmNiJfZmoXAbfcJSAfx
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 27, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:skills#1 · session_01ADHkmSrRBdu9MdC6fc33Uu · head 8090d1fa · 2026-10-27T18:43Z

  1. 形态 — Tier H (.github/workflows/ci.yml): four rows of the tooling(ui-wiring): objectstack-side inert wiring for the ui/ workspace — workspace globs, root turbo tasks, path-filtered ui test/typecheck/lint/E2E jobs, spec change compiles the Console; pin gates retire at cutover (parent #25677, 3/6) #25697 roster that have run nowhere since ci(ui-merge): delete objectui's nested workflows and Dependabot config; retire the action-ref scan and the CI page pin test with them #26058 are ported. Rows 22 (check:eager-closure, with its PR base-freshness step) and 7 (test:dist of @object-ui/components) fold into Build & E2E UI, reading the build that job already makes. Rows 23 (check:published-dist) and 21 (check:node-esm-load) run in a new non-required job, UI Published Artifacts, in the full battery only (push / schedule / dispatch), with one pnpm build and then --no-build. scripts/release-publish.sh runs the published-dist scan before npm's record starts and refuses to publish on non-zero. No deletion, no new required context, ui/package.json untouched (chore(deps)(deps-dev): bump the development-dependencies group across 1 directory with 22 updates #26556).
  2. 前提实测 — the seat read the diff on the head: the base-freshness step is continue-on-error but fails closed (the checker exits 2 with its variables unset); the new job copies Build & E2E UI's if: plus the full-battery clause and spells its setup by hand for check-pre-install-import-graph.mjs; the release-path scan sits above npm's record, so its npm pack --dry-run runs stay out of what release-verify-npm.mjs reads back.
  3. 门禁 check-runs — dev ran the 71 derived commands, all exit 0 (--ran 71 run, 0 NOT-MEASURED, 0 UNRUN). Four anti-probes each turned red and then recovered. CI on the head is read again before landing.
  4. 接受的偏差 — two tests moved with the change, both tightening: release-verify-npm.mjs group 16 now stubs npm and its case floor goes 8 → 12; the per-PR pin in check-published-dist-tooling.test.ts reads by job, in its own commit. Row 23's placement follows recommendation B on the decision card the maintainer has open in the card thread; on A or C that commit is dropped.
  5. 范围外去向 — the dev measured rows 19 / 18 / 7-docs / 17 / 26 and the unported half of objectui's budget step (the 350 KB entry, eager-locale catalogues) as carried by no root CI → the next card. Two ui script defects (node-esm-load build mode exits 254; published-dist build mode builds docs) → filed by the seat. Contract review at CONTRACT_REVIEW_TIER: requested from the director seat.
  6. 落地条款 — governed, Tier H, and ci.yml: stays draft until a GOVERNED_APPROVERS approval and the maintainer's explicit approval of the ci.yml change; then it lands through the queue.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Test UI (6/16) is red on 8090d1fa, and the failure is not this PR's.


Generated by Claude Code

@objectstack-fleet

objectstack-fleet Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor Author

Contract review (FAIL) on head 8090d1fa — WITHDRAWN by the director seat: its one must-fix read scripts/pm/check-expected-skips.mjs from a checkout behind main; since #26648 the roster is derived from each job's live if:, and UI Published Artifacts derives with no edit. The record of record for this head is the seat's superseding comment on this PR (**VERDICT: PASS**, same head). Nothing in the diff changed.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8090d1fa587b27e0ce7c115979eecd7c96ec1b36
Local-runs: none
Scope: .github/workflows/ci.yml + scripts/release-publish.sh, scripts/release-verify-npm.mjs, ui/scripts/tests/check-published-dist-tooling.test.ts, scripts/pm/check-expected-skips.mjs, scripts/ci/scheduled-full-run.mjs, ui/scripts/check-published-dist-tooling.mjs, ui/scripts/check-node-esm-load.mjs, ui/scripts/check-eager-closure-budget.mjs, ui/scripts/check-pre-install-import-graph.mjs, ui/package.json, turbo.json, package.json

Director seat (summon #52, ledger objectstack#12708). This record supersedes 6450870402 on the same head; nothing in the diff changed. That record's one must-fix was wrong: it read scripts/pm/check-expected-skips.mjs from a checkout 96 commits behind main — before #26648 (55b92731) replaced the hand-listed roster with one derived from each job's live if:. Re-read on origin/main 0ea90c7a, the roster is derived (header L43–51), jobGate (L474) splits the job-level if: on top-level && and admits a pr-event term github.event_name != 'pull_request' | 'merge_group' (L438, L455) beside filter-output terms, and the self-test refuses any exported EXPECTED_SKIPS literal (L1902, "no hand-listed roster survives"). The new job's if: is exactly that shape, so UI Published Artifacts derives on the head with no roster edit, and the row the earlier record prescribed would have turned the self-test red. The seat's reading (6450962911) is right; the error was this seat's. Everything else in 6450870402 stands and is restated here so this record reads whole. Served at CONTRACT_REVIEW_TIER. The seat records; it does not approve, ready, queue or merge.

① Derived judgments

  • The four ports read the originals faithfully. Row 22: the ceiling-freshness step is objectui's performance-budget.yml step verbatim but for the checker path, including the continue-on-error that is not a fail-open — check-eager-closure-budget.mjs:3904 refuses to run on a pull_request without EAGER_CLOSURE_PR_BASE_SOURCE (:4175–4181 read the set). The budget step reads the Console the job already builds (VITE_BASE_PATH=/console/, vite build, which writes eager-closure.json). Row 7: objectui's test-dist-pins job is @object-ui/components' test:dist script; turbo.json gives the task dependsOn: ["build"], so the registry build replays from cache. Rows 23 / 21: both objectui workflows ran on schedule + push only under a "⛔ Do not add pull_request here" header; the new job keeps that with the full-battery clause, runs the release's own pnpm build (package.json release = pnpm run build && … release-publish.sh) and grades it with --no-build, whose vacuous-pass guards are in both scripts (no-build-output is a finding; a floor on packages inspected; the ESM gate's provenance leg refuses a foreign tree). Both aliases exist in ui/package.json.
  • The publish-path refusal is placed right. release-publish.sh runs the scan after the npm_config_globalconfig unset and before --start-npm-record, so the scan's npm pack --dry-run runs stay out of the record release-verify-npm.mjs reads back; the script locates itself from import.meta.url (:140), so the root-relative spelling is sound. Battery 16's four new cases pin that order and the refusal; its floor moves 8 → 12 with them.
  • The release gate reads the new job without registration. scheduled-full-run.mjs --release-gate selects the nearest completed schedule/push run on main per workflow and reads the RUN's conclusion, so a red UI Published Artifacts is a red full battery.
  • The enqueue roster derives the new job. On a checkout that carries this ci.yml, deriveGatedSkips admits ui-published-artifacts under the name UI Published Artifacts with gate filter ui_tree + filter ui|ui_consumers + pr-event pull_request|merge_group. This PR's own enqueue read is the one exception: a judge run from a main checkout derives from main's ci.yml, which lacks the job until this lands — the one-time exit 4 the self-test's add case describes; the landing seat reads it as such.
  • The if: is Build & E2E UI's plus the full-battery clause; THE TWO SPEEDS header names four jobs. The spelled-out setup is right for check-pre-install-import-graph.mjs, whose population is every node step before the first pnpm install in the same job. Restore-only caches, the turbo keys and action versions match the sibling ui jobs.

② Semver level

skip-changeset: a workflow, two root scripts and a ui test; nothing published moves. Correct.

③ Boundary flags

④ Design judgment

  • ci.yml as the rules face: folding rows 22 and 7 into the job that already holds the Console and registry builds is the roster's own note and costs no second build; putting rows 23 and 21 behind one release-shaped build with --no-build makes CI grade the same artifacts the publish path grades, better than objectui's two separate builds, and keeps the per-PR ruling intact per job. The blocking copy on release-publish.sh protects the npm consumer; the CI copy is the early warning. Holds; nothing to break.

Implemented-by: claude/project-thread-s71sz8
Reviewed-by: session_01ADHkmQr732toX135tyo5zs

VERDICT: PASS


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants