Repository navigation
Commit 18cd410
ci: hourly Spec Shape Gate on main -- objectui main compiled against the spec built from the commit, read by the release gate, red attributed to the changing lane (#23283)
Fixes #23238
Clause-②: no
Executes ruling B on this card (comment 6145029395, batch 336 item 3,
maintainer 「其他同意」 2026-10-13T11:58Z): the spec shape gate moves from
objectui to objectstack -- hourly on `main` with the two-speed full
battery, read by the release gate, a red attributed to the lane that
moved the shape; objectui drops its copy after two consecutive agreeing
hourly runs. The mechanism of ruling C on #19011 (consumer compilation
as the one shape gate) is unchanged; only where it runs and who owns a
red changed. Governed surface: `.github/workflows/**` is Tier H, so this
PR stays a draft and lands only on the maintainer's approval. Not
touched, by the card's terms: `.github/workflows/release.yml` (in flight
under another card) and every objectui file (the objectui half is the
objectui `domain:devx` lane's card).
## What changed
- `.github/workflows/spec-shape-gate.yml` (new). One job, `Spec Shape
Gate`: checkout objectstack, checkout `objectstack-ai/objectui` at
`main` under `objectui/`, resolve both shas once, setup Node 22 + the
repo's `setup-pnpm` composite, restore one pnpm store keyed on both
lockfiles, run the gate script's self-test, `pnpm install --filter
@objectstack/spec...`, `pnpm --filter @objectstack/spec build`, `npm
pack --ignore-scripts` (the file set a release from this commit would
ship), install objectui, inject the tarball into objectui's pnpm virtual
store, `TURBO_FORCE=true pnpm type-check --continue` with the exit code
captured before any pipe, then an always-run report step that carries
the verdict. Triggers: `schedule` at `52 * * * *` (off ci.yml's and
lint.yml's `:00`), `workflow_dispatch`, and a `pull_request` trigger
path-filtered to the gate's own three files -- so a change to the gate,
a registration included, is exercised on its own PR and no other PR pays
the ~13 minutes (two-speed rule 1). No `merge_group`. Permissions are
reads only (`contents`, `actions`, `pull-requests`, `issues`).
Concurrency keys on `github.event_name` so a dispatch and the hourly run
on `main` never cancel each other; only PR runs cancel in progress.
- `scripts/ci/spec-shape-gate.mjs` (new). The objectstack port of
objectui's `scripts/spec-main-shape-gate.mjs` with the direction
reversed: `inject` writes into the DOWNSTREAM clone (`--downstream`),
reads the dependencies the spec was built against from THIS checkout
(`--upstream-checkout`), proves every workspace consumer resolves the
injected copy, and re-points a store sibling the packed manifest's range
refuses (the zod case). Workspace consumers and the turbo
name-to-directory map are now derived from the downstream's
`pnpm-workspace.yaml` rather than a hard-coded directory list. The
artifact-reuse half is dropped: the spec is always the one from the tree
under test. `report` parses the turbo log in both of turbo's log orders,
dedupes the two tsc programs of one task, reads the retirement registry,
splits diagnostics into `known` (registered) and new, attributes a new
red to the commit range since this workflow's last green run on `main`
and the `domain:*` lane of the card each pull fixes (`unattributed` when
it cannot read one), writes the run summary, emits one annotation per
new diagnostic (objectui path and objectstack sha in the title, no
`file=` -- the platform re-homes an unresolvable `file=` onto the
workflow file), and exits 0 green or known-only, 1 new break, 2 reading
not taken. `--self-test`: 60 cases in 7 pinned batteries with the
verdict-flag handshake (the `check-agent-model-declared.mjs` shape,
copied not imported).
- `scripts/ci/spec-shape-retirements.json` (new). The
intentional-retirement registry: one entry per retirement with
`retired`, `objectstack_pr`, `objectstack_commit` (40-hex),
`downstream_card`, `registered_at` and non-empty `matches`, each pattern
a repo-relative objectui `file` (glob allowed) plus at least one of
`code` / `message`. A file-only pattern, an empty `matches`, an unknown
key or a malformed field is refused and the whole run is exit 2 -- the
registry cannot become an ignore-all switch, and a registry that cannot
be read is not an empty one. Entries that matched nothing in a run are
listed for pruning. The first commit of this PR ships it EMPTY (positive
control run 1, expected red); the second commit registers PR #23160 ->
objectstack-ai/objectui#12325 (run 2, expected `known`).
- `scripts/ci/scheduled-full-run.mjs`. `RELEASE_GATE_WORKFLOWS` gains `{
file: 'spec-shape-gate.yml', name: 'Spec Shape Gate' }`, so
`release.yml`'s existing `full-battery` job (`--release-gate`,
unchanged) now refuses a publish unless this gate's newest `schedule`
run at or before the version commit concluded `success` too. The
self-test gains six cases (the recorded 2026-10-12 listing judged
explicitly over the two workflows it recorded, a firing control that the
default roster refuses that same listing naming `Spec Shape Gate` alone,
a red / a missing / a dispatch-only Spec Shape Gate run on synthetic
data, and a pin that every registered workflow file exists); floors rose
6→8, 14→17, 5→6, none lowered.
- `package.json`: `check:spec-shape-gate` alias (`node
scripts/ci/spec-shape-gate.mjs --self-test`), run by the workflow's
self-test step, so the script is a discoverable gate family and
`check-self-test-wired` sees its self-test run in CI.
## The PM's mechanism hypotheses, measured
1. Release-gate reading: `scheduled-full-run.mjs` enumerates
`RELEASE_GATE_WORKFLOWS` by workflow FILE and reads `GET
/actions/workflows/{file}/runs?branch=main&event={schedule|push}&status=completed`,
picks the nearest commit with a non-cancelled run and the newest run on
it, and requires `success` from every workflow. Registration is one row
there; a `known`-only run concludes `success`, so a registered
retirement does not block a release and an unregistered break does.
Confirmed; floors raised, none lowered.
2. Hourly placement: `ci.yml` and `lint.yml` carry `schedule: '0 * * *
*'`; this gate is its own workflow file at `:52`, not in `ci.yml` (no
`check-expected-skips` roster change needed: a workflow-level `paths:`
filter creates no check-run on a PR, so nothing shows as skipped).
3. Direction reversal: as described; the clone is `actions/checkout`
with `repository: objectstack-ai/objectui`, `ref: main`, `path:
objectui`, `fetch-depth: 1`. Measured on objectstack's
`pnpm-workspace.yaml`: every glob is rooted at `packages/`, `apps/` or
`examples/`, so `objectui/` is never swept into this repository's
install.
4. Attribution: last green = this workflow's newest completed `success`
run on `main` (`schedule` / `workflow_dispatch` / `push`) that is not
this run and not this head; range = the compare API `base...head`; per
commit the `(#N)` squash suffix -> pull labels + the pull body's closing
keywords -> card labels; `unattributed` with the reason otherwise.
Failure shape, printed not guessed: no earlier green (the first runs) ->
"no range could be named"; compare truncated at 250 -> marked;
pull-request lookups capped at 40 per run (printed when reached). A PR
run has no range and says so. The verdict never depends on the
attribution.
5. Registry: as described; the two positive-control readings are in the
section below.
6. Positive control and minutes: the `pull_request` path filter makes
this PR's own runs the measurement.
7. Self-test shape: three parts copied from
`scripts/check-agent-model-declared.mjs`; no shared assertion module
imported.
8. Permissions least-privilege and read-only; concurrency per event and
ref; the ~11-minute downstream chain is not run locally (foreground
cap), only the self-tests and the derived gates.
## 实测分钟数 -- measured minutes
Measured on this PR's own `Spec Shape Gate` runs (the `pull_request`
path filter makes them the measurement): run 1
[38411582655](https://github.com/objectstack-ai/objectstack/actions/runs/38411582655)
(head `1c3bc822`, empty registry) -- job wall **10.4 min**; run 2
[38413661014](https://github.com/objectstack-ai/objectstack/actions/runs/38413661014)
(head `fde9ac1c38`, PR #23160 registered) -- job wall **13.9 min**. 10.4
and 13.9 minutes of one `ubuntu-latest` runner per run; the uncached
downstream build plus typecheck is 8.3 to 11.3 of them (build spec 63-83
s, type-check objectui 498-677 s); hourly on `main` that is roughly
**250 to 335 runner-minutes a day** on the public repo. Both runs
restored the pnpm store from ci.yml's existing entries (objectui install
11-14 s); the save step runs only on `main` runs. Reported to the
maintainer under the ruling's fork clause (the seat's ACCEPT on #23238
and the final 维护者速读 below); this PR takes no fallback to option C.
## 阳性对照 -- positive control, two runs
- Run 1 (commit with the EMPTY registry): expected RED on the retirement
objectstack `main` carries since PR #23160 (view-level `provider:
'api'`), the same diagnostics objectui's hourly run shows --
`packages/react/src/hooks/useViewData.ts` TS2367 and
`packages/core/src/adapters/resolveDataSource.ts` TS2678 / TS2339.
- Run 2 (commit registering PR #23160 -> objectstack-ai/objectui#12325):
expected the same diagnostics read as `known (registered:
objectstack-ai/objectui#12325)`, zero new, job green.
- Readings (seat-verified from the runs' jobs and check-run
annotations): run 1
[38411582655](https://github.com/objectstack-ai/objectstack/actions/runs/38411582655)
-- job `failure`, `Report` step `failure` (exit 1), **10 new-break
annotations** (`spec shape break: objectui PATH:LINE (objectstack
f435d69)`, GitHub's 10-per-step cap; the full table is in the run
summary) over `packages/core/src/adapters/resolveDataSource.ts`,
`packages/react/src/hooks/useViewData.ts`,
`packages/plugin-calendar/src/ObjectCalendar.tsx`,
`packages/plugin-gantt/src/ObjectGantt.tsx` and two
`packages/types/src/__tests__/*.test.ts` fixtures -- every row the PR
#23160 retirement; run 2
[38413661014](https://github.com/objectstack-ai/objectstack/actions/runs/38413661014)
-- job `success`, report exit 0, **0 new-break annotations** (the only
failure-level annotations left are turbo's own task-failure lines), the
`known` table in the run summary.
## Verification
Local: `node scripts/ci/spec-shape-gate.mjs --self-test` (60/60 across 7
batteries), `node scripts/ci/scheduled-full-run.mjs --self-test` and
`--check-concurrency`, `pnpm check:nul-bytes`, and the gate list derived
by `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (58 commands) with exit codes captured
before any pipe; the per-command table is in the report. The diff
touches no published package, so the local build/test scope ① ② is
empty. The downstream compile itself is measured by this PR's own CI
runs, not locally.
## 维护者速读(草稿)
### 改了什么
把「拿平台当前代码编一份 spec、去编译下游 objectui」这道检查,从 objectui 搬到 objectstack:每小时在
`main` 上跑一次,结果进发版前必过的清单;红的那次会写明是哪些 objectstack
提交、哪条车道改坏了接口;有意的退役在「退役登记」里记一行,对应的报错就显示「已登记」而不再翻红。objectui
那边的同款检查暂不动,等这边连续两小时的结果和那边一致后由 objectui 车道删除。
### 为什么改
维护者裁决 B。现状是下游每个 PR 替上游做一次约 13
分钟的体检,红总在上游合并之后才出现在下游,而且有意的退役会让它连红好几天、盖住真正的意外破坏。搬到平台这边、定时跑、发版前必过,问题记在改动方名下,像出厂检验。
### 风险与代价(含回滚)
- 代价:objectstack 公共仓每小时多一次约 11–13
分钟的运行(实测分钟数见上文,由席位转交);发版门禁多读一道结果,这道结果红着(未登记的新破坏)就不能发版 —— 这正是裁决的意图。
-
风险:登记文件若被滥用成「全部忽略」会失去意义;脚本在结构上拒绝只写文件名不写判据的登记,也拒绝任何读不懂的登记(整次运行记作「没读到」,不是通过)。
- 回滚:删除这个 workflow 文件,并把 `scripts/ci/scheduled-full-run.mjs` 里
`RELEASE_GATE_WORKFLOWS` 的那一行去掉,发版门禁即回到只读 CI 与 Lint 两道的状态;objectui
侧的检查在此期间一直在跑,不会出现空档。
### 席位意见
(留空)
### 你要做的
1. 看上文「实测分钟数」:每小时一次的运行成本是否可接受。不可接受就在卡上说一声,⛔ 本 PR 不会自行退回方案 C。
2. 可接受则对本 PR 批准(`.github/workflows/**` 是 Tier H,需要你的一次 approve),之后由席位落地。
## Acceptance notes
- objectui's own runs carry ten extra annotations re-homed onto
`.github`: `actions/setup-node` registers a `tsc` problem matcher that
parses the echoed typecheck log a second time with paths that do not
resolve. Handled here by `::remove-matcher owner=tsc::` before the log
is echoed. Observation on objectui's side, not filed; carrier: the
objectui `domain:devx` deletion card, which removes that workflow
anyway.
- `scheduled-full-run-card.yml` files a card for a red hourly `CI` /
`Lint & Type Check` run with a fixed `domain:devx` label. This gate's
red is reported in the run summary and annotations with its own lane
attribution and files no card; wiring it into the card filer would label
the card with the wrong lane. Recorded as an open question in the
report, not changed here (the card's "no other check" term).
- `scripts/ci/scheduled-full-run.mjs --release-gate` reads `schedule`
and `push` events; this workflow has no `push` trigger, so only its
hourly runs are read by the release gate, and a `workflow_dispatch` run
is attributed but not read -- the same rule the other two workflows live
under.
- `release.yml`'s `full-battery` job comment still names two workflows
in prose; the script is the authority and is unchanged in release.yml by
the card's terms (in flight under another card). Noted for that card's
carrier.
---
_Generated by [Claude
Code](https://claude.ai/code/session_013zGKMG1cRedVzWtqGGGZW9)_
---------
Co-authored-by: objectstack-dev <dev@objectstack.ai>
Co-authored-by: Claude <noreply@anthropic.com>1 parent 5b68524 commit 18cd410
5 files changed
Lines changed: 2085 additions & 14 deletions
File tree
- .github/workflows
- scripts/ci
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
102 | 102 | | |
103 | 103 | | |
104 | 104 | | |
| 105 | + | |
105 | 106 | | |
106 | 107 | | |
107 | 108 | | |
| |||
0 commit comments