Skip to content

ci(spec-shape-gate): drop the closed objectui#12325 retirement entry and point the header at the deleted objectui copy’s last commit #55

ci(spec-shape-gate): drop the closed objectui#12325 retirement entry and point the header at the deleted objectui copy’s last commit

ci(spec-shape-gate): drop the closed objectui#12325 retirement entry and point the header at the deleted objectui copy’s last commit #55

Workflow file for this run

name: Spec Shape Gate
# Compile the downstream consumer -- objectstack-ai/objectui at `main` -- against
# `@objectstack/spec` built from THIS commit, and name the objectui file, the
# objectstack commit and the owning lane when it does not compile.
#
# ## The ruling this executes
#
# Ruling B on the decision card for the spec shape gate (#23238, comment
# 6145029395, batch 336 item 3, 2026-10-13): the gate MOVES 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
# once two consecutive hourly runs here agree with objectui's. The MECHANISM of
# ruling C on #19011 (consumer compilation as the ONE shape gate for the spec's
# public surface) is kept unchanged; what moved is where it runs and who owns a
# red. ⛔ Neither direction is re-adjudicated here.
#
# The objectui half -- `.github/workflows/spec-main-shape-gate.yml` there, its
# script `scripts/spec-main-shape-gate.mjs` and its three pins -- is the
# objectui `domain:devx` lane's card, filed once two consecutive hourly runs of
# THIS workflow agree with objectui's hourly run; both run in parallel until
# then. This file is the objectstack port of that workflow with the direction
# reversed: the spec is built from the checkout the run is in, the CLONE is the
# downstream, and there is no artifact reuse -- the spec is always the one from
# the tree under test.
#
# ## Its place in the two speeds (#22998)
#
# `schedule`, hourly on `main`, is this gate's place in the FULL BATTERY. A PR
# or queue build carries the fast set; this job never runs on an ordinary pull
# request (two-speed rule 1) and is NOT subscribed to `merge_group`. The one
# `pull_request` trigger below is path-filtered to this gate's own three files,
# so a change to the gate -- the workflow, the script, or a REGISTRATION in
# the retirement registry -- is exercised before it merges and proves on its
# own pull request that the registered diagnosis reads `known`; no other pull
# request pays the roughly thirteen minutes of a downstream compile.
#
# `scripts/ci/scheduled-full-run.mjs --release-gate` reads this workflow's
# newest `schedule` run at or before the version commit, beside `CI`'s and
# `Lint & Type Check`'s, and the publish lane refuses unless all conclude
# `success` (`RELEASE_GATE_WORKFLOWS` there is the registration). A run whose
# every diagnostic is a registered retirement concludes `success`.
#
# ## Who owns a red
#
# ⭐ A red on the hourly run is a MAIN-RED under #22998 rule 3: it is the FIRST
# WORK of the lane the report names. The report step attributes it to the
# objectstack commits since this workflow's last green run on `main`, the pull
# request each landed in, and the `domain:*` lane of the card that pull fixes;
# a lane it cannot read is written `unattributed` -- a reading this gate could
# not take, never a lane with no owner. An INTENTIONAL retirement is not a red
# for long: the accepting seat files the downstream card and registers the
# retirement in `scripts/ci/spec-shape-retirements.json` in the same act
# (cross-repo rule 3), and the next run reads the matching diagnostics as
# `known (registered: <card>)` and does not red. ⛔ The registry is one line per
# retirement with a per-diagnostic pattern; it is not, and must never become,
# an ignore-all switch -- `scripts/ci/spec-shape-gate.mjs` (THE REGISTRY)
# refuses an entry that would be one.
#
# ⭐ WHAT A RED MUST SAY is the acceptance criterion, not decoration: which
# objectui file fails against which objectstack commit, so that the objectstack
# pull request which moved the shape is the one that answers. The script owns
# that half -- it attributes every diagnostic to a repo-relative objectui path
# paired with the objectstack sha, in the run summary and in annotations -- and
# it refuses (exit 2, loudly) to report an unattributable failure as a shape
# break or as a pass.
#
# ## Why the clone is objectui `main`, not the pin
#
# The question this gate asks is "does TODAY'S downstream compile against this
# commit". `.objectui-sha` is the release artifact's pin and stays the Console
# Pin Gate's question (ci.yml `console-pin`); the two gates are not the same
# question and neither replaces the other.
#
# ## ⛔ The turbo cache is a FALSE GREEN here
#
# `turbo`'s hash covers objectui's sources, lockfile and a declared env list,
# not the CONTENT of `node_modules`, which is the only thing this job changes,
# so a cached `type-check` could replay a verdict taken against the PUBLISHED
# spec. The typecheck runs with `TURBO_FORCE=true` and no turbo cache action.
# `--continue` is part of the READING: without it turbo stops scheduling at the
# first failed task and the log holds one package's diagnostics instead of
# every package's.
#
# ## Two runner details that change what a reader sees
#
# `actions/setup-node` registers a `tsc` problem matcher; left on, the runner
# parses the echoed typecheck log and mints a second annotation per diagnostic
# with a path that does not resolve in this repository (measured on objectui's
# own runs: ten extra annotations re-homed onto `.github`). The compile step
# removes that matcher before echoing the log, so the only annotations are the
# report's, each naming the objectui path and the objectstack sha in its title.
# The exit code of the compile is captured with NO PIPE in between.
#
# The downstream's install runs its lifecycle scripts, as any consumer compile
# must; the token this job holds is read-only (`permissions` below) and nothing
# here writes through it.
on:
# The full battery's hourly run on `main`. Off the hour on purpose -- ci.yml
# and lint.yml take `:00`, and scheduled workflows queue behind everyone's
# `:00` cron -- and on a minute no sibling here uses.
schedule:
- cron: '52 * * * *'
# The same reading on demand: an operator who has just registered a
# retirement or landed a fix should not wait an hour to see it answered.
workflow_dispatch: {}
# This gate's own files only. See "Its place in the two speeds" above.
pull_request:
paths:
- '.github/workflows/spec-shape-gate.yml'
- 'scripts/ci/spec-shape-gate.mjs'
- 'scripts/ci/spec-shape-retirements.json'
# Least privilege, all reads: `contents` for the two checkouts, `actions` for
# the report's lookup of this workflow's own earlier runs (the last green),
# `pull-requests` and `issues` for the lane attribution (a pull's labels and
# body, a card's labels). Nothing writes through the token.
permissions:
contents: read
actions: read
pull-requests: read
issues: read
# `github.event_name` is in the key on purpose: a `schedule` run and a
# `workflow_dispatch` run on `main` carry the same `github.ref`, and neither
# may cancel the other -- the hourly run is the reading the release gate waits
# on, so a cancellation there is a missing reading rather than a saved minute.
# Superseded pull-request runs are cancelled; runs on `main` never are.
concurrency:
group: spec-shape-gate-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
shape-gate:
name: Spec Shape Gate
runs-on: ubuntu-latest
# Sized against objectui's measured runs (about thirteen minutes, of which
# the forced, uncached downstream build + typecheck is eleven) with room
# for a cold runner; a ceiling this job can reach turns a verdict into
# `cancelled`, which reports nothing a reader can act on.
timeout-minutes: 45
steps:
- name: Checkout objectstack (the commit under test)
uses: actions/checkout@v7
# The downstream, at `main`, inside the workspace (actions/checkout
# refuses a path outside it). objectstack's workspace globs are rooted at
# `packages/`, `apps/` and `examples/`, so nothing under `objectui/` is
# swept into this repository's install, and pnpm run inside `objectui/`
# finds objectui's own `pnpm-workspace.yaml` first.
- name: Checkout objectui main (the downstream under test)
uses: actions/checkout@v7
with:
repository: objectstack-ai/objectui
ref: main
path: objectui
fetch-depth: 1
# Resolved ONCE, into outputs every later step and the report quote, so
# the sha in the summary is provably the sha that was built.
- name: Resolve the two commits under test
id: subjects
run: |
set -euo pipefail
upstream=$(git rev-parse HEAD)
downstream=$(git -C objectui rev-parse HEAD)
subject=$(git log -1 --format=%s HEAD)
committed=$(git log -1 --format=%cI HEAD)
echo "upstream=$upstream" >> "$GITHUB_OUTPUT"
echo "downstream=$downstream" >> "$GITHUB_OUTPUT"
echo "commit=${upstream:0:12} -- $committed -- $subject" >> "$GITHUB_OUTPUT"
echo "objectstack@$upstream ($committed) against objectui@$downstream"
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
- name: Setup pnpm
uses: ./.github/actions/setup-pnpm
- name: Get pnpm store directory
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_ENV"
# One store for both installs (same pnpm, same store path), keyed on both
# lockfiles. Restore here; the save below runs only on `main` runs, so an
# hourly run seeds the next hour and a pull-request run spends nothing
# from the cache pool. ci.yml's store entries are the cold fallback.
- name: Restore the pnpm store
id: store
uses: actions/cache/restore@v6
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-spec-shape-gate-pnpm-store-${{ hashFiles('pnpm-lock.yaml', 'objectui/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-spec-shape-gate-pnpm-store-
${{ runner.os }}-pnpm-store-v3-
# "A change to the gate runs the gate's self-test": offline, no install.
- name: Self-test the gate script
run: pnpm check:spec-shape-gate
# ── Build the spec from THIS commit, and PACK it ──────────────────────
# `--filter @objectstack/spec...` installs the spec and its dependencies
# only; `inject` later reads the dependencies this install resolved
# (`packages/spec/node_modules`) to re-point a store sibling the packed
# manifest's range refuses.
- name: Install the spec's dependency closure
run: pnpm install --frozen-lockfile --filter @objectstack/spec...
- name: Build @objectstack/spec from this commit
run: pnpm --filter @objectstack/spec build
# What gets injected is what a RELEASE from this commit would ship -- the
# `files` field's file set, not a working tree. `--ignore-scripts`
# because the build above already ran.
- name: Pack the built spec (the file set a release from this commit would ship)
id: pack
working-directory: packages/spec
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/spec-built"
packed=$(npm pack --ignore-scripts --pack-destination "$RUNNER_TEMP/spec-built" | tail -n 1)
echo "tarball=$RUNNER_TEMP/spec-built/$packed" >> "$GITHUB_OUTPUT"
ls -l "$RUNNER_TEMP/spec-built/$packed"
# ── The downstream's install ─────────────────────────────────────────
# Corepack reads the nearest `packageManager` pin, so pnpm invoked inside
# `objectui/` is objectui's pinned version (downloaded here, once, when
# it differs from this repository's). A named step so a toolchain
# download that fails is answerable from the checks list alone.
- name: Materialise objectui's pinned pnpm
working-directory: objectui
run: pnpm --version
- name: Install objectui dependencies
working-directory: objectui
run: pnpm install --frozen-lockfile
- name: Save the pnpm store
if: github.event_name != 'pull_request' && steps.store.outputs.cache-hit != 'true'
uses: actions/cache/save@v6
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-spec-shape-gate-pnpm-store-${{ hashFiles('pnpm-lock.yaml', 'objectui/pnpm-lock.yaml') }}
# Replaces the installed spec in objectui's pnpm virtual store -- REMOVE
# then copy, because those files are hardlinks into the global store --
# re-points a store sibling the packed manifest's range refuses at the
# copy this tree's spec build resolved, and proves every workspace
# consumer resolves the injected copy before returning. Nothing tracked
# by git is touched in either checkout.
- name: Inject the source-built spec into objectui's install
env:
UPSTREAM_SHA: ${{ steps.subjects.outputs.upstream }}
TARBALL: ${{ steps.pack.outputs.tarball }}
run: node scripts/ci/spec-shape-gate.mjs inject --tarball "$TARBALL" --sha "$UPSTREAM_SHA" --upstream-checkout "$GITHUB_WORKSPACE" --downstream "$GITHUB_WORKSPACE/objectui"
# ── The compile ──────────────────────────────────────────────────────
# `pnpm type-check` is objectui's `turbo run type-check`, whose task
# dependsOn `^build`: every objectui package is built against the
# injected spec and then type-checked against those declarations. See the
# header for `TURBO_FORCE`, `--continue`, the matcher and the no-pipe
# capture. The step itself always exits 0; the verdict is the report's.
- name: Type-check objectui against it
id: compile
working-directory: objectui
run: |
set +e
echo "::remove-matcher owner=tsc::"
TURBO_FORCE=true pnpm type-check --continue > "$RUNNER_TEMP/typecheck.log" 2>&1
code=$?
set -e
echo "status=$code" >> "$GITHUB_OUTPUT"
cat "$RUNNER_TEMP/typecheck.log"
exit 0
# ALWAYS: this step IS the delivery. It reads the registry, writes the
# summary (new diagnostics, known ones with their registration, the
# commit range and lanes), emits one annotation per NEW diagnostic naming
# the objectui file and the objectstack commit, and carries the verdict:
# exit 0 green or known-only, exit 1 a new shape break, exit 2 a reading
# it could not take (never a pass, never a shape break).
- name: Report which objectui file fails against which objectstack commit, known or new, and whose lane
if: always()
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
UPSTREAM_SHA: ${{ steps.subjects.outputs.upstream }}
DOWNSTREAM_SHA: ${{ steps.subjects.outputs.downstream }}
UPSTREAM_COMMIT: ${{ steps.subjects.outputs.commit }}
COMPILE_STATUS: ${{ steps.compile.outputs.status }}
run: |
node scripts/ci/spec-shape-gate.mjs report \
--log "$RUNNER_TEMP/typecheck.log" \
--sha "$UPSTREAM_SHA" \
--downstream "$GITHUB_WORKSPACE/objectui" \
--downstream-sha "$DOWNSTREAM_SHA" \
--commit "$UPSTREAM_COMMIT" \
--event "$GITHUB_EVENT_NAME" \
--status "${COMPILE_STATUS:-2}"