Repository navigation
feat(types): NamedListView mirrors spec 18's list-view requiredPermissions — no client gate #3456
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}" |