Skip to content

feat(types): NamedListView mirrors spec 18's list-view requiredPermissions — no client gate #3456

feat(types): NamedListView mirrors spec 18's list-view requiredPermissions — no client gate

feat(types): NamedListView mirrors spec 18's list-view requiredPermissions — no client gate #3456

name: Spec Main Shape Gate
# Compile THIS repository against `@objectstack/spec` built from objectstack
# `main`, and name the objectui file and the objectstack commit when it does not
# compile (objectui#9860).
#
# ## The ruling this executes
#
# The maintainer chose option C on 2026-09-18: the ONLY shape gate for
# `@objectstack/spec`'s public surface is a real consumer compiling against
# objectstack's current `main`. objectui is the first consumer. It supersedes the
# 2026-09-06 ruling that put a declaration-text snapshot in the platform
# repository; that snapshot is being reverted there, and nothing replaces it
# except this job. ⛔ The direction is not re-adjudicated here.
#
# ⭐ 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. A red saying only
# "typecheck failed" does not satisfy the ruling. `scripts/spec-main-shape-gate.mjs`
# owns that half — it attributes every diagnostic to a repo-relative objectui path
# and pairs it with the objectstack sha, in GitHub annotations and in the run
# summary, and it refuses (exit 2, loudly) to report an unattributable failure as
# a shape break.
#
# ## Why NO path filter, and why that is the opposite of an oversight
#
# This gate's subject is not the pull request's diff. It is the drift between two
# repositories, and that drift moves when NEITHER a file here nor this workflow
# changes. A `paths:` filter would make the gate report on exactly the pull
# requests whose author already knows about the spec, and skip the ones that find
# out the hard way. Unfiltered also keeps it requirable: a check that does not
# report on every pull request cannot be in a required set (objectui#3523 — a
# required check that never reports does not fail the pull request, it leaves it
# pending until the ruleset's 60-minute timeout).
#
# ## ADVISORY, not required: the full battery on `main`, a signal on a PR
#
# objectui#12240 executes the maintainer's ruling B on objectui#12214's open
# question 1 (ACCEPT 6123370050), the two-speed doctrine of objectstack#22998
# applied to this gate: the context leaves the merge queue's required set. It
# was the pull request's critical path, and most of its reds are objectstack
# drift rather than the pull request's own change (the measurement is on
# objectui#12214, not copied here). ⛔ NOTHING it verifies changed -- the
# bypass, the anti-false-green marker and the attribution below are as they
# were. What changed is who WAITS for the verdict:
#
# - `schedule`, hourly on `main`, is its place in the full battery.
# ⭐ A red on that run is a MAIN-RED for the `domain:devx` lane at objectui,
# under objectstack#22998's rule 3: it is that lane's first work, the
# lane's next pull request carries the fix or the pin bump, and no pull
# request of that lane arms the queue while the red stands.
# - `pull_request` posts the same verdict as an advisory check. A human reads
# it; nothing waits for it.
# - `merge_group` is NOT subscribed (objectui#12252), the other half of the
# same ruling: the context is not in the queue's required set, so a
# verdict on a queue build would be read by nobody while every merge group
# paid a runner for it. (Whether the ruleset says so is
# `scripts/check-required-check-set.mjs`'s reading, ⛔ not this comment's.)
# ⛔ Re-subscribe `merge_group` BEFORE re-enrolling the context, never
# after: a required context that never reports on a queue build stalls
# it at the ruleset's 60-minute timeout with nothing red to point at
# (objectui#3523 -- subscribe first, enrol second). `merge-queue-reporting`
# derives that floor from `REQUIRED_CONTEXTS`, and
# `scripts/__tests__/spec-main-shape-gate.test.ts` pins the absence, so
# re-enrolling edits both on purpose rather than by accident.
#
# ⚠️ Requiredness is repository SETTINGS (GitHub ruleset 11776024), which no
# file in this tree can set, so "advisory" is not a mechanism in this file.
# ⛔ Never add `continue-on-error` to make it look so: it would hide the verdict
# on the run and change nothing about who waits for it. The in-tree halves are
# `scripts/dependabot-merge-gate.mjs`, which classifies the name in
# `NOT_A_GATE` again (a Dependabot bump does not wait for it either), and
# `scripts/check-required-check-set.mjs`, which no longer watches it. Whether
# the ruleset still lists it is that script's reading, ⛔ not this comment's.
#
# ## The fetch shape: sparse, blobless, at a sha
#
# The pin discussion of 2026-09-18 excluded a full clone per run. Measured
# against this repository's CI on 2026-09-18 (three shapes, one box, curl's and
# git's own counters — the numbers are in objectui#9860's pull request, not
# copied here, because a figure in a comment is derived once and never again):
# the blobless + depth-1 + cone-sparse clone transfers the least by a wide
# margin, because a packed codeload tarball has no server-side path filter and
# still ships the whole repository. Each directory in the cone is there because
# a step below fails without it:
# - `packages/spec` is the subject.
# - `scripts`: the spec's own build script calls scripts from the objectstack
# repository root, so a cone without it fails in the build step.
# - `patches`: objectstack's root `pnpm-workspace.yaml` declares
# `patchedDependencies`, and pnpm reads and hashes EVERY patch file that map
# names on any install, filtered or not, so a cone without it fails in the
# install step before anything is built (objectui#11024). The cone holds the
# directory `pnpm patch-commit` writes to; a patch declared anywhere else
# needs its directory added here, because nothing derives the cone from
# that map yet.
#
# ## The spec is built only when objectstack `main` moved (objectui#12214)
#
# The two-speed doctrine (objectstack#22998) keeps this gate on every event,
# and adds an hourly `schedule` run on `main` -- the gate's place in the full
# battery. That scheduled run also PUBLISHES what it built: the packed spec,
# with the sha it was built at, as the artifact `objectstack-spec-<sha>`, kept
# one day. Every other run first looks for that artifact at the sha it just
# resolved and, when there is one, injects it instead of building it again:
# the clone, the spec's own install and everything after the pack run as
# before, so `inject` still reads the dependencies the spec resolves.
#
# Only an artifact from a `main` run of THIS repository is reused. A pull
# request (or a manual dispatch on a branch) runs the workflow file of its own
# branch, so an artifact it uploaded is a file anybody with a branch could have
# shaped; a run on `main` executes reviewed YAML. The lookup is a READ
# (`actions: read`); nothing here writes through the token, and the upload uses
# the runner's own artifact credential. A lookup that fails, a download that
# fails, or an artifact whose recorded sha is not the one resolved above all
# mean BUILD -- never a verdict about a spec this job did not hold.
#
# ⚠️ What this buys is the build and the pack, not the compile: the forced
# typecheck below is the bulk of this job and is deliberately never cached.
# The measured split and the hit rate are on objectui#12214's pull request,
# not copied here.
#
# ## ⛔ The turbo cache is a FALSE GREEN here
#
# `turbo`'s hash covers this repository's sources, its lockfile and a declared env
# list. It does NOT cover the content of `node_modules`, which is the only thing
# this job changes — so a cached `type-check` can replay a verdict taken against
# a DIFFERENT spec, including the published one. The typecheck below therefore
# runs with `TURBO_FORCE=true` and no cache action, and
# `scripts/__tests__/spec-main-shape-gate.test.ts` pins that it keeps doing so.
on:
pull_request:
branches: [main, develop]
workflow_dispatch: {}
# The full battery's hourly run on `main` (objectui#12214). Off the hour on
# purpose, and on a different minute from `ci.yml`'s.
schedule:
- cron: '48 * * * *'
# Least privilege: the job reads this repository, reads a public one over HTTPS,
# and writes nothing anywhere through the token. `actions: read` is the
# artifact lookup and download of the reused spec (objectui#12214); the
# scheduled upload goes through the runner's own artifact credential.
permissions:
contents: read
actions: read
concurrency:
group: spec-main-shape-gate-${{ github.ref }}
# Superseded pull-request runs are cancelled; a run on `main` (the hourly
# `schedule`, or a dispatch) never is. A cancelled check is not `success`, and
# the scheduled run is the one place this gate's verdict is waited on, so a
# cancellation there is a missing reading rather than a saved runner minute.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
shape-gate:
name: Spec Main Shape Gate
runs-on: ubuntu-latest
# Sized against measurements, not taste: the spec's own build and this
# repository's forced (uncached) build + typecheck each cost minutes, and the
# runner is slower than the box they were measured on. A ceiling this job can
# reach turns a verdict into `cancelled`, which reads as a red on a pull
# request or on `main` and reports nothing a reader can act on.
timeout-minutes: 45
steps:
- name: Checkout objectui
uses: actions/checkout@v7
# No history is read by this gate: it compares two trees, not two commits
# of one. Nothing here diffs against a merge base.
# ── The objectstack commit under test ────────────────────────────────
# Resolved ONCE, into an output every later step and the report quote, so
# the sha in the annotation is provably the sha that was built. Resolving
# it twice would let a push between the two steps produce a report about a
# commit this job never compiled.
- name: Resolve objectstack main
id: upstream
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
commit_json="$RUNNER_TEMP/objectstack-main.json"
curl -sS --fail-with-body \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/repos/objectstack-ai/objectstack/commits/main \
-o "$commit_json"
sha=$(jq -r '.sha' "$commit_json")
# A gate that could not take its reading is a failure, never a pass.
if [ -z "$sha" ] || [ "$sha" = 'null' ]; then
echo "::error::could not resolve objectstack-ai/objectstack@main — no sha in the API response. This job has no subject; it is red rather than green."
exit 1
fi
subject=$(jq -r '.commit.message' "$commit_json" | head -n 1)
authored=$(jq -r '.commit.committer.date' "$commit_json")
echo "sha=$sha" >> "$GITHUB_OUTPUT"
echo "commit=${sha:0:12} · $authored · $subject" >> "$GITHUB_OUTPUT"
echo "Compiling against objectstack-ai/objectstack@$sha ($authored)"
# ── Reuse the spec a `main` run already built at that sha ────────────
# See "The spec is built only when objectstack `main` moved" in the
# header. Any failure here is `hit=false`, i.e. BUILD.
- name: Look for the spec a main run already built at that commit
id: reuse
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
UPSTREAM_SHA: ${{ steps.upstream.outputs.sha }}
REPOSITORY_ID: ${{ github.repository_id }}
run: |
set -uo pipefail
listing="$RUNNER_TEMP/spec-artifacts.json"
if ! curl -sS --fail-with-body \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/$GITHUB_REPOSITORY/actions/artifacts?name=objectstack-spec-$UPSTREAM_SHA&per_page=30" \
-o "$listing"; then
echo "hit=false" >> "$GITHUB_OUTPUT"
echo "::notice::could not list artifacts; building the spec at $UPSTREAM_SHA"
exit 0
fi
run_id=$(jq -r --arg repo "$REPOSITORY_ID" '
[.artifacts[]
| select(.expired == false
and .workflow_run.head_branch == "main"
and ((.workflow_run.repository_id | tostring) == $repo))]
| sort_by(.created_at) | last | .workflow_run.id // empty' "$listing")
if [ -n "$run_id" ]; then
echo "hit=true" >> "$GITHUB_OUTPUT"
echo "run_id=$run_id" >> "$GITHUB_OUTPUT"
echo "Reusing the spec main run $run_id built at $UPSTREAM_SHA"
else
echo "hit=false" >> "$GITHUB_OUTPUT"
echo "No main run has built the spec at $UPSTREAM_SHA yet; building it here"
fi
- name: Download that spec
id: download
if: steps.reuse.outputs.hit == 'true'
continue-on-error: true
uses: actions/download-artifact@v8
with:
name: objectstack-spec-${{ steps.upstream.outputs.sha }}
path: ${{ runner.temp }}/spec-built
run-id: ${{ steps.reuse.outputs.run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Fetch packages/spec from that commit (sparse, blobless — never a full clone)
env:
UPSTREAM_SHA: ${{ steps.upstream.outputs.sha }}
run: |
set -euo pipefail
# `--filter=blob:none` fetches file contents lazily, `--depth=1` drops
# history, and the cone below decides which blobs are ever fetched at
# all. `scripts` is in the cone because the spec's build calls scripts
# from the objectstack repository root; `patches` because the install
# reads every patch file the root `pnpm-workspace.yaml` declares in
# `patchedDependencies` (the header says why each one is needed).
git clone --depth=1 --filter=blob:none --no-checkout --single-branch \
--branch main https://github.com/objectstack-ai/objectstack \
"$RUNNER_TEMP/objectstack"
cd "$RUNNER_TEMP/objectstack"
git sparse-checkout init --cone
git sparse-checkout set packages/spec scripts patches
# Check out the resolved SHA, not `main`: `main` may have moved between
# the step above and this one, and the report names the sha.
git fetch --depth=1 origin "$UPSTREAM_SHA"
git checkout --detach "$UPSTREAM_SHA"
echo "objectstack HEAD: $(git rev-parse HEAD)"
- name: Enable Corepack and download the pinned pnpm
run: bash scripts/ci-setup-pnpm.sh
- name: Verify pnpm version
run: pnpm --version
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22.x'
cache: 'pnpm'
# ── Build the spec from source, and PACK it ──────────────────────────
# `--filter @objectstack/spec...` installs the spec and its dependencies
# only; the rest of that workspace is not even in the checkout. The
# install runs on a reuse too: `inject` reads the dependencies the
# spec's build resolved out of this checkout (objectui#10229).
- name: Build @objectstack/spec from objectstack main
env:
REUSED: ${{ steps.download.outcome == 'success' }}
working-directory: ${{ runner.temp }}/objectstack
run: |
set -euo pipefail
pnpm install --frozen-lockfile --filter @objectstack/spec...
if [ "$REUSED" = 'true' ]; then
echo "Not building: a main run already built and packed the spec at this sha (objectui#12214)."
exit 0
fi
pnpm --filter @objectstack/spec build
# What gets injected is what a RELEASE from that commit would ship — the
# `files` field's file set, not a working tree. `--ignore-scripts` because
# the build above already ran; a lifecycle script re-running here would
# pack something the build step never produced.
#
# A reused spec is the tarball a `main` run packed, beside the sha it was
# packed at. Anything but exactly one tarball recorded at the sha resolved
# above is refused: compiling against it would report a verdict about a
# spec this job cannot name.
- name: Pack the built spec
id: pack
env:
REUSED: ${{ steps.download.outcome == 'success' }}
UPSTREAM_SHA: ${{ steps.upstream.outputs.sha }}
BUILT: ${{ runner.temp }}/spec-built
working-directory: ${{ runner.temp }}/objectstack/packages/spec
run: |
set -euo pipefail
if [ "$REUSED" = 'true' ]; then
recorded=$(cat "$BUILT/sha.txt" 2>&1 || true)
count=$(find "$BUILT" -maxdepth 1 -name '*.tgz' | wc -l)
if [ "$recorded" != "$UPSTREAM_SHA" ] || [ "$count" -ne 1 ]; then
echo "::error::the reused spec artifact records sha '$recorded' and holds $count tarball(s); expected exactly one at $UPSTREAM_SHA"
exit 1
fi
tarball=$(find "$BUILT" -maxdepth 1 -name '*.tgz')
else
mkdir -p "$BUILT"
packed=$(npm pack --ignore-scripts --pack-destination "$BUILT" | tail -n 1)
tarball="$BUILT/$packed"
echo "$UPSTREAM_SHA" > "$BUILT/sha.txt"
fi
echo "tarball=$tarball" >> "$GITHUB_OUTPUT"
ls -l "$tarball"
# Publish what this scheduled `main` run built, for every run that
# resolves the same objectstack sha in the next day. `main` only -- see
# the header for why no other event's artifact is ever reused. A failed
# upload costs the next run a build and says nothing about the code
# under test, so it cannot speak for the verdict.
- name: Publish the built spec for runs at the same objectstack commit
if: github.event_name == 'schedule' && steps.download.outcome != 'success'
continue-on-error: true
timeout-minutes: 5
uses: actions/upload-artifact@v7
with:
name: objectstack-spec-${{ steps.upstream.outputs.sha }}
path: ${{ runner.temp }}/spec-built
retention-days: 1
if-no-files-found: error
- name: Install objectui dependencies
run: pnpm install --frozen-lockfile
# Replaces the installed spec in pnpm's virtual store — REMOVE-then-copy,
# because those files are hardlinks into the global pnpm store that
# `actions/setup-node` caches, and writing through one would poison that
# cache for every later job. The script proves the injection reached every
# workspace consumer before returning; nothing tracked by git is touched,
# so the pin and the lockfile are exactly as committed.
#
# It is handed the objectstack checkout the spec was BUILT in
# (objectui#10229): a declared dependency whose installed store sibling
# does not satisfy the packed manifest's range (zod, when objectstack
# raises its floor) is re-pointed at the copy that build resolved, so the
# declarations are read against the dependency they were emitted against.
# Each substitution is printed here and in the run summary.
- name: Inject the source-built spec into the install
env:
UPSTREAM_SHA: ${{ steps.upstream.outputs.sha }}
TARBALL: ${{ steps.pack.outputs.tarball }}
UPSTREAM_CHECKOUT: ${{ runner.temp }}/objectstack
run: node scripts/spec-main-shape-gate.mjs inject --tarball "$TARBALL" --sha "$UPSTREAM_SHA" --upstream-checkout "$UPSTREAM_CHECKOUT"
# ── The compile ──────────────────────────────────────────────────────
# `pnpm type-check` is `turbo run type-check`, whose task dependsOn
# `^build`, so this builds every workspace package against the injected
# spec and then type-checks against those declarations. Both halves are the
# consumer compile this gate exists to be.
#
# The exit code is captured with NO PIPE in between: `node … | tail` reports
# the PIPE's status and `tail` essentially never fails, so a red and a green
# would read identically — the exact class of silent pass this repository
# has been bitten by before.
#
# ⛔ `--continue` IS THE READING, not a convenience. Without it turbo stops
# scheduling the moment one task fails, so the log this gate parses holds
# the diagnostics of the FIRST package that broke and nothing about the
# packages it never asked. That is a lower bound reported as a set — and
# the harm is not hypothetical while any single long-lived break exists in
# the graph: every run stops there, and every other package's drift against
# the spec stays invisible for as long as that one break is open. Measured
# on this repository, two packages broken on purpose, same command
# otherwise: without the flag turbo reported one of them and counted six
# tasks, with it both and eight. `--continue` still exits non-zero when a
# task failed, so the verdict above is unchanged — only its completeness is.
- name: Type-check objectui against it
id: compile
run: |
set +e
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 writes the summary, emits one
# annotation per diagnostic naming the objectstack commit, and carries the
# verdict — exit 1 for an attributed shape break, exit 2 for a failure it
# could not attribute to a file (which is not a pass and is not reported as
# a shape break either).
- name: Report which objectui file fails against which objectstack commit
if: always()
env:
UPSTREAM_SHA: ${{ steps.upstream.outputs.sha }}
UPSTREAM_COMMIT: ${{ steps.upstream.outputs.commit }}
COMPILE_STATUS: ${{ steps.compile.outputs.status }}
run: |
node scripts/spec-main-shape-gate.mjs report \
--log "$RUNNER_TEMP/typecheck.log" \
--sha "$UPSTREAM_SHA" \
--commit "$UPSTREAM_COMMIT" \
--status "${COMPILE_STATUS:-2}"