Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .changeset/console-pin-pr-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
---

ci: build the pinned objectui SHA on the PR that moves it (#4290)

`.objectui-sha` is the single source of truth for the vendored Console SPA —
release.yml reads it, clones objectui at that commit, builds
`@object-ui/console` and copies the dist into `packages/console/`. Editing that
one line replaces the entire frontend the platform ships, and nothing on the PR
side ever built it.

The pin appeared in exactly two workflows, neither a gate: release.yml
(post-merge, on the main push) and showcase-smoke.yml (manual + nightly, whose
own header says "it never gates PRs"). It is also a root dotfile, so it matched
neither of ci.yml's `core` and `docs` filters — #4288 moved the pin 76 commits
with six of fourteen checks skipped, including Build Core, Test Core, Build Docs
and the Dogfood gate. A SHA that cannot build — a typo, a commit that was
force-pushed away, a broken objectui, a dead bundle canary — reached `main`
green and would have surfaced at publish time. The only thing between the two
was the author remembering to run `scripts/build-console.sh` locally.

Adds ci.yml's `Console Pin Gate`: a `console` paths filter over `.objectui-sha`,
`scripts/build-console.sh` and the two files that gate them, and a job that
builds `@object-ui/console` at the pin against this tree's `@objectstack/client`
and then runs `pnpm check:console-sha`.

Two details that keep it honest rather than merely green:

- It restores `packages/console/dist` under **release.yml's existing cache key**
(`hashFiles('.objectui-sha', 'scripts/build-console.sh')`), so only a pin the
repo has never built misses — and a miss is exactly when the gate has work to
do. Watching the workflow and the drift guard therefore costs about a minute,
not a full vite build.
- The restore/save pair is **split**, where release.yml uses the combined
action, because the combined post-step saves even when the job failed.
`build-console.sh` writes the SHA stamp before it asserts the bundle canary,
so a canary failure leaves a stamped-but-broken dist that would be cached,
restored, and then waved through the stamp check. Saving only once every
assertion is green is what lets a restored dist carry the same guarantees as a
freshly built one. A separate step asserts a real dist is on disk first, since
`check:console-sha` deliberately exits 0 when there is no dist at all.

Scope, stated plainly: this proves the pinned SHA builds. It does not cover a
`packages/client` change breaking the injected-client bundle — that input lives
under `packages/**`, and watching it here would rebuild the console on a large
fraction of every PR. release.yml remains the only check for that direction.

CI and docs only; this releases nothing.
181 changes: 181 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
outputs:
docs: ${{ steps.changes.outputs.docs }}
core: ${{ steps.changes.outputs.core }}
console: ${{ steps.changes.outputs.console }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
Expand All @@ -46,6 +47,19 @@ jobs:
- 'pnpm-lock.yaml'
- 'tsconfig.json'
- '.github/workflows/ci.yml'
# Build inputs of the vendored Console SPA — see the Console Pin Gate
# job at the bottom of this file. `.objectui-sha` is a ROOT DOTFILE, so
# it matches neither filter above: a pin-only diff skipped `core` and
# `docs` alike, which is how #4288 moved the pin 76 commits with six of
# fourteen checks skipped and nothing anywhere building the new SHA.
# The last two entries are the "a change to the guard runs the guard"
# rule the repo applies to every other filtered gate; they are close to
# free here because an unmoved pin hits the dist cache.
console:
- '.objectui-sha'
- 'scripts/build-console.sh'
- 'scripts/check-console-sha.mjs'
- '.github/workflows/ci.yml'

test:
name: Test Core
Expand Down Expand Up @@ -696,3 +710,170 @@ jobs:

- name: Build Docs
run: pnpm --filter @objectstack/docs build

# ── The pinned objectui SHA actually builds (#4290) ───────────────────────
#
# `.objectui-sha` is the single source of truth for the vendored Console SPA:
# release.yml reads it, clones objectstack-ai/objectui at that commit, builds
# @object-ui/console, and copies the dist into packages/console/. Editing that
# one line replaces the entire frontend the platform ships.
#
# Nothing on the PR side ever built it. The pin appeared in exactly two
# workflows, neither of them a gate: release.yml (post-merge, on the main push)
# and showcase-smoke.yml (manual + nightly, whose own header says "it never
# gates PRs"). Combined with the filter hole noted above, #4288 merged a
# 76-commit pin bump green with six of fourteen checks skipped. A SHA that
# cannot build — a typo, a commit that was force-pushed away, a genuinely
# broken objectui, a dead bundle canary — reached main unchallenged and would
# have surfaced at PUBLISH time. The only thing standing between the two was
# the author remembering to run scripts/build-console.sh locally: discipline,
# not a gate.
#
# `pnpm objectui:refresh` does include the build, so the normal path was
# already covered in practice; the hole is a hand-edited pin, or that single
# line cherry-picked out of another branch.
#
# Cost is real but narrowly aimed. A moved pin is the only trigger that pays
# the full clone + vite build, and #4288's 76 commits of staleness are the
# measure of how rare that is. The other triggers (this file, the drift guard)
# leave the pin untouched, so they hit the dist cache and cost about a minute.
#
# Scope, stated plainly: this proves the PINNED SHA builds. It does NOT cover a
# packages/client change breaking the injected-client bundle — that input lives
# under `packages/**`, and watching it here would rebuild the console on a large
# fraction of every PR. release.yml stays the only check for that direction.
#
# The NAME is the required-check contract — the same trap the dogfood shards
# note above: renaming it silently drops the gate wherever branch protection
# lists it, with every PR still green.
console-pin:
name: Console Pin Gate
needs: filter
if: needs.filter.outputs.console == 'true'
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'

- name: Enable Corepack
run: corepack enable

- name: Verify pnpm version
run: pnpm --version

- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV

- name: Setup pnpm cache
uses: actions/cache@v6
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-v3-

# Restore-only, and with no save step at all: this job runs a handful of
# times a month, so its own namespace would almost always be cold. The
# build-core fallbacks below are the ones that actually hit — that job
# builds a superset of the client closure needed here and is seeded from
# main (same reasoning as the Temporal Conformance job's fallback).
- name: Restore Turbo cache
uses: actions/cache/restore@v6
with:
path: .turbo/cache
key: ${{ runner.os }}-turbo-${{ github.job }}-${{ github.ref_name }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-${{ github.job }}-${{ github.ref_name }}-
${{ runner.os }}-turbo-${{ github.job }}-
${{ runner.os }}-turbo-build-core-${{ github.ref_name }}-
${{ runner.os }}-turbo-build-core-

- name: Install dependencies
run: pnpm install --frozen-lockfile

# build-console.sh bundles THIS tree's @objectstack/client into the console
# (via OBJECTSTACK_CLIENT_DIST) and falls back to building it in place when
# the dist is missing — but that fallback is a bare `pnpm build` inside
# packages/client, which tsup cannot finish unless @objectstack/core and
# @objectstack/spec are built already. Build the closure through turbo
# instead: cacheable, and near-free off the restore above. Both of the
# script's existing callers (release.yml, showcase-smoke.yml) pre-build for
# the same reason.
- name: Build @objectstack/client and its dependencies
run: pnpm exec turbo run build --filter=@objectstack/client... --concurrency=4

# Same key as release.yml's "Cache vendored Console dist" step. Actions
# caches are repo-scoped, so a PR reads whatever main already built for this
# pin — that is what makes the non-pin triggers cheap. Only a pin the repo
# has never built misses, and a miss is exactly when this gate has work to do.
#
# Split restore/save where release.yml uses the combined action, because the
# combined form's post-step saves even when the job FAILED. build-console.sh
# writes the SHA stamp before it asserts the bundle canary, so a canary
# failure leaves a stamped-but-broken dist on disk — cache that and the next
# run restores it, skips the build, and sails through the stamp check. Saving
# only once every assertion below is green is what lets a restored dist carry
# the same guarantees as a freshly built one.
- name: Restore vendored Console dist (keyed on the objectui pin)
id: console-dist
uses: actions/cache/restore@v6
with:
path: packages/console/dist
key: ${{ runner.os }}-console-dist-${{ hashFiles('.objectui-sha', 'scripts/build-console.sh') }}

# The gate. Shallow-clones objectui at the pinned SHA, builds
# @object-ui/console against this tree's client, copies the dist into
# packages/console/, and asserts the bundle canary — every failure mode a
# bad pin can carry, surfaced on the PR instead of at publish time.
- name: Build the Console SPA at the pinned objectui SHA
if: steps.console-dist.outputs.cache-hit != 'true'
run: bash scripts/build-console.sh

# check:console-sha exits 0 — deliberately — when there is no dist at all
# (published installs and package-only CI legitimately have none), and warns
# without failing when a dist carries no stamp. Both are right for that
# script and both are VACUOUS here, where producing a dist is the entire
# point: an empty or partial cache restore would otherwise reach the drift
# check with nothing to check and report success. Prove one is really in
# place before letting that check speak for the job.
- name: Assert a real Console dist is in place
env:
CACHE_HIT: ${{ steps.console-dist.outputs.cache-hit }}
run: |
dist=packages/console/dist
if [ "$CACHE_HIT" = "true" ]; then origin="the dist cache restore"; else origin="scripts/build-console.sh"; fi
fail=0
[ -s "$dist/index.html" ] || { echo "::error::$dist/index.html is missing or empty"; fail=1; }
[ -n "$(ls -A "$dist/assets" 2>/dev/null || true)" ] || { echo "::error::$dist/assets is missing or empty"; fail=1; }
[ -s "$dist/.objectui-sha" ] || { echo "::error::$dist/.objectui-sha stamp is missing — check:console-sha would pass vacuously"; fail=1; }
if [ "$fail" -ne 0 ]; then
echo "::error::No usable Console dist after $origin."
exit 1
fi
echo "✓ Console dist present ($(du -sk "$dist" | awk '{print $1}') KB, via $origin) from objectui@$(cut -c1-12 "$dist/.objectui-sha")"

- name: Verify the Console dist stamp matches the pin
run: pnpm check:console-sha

# Reached only with every assertion above green (see the split-restore note).
# This deviates from the workflow's restore-only-on-PRs policy on purpose:
# that policy targets the ~5 turbo entries every push writes, whereas this
# key moves only when the pin does. A pin-bump PR writes one entry, and the
# rest of that PR's pushes reuse it instead of rebuilding the console again.
- name: Save vendored Console dist
if: steps.console-dist.outputs.cache-hit != 'true'
uses: actions/cache/save@v6
with:
path: packages/console/dist
key: ${{ runner.os }}-console-dist-${{ hashFiles('.objectui-sha', 'scripts/build-console.sh') }}
5 changes: 5 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ jobs:
# scratch on every main push (~3 min). Cache the finished dist keyed on
# exactly those inputs; the stamp check below still verifies whatever
# dist ends up in place, restored or fresh.
#
# ci.yml's Console Pin Gate (#4290) uses this exact key, so the pin bump's
# PR run and this job share one build — keep the two in step if either
# input set changes. That gate is now what proves the pin builds; by the
# time this job runs the SHA has already merged.
- name: Cache vendored Console dist (keyed on the objectui pin)
id: console-dist-cache
uses: actions/cache@v6
Expand Down
11 changes: 8 additions & 3 deletions packages/console/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,16 @@ keeps working.
## Updating

1. Run `scripts/bump-objectui.sh` (or `scripts/bump-objectui.sh <sha>`) at
the repo root to update `.objectui-sha`.
2. CI runs `scripts/build-console.sh` before publish, which clones
the repo root to update `.objectui-sha`. Prefer `pnpm objectui:refresh`,
which bumps *and* rebuilds in one step.
2. Opening the PR runs CI's **Console Pin Gate**, which clones objectui at
the new SHA, builds `@object-ui/console` against this tree's
`@objectstack/client`, and runs `pnpm check:console-sha`. A pin that
cannot build fails the PR rather than the release.
3. CI runs `scripts/build-console.sh` again before publish, which clones
objectui at the pinned SHA, builds `@object-ui/console`, and copies
`dist/` into this package.
3. `pnpm publish` ships it at the same version as every other package in
4. `pnpm publish` ships it at the same version as every other package in
the Changesets `fixed` group.

The `dist/` directory is **not** committed — it's a CI publish artifact
Expand Down
Loading