Skip to content

Commit 18cd410

Browse files
objectstack-fleet[bot]objectstack-devclaude
authored
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

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
name: Spec Shape Gate
2+
3+
# Compile the downstream consumer -- objectstack-ai/objectui at `main` -- against
4+
# `@objectstack/spec` built from THIS commit, and name the objectui file, the
5+
# objectstack commit and the owning lane when it does not compile.
6+
#
7+
# ## The ruling this executes
8+
#
9+
# Ruling B on the decision card for the spec shape gate (#23238, comment
10+
# 6145029395, batch 336 item 3, 2026-10-13): the gate MOVES to objectstack --
11+
# hourly on `main` with the two-speed full battery, read by the release gate,
12+
# a red attributed to the lane that moved the shape; objectui drops its copy
13+
# once two consecutive hourly runs here agree with objectui's. The MECHANISM of
14+
# ruling C on #19011 (consumer compilation as the ONE shape gate for the spec's
15+
# public surface) is kept unchanged; what moved is where it runs and who owns a
16+
# red. ⛔ Neither direction is re-adjudicated here.
17+
#
18+
# The objectui half -- `.github/workflows/spec-main-shape-gate.yml` there, its
19+
# script `scripts/spec-main-shape-gate.mjs` and its three pins -- is the
20+
# objectui `domain:devx` lane's card, filed once two consecutive hourly runs of
21+
# THIS workflow agree with objectui's hourly run; both run in parallel until
22+
# then. This file is the objectstack port of that workflow with the direction
23+
# reversed: the spec is built from the checkout the run is in, the CLONE is the
24+
# downstream, and there is no artifact reuse -- the spec is always the one from
25+
# the tree under test.
26+
#
27+
# ## Its place in the two speeds (#22998)
28+
#
29+
# `schedule`, hourly on `main`, is this gate's place in the FULL BATTERY. A PR
30+
# or queue build carries the fast set; this job never runs on an ordinary pull
31+
# request (two-speed rule 1) and is NOT subscribed to `merge_group`. The one
32+
# `pull_request` trigger below is path-filtered to this gate's own three files,
33+
# so a change to the gate -- the workflow, the script, or a REGISTRATION in
34+
# the retirement registry -- is exercised before it merges and proves on its
35+
# own pull request that the registered diagnosis reads `known`; no other pull
36+
# request pays the roughly thirteen minutes of a downstream compile.
37+
#
38+
# `scripts/ci/scheduled-full-run.mjs --release-gate` reads this workflow's
39+
# newest `schedule` run at or before the version commit, beside `CI`'s and
40+
# `Lint & Type Check`'s, and the publish lane refuses unless all conclude
41+
# `success` (`RELEASE_GATE_WORKFLOWS` there is the registration). A run whose
42+
# every diagnostic is a registered retirement concludes `success`.
43+
#
44+
# ## Who owns a red
45+
#
46+
# ⭐ A red on the hourly run is a MAIN-RED under #22998 rule 3: it is the FIRST
47+
# WORK of the lane the report names. The report step attributes it to the
48+
# objectstack commits since this workflow's last green run on `main`, the pull
49+
# request each landed in, and the `domain:*` lane of the card that pull fixes;
50+
# a lane it cannot read is written `unattributed` -- a reading this gate could
51+
# not take, never a lane with no owner. An INTENTIONAL retirement is not a red
52+
# for long: the accepting seat files the downstream card and registers the
53+
# retirement in `scripts/ci/spec-shape-retirements.json` in the same act
54+
# (cross-repo rule 3), and the next run reads the matching diagnostics as
55+
# `known (registered: <card>)` and does not red. ⛔ The registry is one line per
56+
# retirement with a per-diagnostic pattern; it is not, and must never become,
57+
# an ignore-all switch -- `scripts/ci/spec-shape-gate.mjs` (THE REGISTRY)
58+
# refuses an entry that would be one.
59+
#
60+
# ⭐ WHAT A RED MUST SAY is the acceptance criterion, not decoration: which
61+
# objectui file fails against which objectstack commit, so that the objectstack
62+
# pull request which moved the shape is the one that answers. The script owns
63+
# that half -- it attributes every diagnostic to a repo-relative objectui path
64+
# paired with the objectstack sha, in the run summary and in annotations -- and
65+
# it refuses (exit 2, loudly) to report an unattributable failure as a shape
66+
# break or as a pass.
67+
#
68+
# ## Why the clone is objectui `main`, not the pin
69+
#
70+
# The question this gate asks is "does TODAY'S downstream compile against this
71+
# commit". `.objectui-sha` is the release artifact's pin and stays the Console
72+
# Pin Gate's question (ci.yml `console-pin`); the two gates are not the same
73+
# question and neither replaces the other.
74+
#
75+
# ## ⛔ The turbo cache is a FALSE GREEN here
76+
#
77+
# `turbo`'s hash covers objectui's sources, lockfile and a declared env list,
78+
# not the CONTENT of `node_modules`, which is the only thing this job changes,
79+
# so a cached `type-check` could replay a verdict taken against the PUBLISHED
80+
# spec. The typecheck runs with `TURBO_FORCE=true` and no turbo cache action.
81+
# `--continue` is part of the READING: without it turbo stops scheduling at the
82+
# first failed task and the log holds one package's diagnostics instead of
83+
# every package's.
84+
#
85+
# ## Two runner details that change what a reader sees
86+
#
87+
# `actions/setup-node` registers a `tsc` problem matcher; left on, the runner
88+
# parses the echoed typecheck log and mints a second annotation per diagnostic
89+
# with a path that does not resolve in this repository (measured on objectui's
90+
# own runs: ten extra annotations re-homed onto `.github`). The compile step
91+
# removes that matcher before echoing the log, so the only annotations are the
92+
# report's, each naming the objectui path and the objectstack sha in its title.
93+
# The exit code of the compile is captured with NO PIPE in between.
94+
#
95+
# The downstream's install runs its lifecycle scripts, as any consumer compile
96+
# must; the token this job holds is read-only (`permissions` below) and nothing
97+
# here writes through it.
98+
99+
on:
100+
# The full battery's hourly run on `main`. Off the hour on purpose -- ci.yml
101+
# and lint.yml take `:00`, and scheduled workflows queue behind everyone's
102+
# `:00` cron -- and on a minute no sibling here uses.
103+
schedule:
104+
- cron: '52 * * * *'
105+
# The same reading on demand: an operator who has just registered a
106+
# retirement or landed a fix should not wait an hour to see it answered.
107+
workflow_dispatch: {}
108+
# This gate's own files only. See "Its place in the two speeds" above.
109+
pull_request:
110+
paths:
111+
- '.github/workflows/spec-shape-gate.yml'
112+
- 'scripts/ci/spec-shape-gate.mjs'
113+
- 'scripts/ci/spec-shape-retirements.json'
114+
115+
# Least privilege, all reads: `contents` for the two checkouts, `actions` for
116+
# the report's lookup of this workflow's own earlier runs (the last green),
117+
# `pull-requests` and `issues` for the lane attribution (a pull's labels and
118+
# body, a card's labels). Nothing writes through the token.
119+
permissions:
120+
contents: read
121+
actions: read
122+
pull-requests: read
123+
issues: read
124+
125+
# `github.event_name` is in the key on purpose: a `schedule` run and a
126+
# `workflow_dispatch` run on `main` carry the same `github.ref`, and neither
127+
# may cancel the other -- the hourly run is the reading the release gate waits
128+
# on, so a cancellation there is a missing reading rather than a saved minute.
129+
# Superseded pull-request runs are cancelled; runs on `main` never are.
130+
concurrency:
131+
group: spec-shape-gate-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
132+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
133+
134+
jobs:
135+
shape-gate:
136+
name: Spec Shape Gate
137+
runs-on: ubuntu-latest
138+
# Sized against objectui's measured runs (about thirteen minutes, of which
139+
# the forced, uncached downstream build + typecheck is eleven) with room
140+
# for a cold runner; a ceiling this job can reach turns a verdict into
141+
# `cancelled`, which reports nothing a reader can act on.
142+
timeout-minutes: 45
143+
144+
steps:
145+
- name: Checkout objectstack (the commit under test)
146+
uses: actions/checkout@v7
147+
148+
# The downstream, at `main`, inside the workspace (actions/checkout
149+
# refuses a path outside it). objectstack's workspace globs are rooted at
150+
# `packages/`, `apps/` and `examples/`, so nothing under `objectui/` is
151+
# swept into this repository's install, and pnpm run inside `objectui/`
152+
# finds objectui's own `pnpm-workspace.yaml` first.
153+
- name: Checkout objectui main (the downstream under test)
154+
uses: actions/checkout@v7
155+
with:
156+
repository: objectstack-ai/objectui
157+
ref: main
158+
path: objectui
159+
fetch-depth: 1
160+
161+
# Resolved ONCE, into outputs every later step and the report quote, so
162+
# the sha in the summary is provably the sha that was built.
163+
- name: Resolve the two commits under test
164+
id: subjects
165+
run: |
166+
set -euo pipefail
167+
upstream=$(git rev-parse HEAD)
168+
downstream=$(git -C objectui rev-parse HEAD)
169+
subject=$(git log -1 --format=%s HEAD)
170+
committed=$(git log -1 --format=%cI HEAD)
171+
echo "upstream=$upstream" >> "$GITHUB_OUTPUT"
172+
echo "downstream=$downstream" >> "$GITHUB_OUTPUT"
173+
echo "commit=${upstream:0:12} -- $committed -- $subject" >> "$GITHUB_OUTPUT"
174+
echo "objectstack@$upstream ($committed) against objectui@$downstream"
175+
176+
- name: Setup Node.js
177+
uses: actions/setup-node@v7
178+
with:
179+
node-version: '22'
180+
181+
- name: Setup pnpm
182+
uses: ./.github/actions/setup-pnpm
183+
184+
- name: Get pnpm store directory
185+
shell: bash
186+
run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_ENV"
187+
188+
# One store for both installs (same pnpm, same store path), keyed on both
189+
# lockfiles. Restore here; the save below runs only on `main` runs, so an
190+
# hourly run seeds the next hour and a pull-request run spends nothing
191+
# from the cache pool. ci.yml's store entries are the cold fallback.
192+
- name: Restore the pnpm store
193+
id: store
194+
uses: actions/cache/restore@v6
195+
with:
196+
path: ${{ env.STORE_PATH }}
197+
key: ${{ runner.os }}-spec-shape-gate-pnpm-store-${{ hashFiles('pnpm-lock.yaml', 'objectui/pnpm-lock.yaml') }}
198+
restore-keys: |
199+
${{ runner.os }}-spec-shape-gate-pnpm-store-
200+
${{ runner.os }}-pnpm-store-v3-
201+
202+
# "A change to the gate runs the gate's self-test": offline, no install.
203+
- name: Self-test the gate script
204+
run: pnpm check:spec-shape-gate
205+
206+
# ── Build the spec from THIS commit, and PACK it ──────────────────────
207+
# `--filter @objectstack/spec...` installs the spec and its dependencies
208+
# only; `inject` later reads the dependencies this install resolved
209+
# (`packages/spec/node_modules`) to re-point a store sibling the packed
210+
# manifest's range refuses.
211+
- name: Install the spec's dependency closure
212+
run: pnpm install --frozen-lockfile --filter @objectstack/spec...
213+
214+
- name: Build @objectstack/spec from this commit
215+
run: pnpm --filter @objectstack/spec build
216+
217+
# What gets injected is what a RELEASE from this commit would ship -- the
218+
# `files` field's file set, not a working tree. `--ignore-scripts`
219+
# because the build above already ran.
220+
- name: Pack the built spec (the file set a release from this commit would ship)
221+
id: pack
222+
working-directory: packages/spec
223+
run: |
224+
set -euo pipefail
225+
mkdir -p "$RUNNER_TEMP/spec-built"
226+
packed=$(npm pack --ignore-scripts --pack-destination "$RUNNER_TEMP/spec-built" | tail -n 1)
227+
echo "tarball=$RUNNER_TEMP/spec-built/$packed" >> "$GITHUB_OUTPUT"
228+
ls -l "$RUNNER_TEMP/spec-built/$packed"
229+
230+
# ── The downstream's install ─────────────────────────────────────────
231+
# Corepack reads the nearest `packageManager` pin, so pnpm invoked inside
232+
# `objectui/` is objectui's pinned version (downloaded here, once, when
233+
# it differs from this repository's). A named step so a toolchain
234+
# download that fails is answerable from the checks list alone.
235+
- name: Materialise objectui's pinned pnpm
236+
working-directory: objectui
237+
run: pnpm --version
238+
239+
- name: Install objectui dependencies
240+
working-directory: objectui
241+
run: pnpm install --frozen-lockfile
242+
243+
- name: Save the pnpm store
244+
if: github.event_name != 'pull_request' && steps.store.outputs.cache-hit != 'true'
245+
uses: actions/cache/save@v6
246+
with:
247+
path: ${{ env.STORE_PATH }}
248+
key: ${{ runner.os }}-spec-shape-gate-pnpm-store-${{ hashFiles('pnpm-lock.yaml', 'objectui/pnpm-lock.yaml') }}
249+
250+
# Replaces the installed spec in objectui's pnpm virtual store -- REMOVE
251+
# then copy, because those files are hardlinks into the global store --
252+
# re-points a store sibling the packed manifest's range refuses at the
253+
# copy this tree's spec build resolved, and proves every workspace
254+
# consumer resolves the injected copy before returning. Nothing tracked
255+
# by git is touched in either checkout.
256+
- name: Inject the source-built spec into objectui's install
257+
env:
258+
UPSTREAM_SHA: ${{ steps.subjects.outputs.upstream }}
259+
TARBALL: ${{ steps.pack.outputs.tarball }}
260+
run: node scripts/ci/spec-shape-gate.mjs inject --tarball "$TARBALL" --sha "$UPSTREAM_SHA" --upstream-checkout "$GITHUB_WORKSPACE" --downstream "$GITHUB_WORKSPACE/objectui"
261+
262+
# ── The compile ──────────────────────────────────────────────────────
263+
# `pnpm type-check` is objectui's `turbo run type-check`, whose task
264+
# dependsOn `^build`: every objectui package is built against the
265+
# injected spec and then type-checked against those declarations. See the
266+
# header for `TURBO_FORCE`, `--continue`, the matcher and the no-pipe
267+
# capture. The step itself always exits 0; the verdict is the report's.
268+
- name: Type-check objectui against it
269+
id: compile
270+
working-directory: objectui
271+
run: |
272+
set +e
273+
echo "::remove-matcher owner=tsc::"
274+
TURBO_FORCE=true pnpm type-check --continue > "$RUNNER_TEMP/typecheck.log" 2>&1
275+
code=$?
276+
set -e
277+
echo "status=$code" >> "$GITHUB_OUTPUT"
278+
cat "$RUNNER_TEMP/typecheck.log"
279+
exit 0
280+
281+
# ALWAYS: this step IS the delivery. It reads the registry, writes the
282+
# summary (new diagnostics, known ones with their registration, the
283+
# commit range and lanes), emits one annotation per NEW diagnostic naming
284+
# the objectui file and the objectstack commit, and carries the verdict:
285+
# exit 0 green or known-only, exit 1 a new shape break, exit 2 a reading
286+
# it could not take (never a pass, never a shape break).
287+
- name: Report which objectui file fails against which objectstack commit, known or new, and whose lane
288+
if: always()
289+
env:
290+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
291+
UPSTREAM_SHA: ${{ steps.subjects.outputs.upstream }}
292+
DOWNSTREAM_SHA: ${{ steps.subjects.outputs.downstream }}
293+
UPSTREAM_COMMIT: ${{ steps.subjects.outputs.commit }}
294+
COMPILE_STATUS: ${{ steps.compile.outputs.status }}
295+
run: |
296+
node scripts/ci/spec-shape-gate.mjs report \
297+
--log "$RUNNER_TEMP/typecheck.log" \
298+
--sha "$UPSTREAM_SHA" \
299+
--downstream "$GITHUB_WORKSPACE/objectui" \
300+
--downstream-sha "$DOWNSTREAM_SHA" \
301+
--commit "$UPSTREAM_COMMIT" \
302+
--event "$GITHUB_EVENT_NAME" \
303+
--status "${COMPILE_STATUS:-2}"

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102
"check:pm-governed-prose": "node scripts/pm/check-governed-prose.mjs --self-test && node scripts/pm/check-governed-prose.mjs",
103103
"check:pm-settings-deny-roster": "node scripts/pm/check-settings-deny-roster.mjs --self-test && node scripts/pm/check-settings-deny-roster.mjs",
104104
"check:pm-expected-skips": "node scripts/pm/check-expected-skips.mjs --self-test",
105+
"check:spec-shape-gate": "node scripts/ci/spec-shape-gate.mjs --self-test",
105106
"check:publish-smoke-pin": "node scripts/publish-smoke-pack.mjs --self-test",
106107
"check:partof-closing-keyword": "node scripts/check-partof-closing-keyword.mjs --self-test",
107108
"check:commit-card-trailers": "node scripts/check-commit-card-trailers.mjs --self-test",

0 commit comments

Comments
 (0)