From 7a48258d547b06549eafdf8d82b9980bbe11eaf2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 10:02:24 +0000 Subject: [PATCH 1/4] test(ui-live-e2e): boot the workspace backend instead of a published pin The live lane's start-backend.sh now runs this tree's own built @objectstack/cli on the checked-out examples/app-showcase: HEAD console against HEAD backend. backend.env declares the app directory and no published version; the tag resolve, npm install and vitest override retire with the pin. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01QSeKYoUcDM1MUzjdaCjt6U --- ui/e2e/live/ci/backend.env | 68 +++-------- ui/e2e/live/ci/start-backend.sh | 199 +++++++++----------------------- 2 files changed, 69 insertions(+), 198 deletions(-) diff --git a/ui/e2e/live/ci/backend.env b/ui/e2e/live/ci/backend.env index 835df5666aa..af22996d31a 100644 --- a/ui/e2e/live/ci/backend.env +++ b/ui/e2e/live/ci/backend.env @@ -1,55 +1,17 @@ -# Pins for the CI live-e2e backend (see start-backend.sh and -# .github/workflows/live-e2e.yml). +# What the live-e2e backend serves (see start-backend.sh and the `Live E2E UI` +# job in .github/workflows/ci.yml). # -# OBJECTSTACK_VERSION — the published @objectstack/* npm version the backend -# runs on. Until objectui moved into the objectstack repository it had to -# match the @objectstack/spec version this repo's pnpm-lock.yaml resolved, -# and objectui's CI-guide pin test (deleted by objectstack#26058) required the two equal -# (objectui#7689). -# Since the import (objectstack#25803) the console builds against the -# workspace's own @objectstack/spec, a lockfile link with no version, so -# that comparison is retired and this value is the published backend the -# lane runs, nothing more. +# LIVE_BACKEND_APP — the app directory, relative to the repository root, that +# start-backend.sh boots with this tree's own built `@objectstack/cli`. The +# live specs target the showcase's objects and seeded records. # -# This file declares exactly ONE @objectstack pin, and that is the whole -# design: the commit is derived from it, see below. -OBJECTSTACK_VERSION=18.2.0 - -# There is deliberately no OBJECTSTACK_REF key here. The objectstack-ai/objectstack -# commit the showcase app metadata is sparse-checked-out from is DERIVED at -# boot by start-backend.sh, from the release tag -# `@objectstack/cli@${OBJECTSTACK_VERSION}` — `git ls-remote --tags`, taking -# the peeled `^{}` sha of the annotated tag — and the script refuses to start -# when that tag does not resolve to a 40-character sha. The app source and the -# published packages it runs on therefore come from the same release BY -# CONSTRUCTION; there is no second value to move, so the two cannot disagree. -# (Mirror image of the framework repo's `.objectui-sha` console pin.) -# -# It used to be a hand-moved sha here, carrying a stated MUST — "always the -# commit the release tag points at" — that nothing could check, on the -# reasoning that resolving the tag needs the objectstack repository and the -# unit lane cannot reach it. True of the unit lane, and beside the point: the -# live lane clones that very repository at boot to get the app, so the lane -# that CONSUMES the value could always resolve it. objectui#7964 retired the -# pin rather than add a check inside an `informational` lane nobody reads. -# -# ⚠️ One consequence, stated rather than papered over. The workflow caches the -# prepared fixture under `live-backend--`, and the -# resolved sha is no longer in this file. A version bump still moves the key, -# because the version is what the sha is derived from. But if the -# `@objectstack/cli@${OBJECTSTACK_VERSION}` tag were ever re-pointed at another -# commit after a fixture had been cached, that stale fixture would still be -# served until OBJECTSTACK_VERSION itself moved. start-backend.sh's own stamp -# carries the RESOLVED sha, so a re-point is caught in a reused scratch dir; -# the Actions cache is the one path it is not caught on. Published release tags -# are not expected to move, and moving one is the anomaly, not this lane. - -# There is no `better-auth` pin here any more. BETTER_AUTH_VERSION and -# e2e/live/ci/better-auth-pin.mjs held the family at 1.7.2 while the published -# @objectstack/plugin-auth imported an export 1.7.3 removed (objectstack#16186, -# objectui#8084). They were retired together in the 17.5.0 bump, as their own -# text prescribed (objectui#11073, maintainer ruling on objectui#11111): -# plugin-auth 17.5.0 declares the whole family at exactly 1.7.3 and no longer -# imports that export, so the published artifact's own manifest is the pin. -# Holding 1.7.2 on it made the lane test a pair nobody ships, and the seeded -# sign-in never answered. +# There is no published-version pin here any more (objectstack#27154, ruling +# 6464191239 letter D step 2). Until then this file declared +# OBJECTSTACK_VERSION, the published `@objectstack/*` release the backend +# ran, and start-backend.sh derived the showcase commit from that release's +# tag. A HEAD console against a published backend goes red each time the +# console passes the last release (#25914: 0/5 runs green on 18.1.0 against +# the 18.2.0 tree), so a required lane on a pin is a lockstep bump on every +# release. The lane now runs HEAD against HEAD, and the pin, the tag resolve +# and the npm install retired together. +LIVE_BACKEND_APP=examples/app-showcase diff --git a/ui/e2e/live/ci/start-backend.sh b/ui/e2e/live/ci/start-backend.sh index 8f54de6cf71..d0019002db8 100755 --- a/ui/e2e/live/ci/start-backend.sh +++ b/ui/e2e/live/ci/start-backend.sh @@ -1,27 +1,29 @@ #!/usr/bin/env bash -# Boot a real ObjectStack backend for the live e2e lane — from PUBLISHED -# @objectstack/* packages, serving the showcase app the live specs target. +# Boot a real ObjectStack backend for the live e2e lane — the WORKSPACE +# backend: this tree's own built `@objectstack/cli` serving this tree's +# `examples/app-showcase` (LIVE_BACKEND_APP in backend.env), so the lane runs +# the HEAD console against the HEAD backend (objectstack#27154). # -# What it does (idempotent; a stamp file skips prepare when pins are unchanged): -# 1. Resolve the `@objectstack/cli@$OBJECTSTACK_VERSION` release tag to a -# commit, and sparse-checkout `examples/app-showcase` from -# objectstack-ai/objectstack at it. The commit is DERIVED, never pinned: -# see the note above OBJECTSTACK_VERSION in backend.env (objectui#7964). -# 2. Rewrite its package.json: every `@objectstack/*` workspace dep -> the -# pinned published OBJECTSTACK_VERSION; dev-only tooling dropped. -# 3. `npm install` (published tarballs only — nothing is built from source, -# so the lane tests the console against the released artifacts). The -# published manifests decide what installs, which is the pair this lane -# exists to test, with ONE exception: the root `overrides.vitest` that -# npm 10 needs to install the published graph at all (see VITEST_OVERRIDE -# below for why, and for when it goes). -# 4. `objectstack dev --seed-admin --fresh` on $LIVE_BACKEND_PORT with a -# throwaway sqlite db, then poll the seeded sign-in until it answers. +# What it does: +# 1. Check the prerequisite: the workspace is installed and the CLI is built. +# Nothing is built here; the caller builds, as the `Live E2E UI` job does: +# pnpm exec turbo run build --filter=@objectstack/cli... \ +# --filter=@objectstack/example-showcase... +# A missing build refuses by name instead of booting into a stack trace. +# 2. `objectstack dev --seed-admin --fresh` from the checked-out app on +# $LIVE_BACKEND_PORT with a throwaway sqlite db, then poll the seeded +# sign-in until it answers. +# +# Until #27154 this script booted PUBLISHED packages: it resolved the +# `@objectstack/cli@$OBJECTSTACK_VERSION` release tag, sparse-checked-out the +# showcase at it and `npm install`ed the release on top. backend.env says why +# that pin retired; the tag resolve, the install and its vitest override went +# with it. # # Env overrides: -# LIVE_BACKEND_DIR scratch dir (default: $TMPDIR/objectui-live-backend) +# LIVE_BACKEND_DIR scratch dir for the pid file and the log +# (default: $TMPDIR/objectui-live-backend) # LIVE_BACKEND_PORT backend port (default: 4010) -# OBJECTSTACK_REPO_URL metadata source repo (default: GitHub) # # Companion: stop-backend.sh (kills only the PID this script recorded). set -euo pipefail @@ -30,135 +32,42 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=backend.env source "$SCRIPT_DIR/backend.env" +# ui/e2e/live/ci -> the repository root, four levels up. +REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" BACKEND_DIR="${LIVE_BACKEND_DIR:-${TMPDIR:-/tmp}/objectui-live-backend}" PORT="${LIVE_BACKEND_PORT:-4010}" -REPO_URL="${OBJECTSTACK_REPO_URL:-https://github.com/objectstack-ai/objectstack.git}" -APP_DIR="$BACKEND_DIR/app" -STAMP="$BACKEND_DIR/.prepared" - -# The one transitive override the install writes, as a root `overrides` entry in -# the rewritten manifest (objectui#12649). Without it, npm 10 (the npm Node 22 -# ships, and so the npm this lane's runner installs with) cannot install the -# published @objectstack/cli graph: it dies building the ideal tree with -# `Cannot read properties of null (reading 'edgesOut')`. The graph reaches -# `vitest` only as an OPTIONAL peer (at 18.0.0-next.2: `better-auth`'s, under -# plugin-auth, and `@objectstack/metadata-core`'s). The newest vitest 4 peers -# `@vitest/browser-webdriverio ^4.1.11`, whose only 4.x match peers back on an -# older vitest exactly, and npm 10's peer-set walk crashes on that set instead -# of resolving it. Holding vitest at 4.0.x gives it a coherent set to resolve. -# Nothing installs vitest, so the override steers the resolver and changes no -# package the backend runs: measured once, at 18.0.0-next.2, the tree npm 10 -# installs with it was the tree npm 11 installs without it, path for path and -# version for version — a one-off reading that nothing here re-derives. -# It is the remedy objectstack documents for this crash (its troubleshooting -# entry "Cannot read properties of null (reading 'edgesOut') from npm install"). -# The value is part of the stamp below, so a fixture prepared under another -# recipe — the Actions cache restores one without asking — is rebuilt, never -# reused: the cold install cannot hide behind a cache hit. -# ⛔ Remove it (this line, the `overrides` line in prepare(), and the stamp's -# third component) once the published `@objectstack/cli` at OBJECTSTACK_VERSION -# installs on npm 10 with no override. Re-measure on every pin bump: drop the -# `overrides` line, run this script under npm 10 with a FRESH LIVE_BACKEND_DIR -# and a fresh npm cache, and read the install's exit. A re-run in a prepared dir -# proves nothing, because an install from its package-lock.json never crashes. -VITEST_OVERRIDE='~4.0.0' - -# The showcase-app commit is DERIVED here, and is declared nowhere: it is -# whatever the `@objectstack/cli@$OBJECTSTACK_VERSION` release tag points at, so -# the metadata checked out below and the published packages installed on top of -# it cannot come from different trees. backend.env used to carry it as a second, -# hand-moved pin whose stated MUST ("always the commit the release tag points -# at") nothing could check — and a pair that can disagree eventually does -# (objectui#7964, objectui#7689 for the version half). -# -# Annotated tags resolve in two lines: the tag object, then the peeled `^{}` -# commit. Take the peeled sha when present, else the tag's own (lightweight tags -# do not peel). Both patterns are passed explicitly — asking for the tag alone -# does NOT return its peeled line. -OBJECTSTACK_TAG="@objectstack/cli@$OBJECTSTACK_VERSION" -# GIT_TERMINAL_PROMPT=0 so an unreachable/private remote fails in seconds with -# the message below instead of blocking the job on a credential prompt. -if ! TAG_LINES="$(GIT_TERMINAL_PROMPT=0 git ls-remote --tags "$REPO_URL" \ - "refs/tags/$OBJECTSTACK_TAG" "refs/tags/$OBJECTSTACK_TAG^{}" 2>&1)"; then - echo "[live-backend] cannot reach $REPO_URL to resolve the release tag" >&2 - echo "[live-backend] $TAG_LINES" >&2 - exit 1 -fi -OBJECTSTACK_REF="$(printf '%s\n' "$TAG_LINES" | awk -v t="refs/tags/$OBJECTSTACK_TAG" ' - $2 == t "^{}" { peeled = $1 } - $2 == t { plain = $1 } - END { print (peeled != "" ? peeled : plain) } -')" -# A missing tag is not an error to `git ls-remote` — it prints nothing and exits -# 0 — so the shape check below is the actual guard, not a formality. Refuse -# loudly and by name: the alternative is `git fetch --depth 1 origin ""` failing -# 300 seconds downstream, in a log nobody reads until the job goes red. -if [[ ! "$OBJECTSTACK_REF" =~ ^[0-9a-f]{40}$ ]]; then - echo "[live-backend] release tag '$OBJECTSTACK_TAG' does not resolve to a commit in" >&2 - echo "[live-backend] $REPO_URL" >&2 - echo "[live-backend] OBJECTSTACK_VERSION=$OBJECTSTACK_VERSION (backend.env) names a version" >&2 - echo "[live-backend] with no published release tag, or the tag scheme moved. Refusing to" >&2 - echo "[live-backend] start: there is no showcase-app commit matching that release." >&2 - exit 1 -fi -echo "[live-backend] resolved $OBJECTSTACK_TAG -> $OBJECTSTACK_REF" - -# The RESOLVED sha, not the version, is in the stamp: without it, a fixture -# prepared before a pin change satisfies the reuse test and the new pin is never -# installed — and a re-pointed tag is exactly such a change with no key of its -# own to hash. The workflow's fixture cache key is the hash of backend.env, -# which carries the version for the same reason; see the ⚠️ note there for the -# one path that key can no longer see. The third component is the install -# recipe's one override: that key hashes backend.env only, so a recipe change -# made in this script restores the old fixture on a hit, and this stamp is what -# rejects it. (The retired `better-auth` pin used to sit in the same slot.) -WANT_STAMP="$OBJECTSTACK_REF $OBJECTSTACK_VERSION vitest@$VITEST_OVERRIDE" +APP_DIR="$REPO_ROOT/$LIVE_BACKEND_APP" +CLI_BIN="$APP_DIR/node_modules/.bin/objectstack" mkdir -p "$BACKEND_DIR" prepare() { - if [ -f "$STAMP" ] && [ "$(cat "$STAMP")" = "$WANT_STAMP" ] && [ -d "$APP_DIR/node_modules" ]; then - echo "[live-backend] prepare: pins unchanged ($WANT_STAMP), reusing $APP_DIR" - return + local missing=0 + if [ ! -f "$APP_DIR/objectstack.config.ts" ]; then + echo "[live-backend] missing: $LIVE_BACKEND_APP/objectstack.config.ts (LIVE_BACKEND_APP in backend.env)" >&2 + missing=1 fi - echo "[live-backend] prepare: showcase@${OBJECTSTACK_REF:0:12} (from $OBJECTSTACK_TAG) on published @objectstack/*@$OBJECTSTACK_VERSION" - rm -rf "$APP_DIR" "$BACKEND_DIR/src" "$STAMP" - - # Shallow, sparse fetch of the resolved commit — metadata source only. - git init -q "$BACKEND_DIR/src" - git -C "$BACKEND_DIR/src" remote add origin "$REPO_URL" - git -C "$BACKEND_DIR/src" sparse-checkout set examples/app-showcase - git -C "$BACKEND_DIR/src" fetch -q --depth 1 origin "$OBJECTSTACK_REF" - git -C "$BACKEND_DIR/src" checkout -q FETCH_HEAD - - cp -R "$BACKEND_DIR/src/examples/app-showcase" "$APP_DIR" - rm -rf "$BACKEND_DIR/src" - - # workspace:* -> the pinned published version; drop dev tooling the server - # doesn't need (playwright/vitest/typescript — the CLI loads the TS config - # itself). Keep non-@objectstack runtime deps (e.g. @modelcontextprotocol/sdk). - # The only `overrides` entry written is VITEST_OVERRIDE (see its note above); - # otherwise the published @objectstack/* manifests decide their own - # transitive versions. - node -e ' - const fs = require("fs"); - const file = process.argv[1], pin = process.argv[2], vitest = process.argv[3]; - const pkg = JSON.parse(fs.readFileSync(file, "utf8")); - for (const k of Object.keys(pkg.dependencies || {})) - if (k.startsWith("@objectstack/")) pkg.dependencies[k] = pin; - pkg.devDependencies = { "@objectstack/cli": pin }; - pkg.overrides = { ...pkg.overrides, vitest }; - fs.writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n"); - ' "$APP_DIR/package.json" "$OBJECTSTACK_VERSION" "$VITEST_OVERRIDE" - - (cd "$APP_DIR" && npm install --no-audit --no-fund --loglevel=error) - echo "$WANT_STAMP" > "$STAMP" + if [ ! -x "$CLI_BIN" ]; then + echo "[live-backend] missing: $LIVE_BACKEND_APP/node_modules/.bin/objectstack (the workspace is not installed)" >&2 + missing=1 + fi + if [ ! -f "$REPO_ROOT/packages/cli/dist/index.js" ]; then + echo "[live-backend] missing: packages/cli/dist/index.js (the CLI is not built)" >&2 + missing=1 + fi + if [ "$missing" -ne 0 ]; then + echo "[live-backend] the workspace backend is not ready. From $REPO_ROOT:" >&2 + echo "[live-backend] pnpm install --frozen-lockfile" >&2 + echo "[live-backend] pnpm exec turbo run build --filter=@objectstack/cli... --filter=@objectstack/example-showcase..." >&2 + exit 1 + fi + echo "[live-backend] prepare: workspace @objectstack/cli $(node -p 'require(process.argv[1]).version' "$REPO_ROOT/packages/cli/package.json") on $LIVE_BACKEND_APP at $(git -C "$REPO_ROOT" rev-parse --short HEAD 2>/dev/null || echo 'an unknown commit')" } start() { # Trust a recorded pid only if it is alive AND is actually an objectstack - # process — the pid file can ride in from the CI cache, where the raw number - # may collide with an unrelated process on a fresh runner. + # process — a stale pid file's raw number may by now name an unrelated + # process. if [ -f "$BACKEND_DIR/backend.pid" ]; then local old_pid old_pid="$(cat "$BACKEND_DIR/backend.pid")" @@ -172,15 +81,15 @@ start() { # --fresh: throwaway sqlite db, auto-removed on SIGTERM. OS_CLOUD_URL=off: # no marketplace/cloud calls — the lane must be hermetic. # The `cd` runs first and only the `nohup …` command goes to the background, - # so `$!` is the `objectstack dev` process itself (nohup and the bin's - # `env node` shebang both exec in place); its SIGTERM takes down the `serve` - # child that holds the port. Backgrounding `cd … && nohup …` as one list - # forks a subshell and records ITS pid instead: stop-backend.sh then kills - # that subshell and the server keeps the port, and the `already running` - # check above never matches, because the subshell's cmdline is this script's - # (objectui#12676, measured there at 18.0.0-next.2; nothing here re-derives). + # so `$!` is the `objectstack dev` process itself (nohup and pnpm's bin shim, + # which ends in `exec node …/packages/cli/bin/run.js`, both exec in place); + # its SIGTERM takes down the `serve` child that holds the port. Backgrounding + # `cd … && nohup …` as one list forks a subshell and records ITS pid instead: + # stop-backend.sh then kills that subshell and the server keeps the port, and + # the `already running` check above never matches, because the subshell's + # cmdline is this script's (objectui#12676). (cd "$APP_DIR" - OS_CLOUD_URL=off nohup ./node_modules/.bin/objectstack dev \ + OS_CLOUD_URL=off nohup "$CLI_BIN" dev \ --seed-admin --fresh -p "$PORT" > "$BACKEND_DIR/backend.log" 2>&1 & echo $! > "$BACKEND_DIR/backend.pid") From f69a33b256b8584ce022537e733874695ed06298 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 10:06:51 +0000 Subject: [PATCH 2/4] ci: run the live e2e lane as the Live E2E UI job and enrol it as a required context A ci.yml job builds the workspace backend's closure and the Console (VITE_BASE_PATH=/), boots the backend through start-backend.sh, serves vite preview with the /api proxy and runs test:e2e:live:ci with --retries 0, gated as Build & E2E UI is. Its name joins REQUIRED_CONTEXTS and the two instruction surfaces that state the set; the Chromium-provisioning pin reads the new job too. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01QSeKYoUcDM1MUzjdaCjt6U --- .../references/platform-readings.md | 4 +- .github/workflows/ci.yml | 173 ++++++++++++++++++ AGENTS.md | 8 +- scripts/check-required-contexts.mjs | 21 ++- .../__tests__/ensure-chromium-ready.test.ts | 18 +- ui/scripts/__tests__/helpers/root-ci.ts | 7 + 6 files changed, 213 insertions(+), 18 deletions(-) diff --git a/.claude/skills/pm-dispatch/references/platform-readings.md b/.claude/skills/pm-dispatch/references/platform-readings.md index 57720275aec..cd8dffbdbf7 100644 --- a/.claude/skills/pm-dispatch/references/platform-readings.md +++ b/.claude/skills/pm-dispatch/references/platform-readings.md @@ -385,9 +385,9 @@ - 强制档不因临缺授权降档:临缺非退役,退役唯维护者裁;不达档起隔离达档子代理或等档。 - 本车道强制多是过宽的回忆:`dispatch-gates.mjs --tier PATH` 逐路径现推,路径线是下限非放行。 - 该脚本只答自己那棵树:姊妹仓路径回 absent from this tree,姊妹仓的档位与条款②只能手推。 -- required checks 的名单是每仓事实,objectstack 九个: +- required checks 的名单是每仓事实,objectstack 十个: `TypeScript Type Check` · `Lint & Repo Gates` · `Test Core` · `Build Core` · `Lint UI` · - `Governed Surface Queue Guard` · `Type Check UI` · `Test UI` · `Build & E2E UI`。 + `Governed Surface Queue Guard` · `Type Check UI` · `Test UI` · `Build & E2E UI` · `Live E2E UI`。 - ⭐ 本表的 `mustName` 声明 `statesTheSet` ⇒ 必须逐字等于注册表,加注册行不跟改本表即红。 - ⇒ 门绿即名单逐字齐全的读数;⛔ 但数量词未钉:实测 七→六 照绿,只能手跟改。 - ⛔ 聚合命令同样不作判定:`check:type-check-debt` 可在包级 typecheck 绿时红。 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1f78bc250d..ef31d101a36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4448,6 +4448,179 @@ jobs: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} + # ── Live E2E UI: the Console against a real backend (#27154) ───────────── + # Row 17 of the objectstack#25697 workflow roster, objectui's `live-e2e.yml`, + # "kept, follow-up port": it has run nowhere since #26058 deleted + # `ui/.github/workflows/`. Ruling 6435339772 Q1 A made it required; ruling + # 6464191239 letter D re-based it on the WORKSPACE backend, so it runs the + # HEAD Console against the HEAD backend and no published version is pinned + # anywhere (`ui/e2e/live/ci/backend.env` says why the pin retired). + # + # What it runs: the four allowlisted live specs, `test:e2e:live:ci`, with + # `--retries 0` (the config's own value, stated again on the command line), + # against `objectstack dev --seed-admin --fresh` serving `examples/app-showcase` + # from this tree's own built CLI (`ui/e2e/live/ci/start-backend.sh`) and the + # Console's production build served by `vite preview`, whose `/api` proxy + # (the Console's `server.proxy`, which preview inherits) points at that + # backend through DEV_PROXY_TARGET. ⛔ No retry wrapper and no + # `continue-on-error` anywhere in this job: a red run is a red check. + # + # Gated as Build & E2E UI is. Not behind Build UI Deps: this job builds a + # larger closure (the CLI and the showcase app's) than the seven packages that + # job hands on, so it restores Build Core's turbo seed instead. + # + # Ceiling: there is no CI population yet, so it is derived from one cold + # local run on 4 shared cores (#27154, 2026-10-28), by the rule above Lint UI: + # install 60 s + closure build 384 s + Console build 119 s + backend boot + # 19 s + specs 26 s + browser provisioning and setup ~120 s ≈ 730 s + # (12.2 min) => max(36.5, 27.2) => 40. Re-derive from the first population. + live-e2e-ui: + name: Live E2E UI + needs: filter + if: ${{ !cancelled() && needs.filter.outputs.ui_tree != 'false' && (needs.filter.outputs.ui != 'false' || needs.filter.outputs.ui_consumers != 'false') }} + runs-on: ubuntu-latest + timeout-minutes: 40 + permissions: + contents: read + env: + # The Console's build OOMs at Node's default heap (see Build & E2E UI). + NODE_OPTIONS: --max-old-space-size=4096 + LIVE_BACKEND_DIR: ${{ runner.temp }}/live-backend + LIVE_BACKEND_PORT: '4010' + LIVE_API_URL: http://localhost:4010 + LIVE_APP_URL: http://localhost:5190 + + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version-file: .nvmrc + + - 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 + + # Restore only, here and below: Build UI Deps saves the store and Build + # Core the turbo seed. + - name: Restore pnpm cache + uses: actions/cache/restore@v6 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store-v3- + + - name: Restore Turbo cache + uses: actions/cache/restore@v6 + with: + path: .turbo/cache + key: ${{ runner.os }}-turbo-build-core-${{ github.ref_name }}-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-turbo-build-core-${{ github.ref_name }}- + ${{ runner.os }}-turbo-build-core- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + # The backend's closure (the CLI and the showcase app, the `pkg...` form + # Dogfood Verify CLI uses) plus the seven packages the Console builds + # against, the same filters as Build & E2E UI's build step. + - name: Build the workspace backend and the @objectstack packages objectui consumes + run: | + pnpm exec turbo run build --concurrency=4 \ + --filter=@objectstack/cli... \ + --filter=@objectstack/example-showcase... \ + --filter=@objectstack/spec... \ + --filter=@objectstack/client... \ + --filter=@objectstack/core... \ + --filter=@objectstack/formula... \ + --filter=@objectstack/lint... \ + --filter=@objectstack/sdui-parser... \ + --filter=@objectstack/types... + + # Served at the root, where the live specs navigate. + - name: Build the Console for the live lane + env: + VITE_BASE_PATH: / + run: pnpm --dir ui/apps/console exec vite build + + - name: Get the Playwright version + id: playwright-version + run: | + set -eo pipefail + if ! version=$(pnpm --dir ui list @playwright/test --depth=0 --json | jq -r '.[0].devDependencies["@playwright/test"].version'); then + echo "::error::Reading the @playwright/test version failed (pnpm list --json | jq). Refusing to write a Playwright browser cache key from it." + exit 1 + fi + if [ -z "$version" ] || [ "$version" = "null" ]; then + echo "::error::Could not resolve the @playwright/test version (got: '${version}'). Refusing to write an empty or null version into the Playwright browser cache key." + exit 1 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + + # Build & E2E UI saves this key; this job only reads it. + - name: Restore Playwright browsers + id: playwright-cache + uses: actions/cache/restore@v6 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} + + - name: Install Playwright browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: pnpm --dir ui exec playwright install chromium + + - name: Ensure Chromium can launch + working-directory: ui/scripts + run: bash ensure-chromium-ready.sh + + - name: Boot the workspace backend + run: bash ui/e2e/live/ci/start-backend.sh + + # In the background for the rest of the job; the runner reaps it at job + # end. Ready = the Console's index answers through the preview server. + - name: Serve the Console with vite preview + env: + DEV_PROXY_TARGET: http://localhost:4010 + run: | + cd ui/apps/console + nohup pnpm exec vite preview --port 5190 --strictPort > "$RUNNER_TEMP/vite-preview.log" 2>&1 & + deadline=$((SECONDS + 60)) + until curl -sf -o /dev/null http://localhost:5190/; do + if [ "$SECONDS" -ge "$deadline" ]; then + echo "::error::vite preview did not answer on :5190 within 60 s." + cat "$RUNNER_TEMP/vite-preview.log" + exit 1 + fi + sleep 1 + done + echo "vite preview answers on :5190, proxying /api to :4010." + + - name: Run the live e2e specs + run: pnpm --dir ui run test:e2e:live:ci --retries 0 + + - name: Stop the workspace backend + if: ${{ always() }} + run: bash ui/e2e/live/ci/stop-backend.sh + + - name: Upload failure artefacts + if: ${{ !cancelled() && failure() }} + uses: actions/upload-artifact@v7 + with: + name: ui-live-e2e-failure-artifacts + path: | + ui/test-results/ + ${{ runner.temp }}/live-backend/backend.log + ${{ runner.temp }}/vite-preview.log + retention-days: 14 + # ── UI Published Artifacts: two objectui gates over a full build (#26717) ─ # Rows 23 and 21 of the objectstack#25697 workflow roster, "kept, follow-up # port" (objectui's published-dist-gate.yml and node-esm-load-gate.yml), both diff --git a/AGENTS.md b/AGENTS.md index ba46ab56e77..5c74d236103 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -503,11 +503,11 @@ Even inside your own worktree, operate defensively: yet"; `in_progress` is not a pass, and arming a red PR hides it ([why](docs/agents/multi-agent-discipline.md#arming-a-red-pr-hides-it)). Always read *two* things: the queue branch **and** `origin/main`. And **the queue enforces only the required - set** — nine contexts block: `Lint & Repo Gates` (all `check:*` gates), `TypeScript Type Check`, - `Test Core`, `Build Core`, `Governed Surface Queue Guard`, `Lint UI`, `Type Check UI`, `Test UI` - and `Build & E2E UI`; a check outside those nine is advisory + set** — ten contexts block: `Lint & Repo Gates` (all `check:*` gates), `TypeScript Type Check`, + `Test Core`, `Build Core`, `Governed Surface Queue Guard`, `Lint UI`, `Type Check UI`, `Test UI`, + `Build & E2E UI` and `Live E2E UI`; a check outside those ten is advisory ([why that matters](docs/agents/multi-agent-discipline.md#advisory-checks-ride-through)). A required context is - matched by check-run name, so a rename detaches its gate silently — treat those nine names as contract. + matched by check-run name, so a rename detaches its gate silently — treat those ten names as contract. **Two speeds** — PR and queue runs carry the fast set; `main`'s scheduled and push runs carry the full battery ([detail](docs/agents/multi-agent-discipline.md#two-speeds-which-runs-carry-which-battery)). **Main-red:** a full-battery red is its lane's first work — triage files it under diff --git a/scripts/check-required-contexts.mjs b/scripts/check-required-contexts.mjs index d8f43759a4d..8ddd39bdbdf 100644 --- a/scripts/check-required-contexts.mjs +++ b/scripts/check-required-contexts.mjs @@ -480,6 +480,16 @@ export const REQUIRED_CONTEXTS = [ 'after the ui flake reading 6436579765 (0/50); the ruleset entry is the maintainer\'s, in the same sitting as the merge (#9325)', carries: 'the sharded `ui/` vitest matrix, `Test UI (k/16)`, read through the aggregate job', }, + { + workflow: 'ci.yml', + job: 'live-e2e-ui', + context: 'Live E2E UI', + authorized: + '#25914 ruling 6435339772 Q1 A (the required check), re-based by ruling 6464191239 letter D on the workspace backend, ' + + 'maintainer 「其他同意」 2026-10-28T08:22Z — this row is the repo half (#27154); the ruleset entry is the maintainer\'s, ' + + 'in the same sitting as the merge (#9325)', + carries: 'the Console against a real workspace backend: the allowlisted `ui/e2e/live` specs', + }, ]; /** @@ -595,9 +605,10 @@ export const INSTRUCTION_SURFACES = [ // the judge. Then 7→5 by #22998 (two-speed CI: the dogfood and temporal // contexts moved to the full battery). Then 5→7 by #25853 (the `ui/` // import's `Lint UI` and `Build & E2E UI`), 7→8 with `Type Check UI` - // once bill item 49 closed (#25856), and 8→9 with the `Test UI` aggregate - // (#26460). The full set is pinned here so the corrected sentence cannot - // rot back — a rename in ANY of the nine reddens this gate instead. + // once bill item 49 closed (#25856), 8→9 with the `Test UI` aggregate + // (#26460), and 9→10 with `Live E2E UI` (#27154). The full set is pinned + // here so the corrected sentence cannot rot back — a rename in ANY of the + // ten reddens this gate instead. file: 'AGENTS.md', statesTheSet: true, mustName: [ @@ -610,6 +621,7 @@ export const INSTRUCTION_SURFACES = [ 'Build & E2E UI', 'Type Check UI', 'Test UI', + 'Live E2E UI', ], }, { @@ -642,6 +654,7 @@ export const INSTRUCTION_SURFACES = [ 'Build & E2E UI', 'Type Check UI', 'Test UI', + 'Live E2E UI', ], }, { @@ -663,7 +676,7 @@ export const INSTRUCTION_SURFACES = [ * of the six surfaces have one; `AGENTS.md` does not, because a repo-root FILE * has no separator to be found by — so an AGENTS.md card derived this gate not * at all, while that surface is the one carrying `mustName` for every - * required context (widened 2→6 by #9677, 6→7 by #17798, 7→5 by #22998, 5→7 by #25853, 7→8, 8→9 by #26460). Editing the merge-queue + * required context (widened 2→6 by #9677, 6→7 by #17798, 7→5 by #22998, 5→7 by #25853, 7→8, 8→9 by #26460, 9→10 by #27154). Editing the merge-queue * paragraph there is precisely how this gate goes red, and it was reachable * only by judgment. * diff --git a/ui/scripts/__tests__/ensure-chromium-ready.test.ts b/ui/scripts/__tests__/ensure-chromium-ready.test.ts index d16e7ae9a8c..bbf7ce5a6bd 100644 --- a/ui/scripts/__tests__/ensure-chromium-ready.test.ts +++ b/ui/scripts/__tests__/ensure-chromium-ready.test.ts @@ -5,7 +5,7 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { jobBlock, rootCi, UI_JOBS } from './helpers/root-ci'; +import { jobBlock, rootCi, UI_JOBS, UI_LIVE_E2E_JOB } from './helpers/root-ci'; /** * objectui#5304 — `Build & E2E` hung inside `apt-get update` and was cancelled @@ -241,14 +241,16 @@ describe('ensure-chromium-ready.sh — the bound holds where GNU `timeout` is ab describe('the workflows actually use it', () => { /** - * The job that provisions Chromium: `build-e2e-ui` in objectstack's root - * ci.yml, objectui's `Build & E2E` (objectstack#25697). The ci.yml under - * `ui/.github/` runs nowhere (objectstack#25712). objectui's second caller, - * `live-e2e.yml`, left this list with it: it is row 17 of the - * objectstack#25697 workflow roster, "kept, follow-up port", and comes back - * here with that port. + * The jobs that provision Chromium: `build-e2e-ui` in objectstack's root + * ci.yml, objectui's `Build & E2E` (objectstack#25697), and `live-e2e-ui`, + * objectui's `live-e2e.yml` (row 17 of the objectstack#25697 workflow + * roster, "kept, follow-up port"), back with that port (objectstack#27154). + * The ci.yml under `ui/.github/` runs nowhere (objectstack#25712). */ - const workflows = [{ file: 'ci.yml `build-e2e-ui`', read: () => jobBlock(rootCi(), UI_JOBS.buildE2e) }]; + const workflows = [ + { file: 'ci.yml `build-e2e-ui`', read: () => jobBlock(rootCi(), UI_JOBS.buildE2e) }, + { file: 'ci.yml `live-e2e-ui`', read: () => jobBlock(rootCi(), UI_LIVE_E2E_JOB) }, + ]; /** * Whole-line comments removed. Required, not cosmetic: objectui's workflows diff --git a/ui/scripts/__tests__/helpers/root-ci.ts b/ui/scripts/__tests__/helpers/root-ci.ts index f4475a8390d..924e5b8228a 100644 --- a/ui/scripts/__tests__/helpers/root-ci.ts +++ b/ui/scripts/__tests__/helpers/root-ci.ts @@ -33,6 +33,13 @@ export const UI_JOBS = { buildE2e: 'build-e2e-ui', } as const; +/** + * objectui's live-e2e lane, `live-e2e.yml`, ported to the root as its own job + * on the workspace backend (objectstack#27154). Not one of `UI_JOBS`: it runs + * none of the lint, type-check or vitest gates those pins hold the four to. + */ +export const UI_LIVE_E2E_JOB = 'live-e2e-ui'; + /** * objectui's coverage lane, ported to the root as a nightly workflow * (objectstack#25924): the file, the sharded job and the merge job that From 9264ac22590020b57c9b92462c59288b4990b68d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 10:57:48 +0000 Subject: [PATCH 3/4] ci(live-e2e): set LIVE_BACKEND_DIR on the steps, not the job The runner context is not available in a job-level env:, so ci.yml did not validate and the CI workflow never started. The two steps that run the backend scripts now carry LIVE_BACKEND_DIR themselves. actionlint reports nothing on the result and the runner-context error on the previous head. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01QSeKYoUcDM1MUzjdaCjt6U --- .github/workflows/ci.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef31d101a36..f7ff0c63d1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4485,7 +4485,9 @@ jobs: env: # The Console's build OOMs at Node's default heap (see Build & E2E UI). NODE_OPTIONS: --max-old-space-size=4096 - LIVE_BACKEND_DIR: ${{ runner.temp }}/live-backend + # LIVE_BACKEND_DIR is set on the two steps that run the scripts: the + # `runner` context is not available in a job-level `env:`, and a + # workflow that names it there does not validate at all. LIVE_BACKEND_PORT: '4010' LIVE_API_URL: http://localhost:4010 LIVE_APP_URL: http://localhost:5190 @@ -4582,6 +4584,8 @@ jobs: run: bash ensure-chromium-ready.sh - name: Boot the workspace backend + env: + LIVE_BACKEND_DIR: ${{ runner.temp }}/live-backend run: bash ui/e2e/live/ci/start-backend.sh # In the background for the rest of the job; the runner reaps it at job @@ -4608,6 +4612,8 @@ jobs: - name: Stop the workspace backend if: ${{ always() }} + env: + LIVE_BACKEND_DIR: ${{ runner.temp }}/live-backend run: bash ui/e2e/live/ci/stop-backend.sh - name: Upload failure artefacts From 3f0b396fae04325ab7894a7100cfe73cdc302df0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 11:12:15 +0000 Subject: [PATCH 4/4] docs(ci): count ten required contexts and five ui jobs after Live E2E UI The advisory-checks section of multi-agent-discipline.md, which AGENTS.md links to, still said nine; the filter job's comment said four ui jobs read its outputs. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01QSeKYoUcDM1MUzjdaCjt6U --- .github/workflows/ci.yml | 5 +++-- docs/agents/multi-agent-discipline.md | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7ff0c63d1f..8469c6b9113 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -162,7 +162,8 @@ jobs: # filter below and the `test` job's `if:` (#9829, #10015). crosspkg: ${{ github.event_name != 'merge_group' && steps.changes.outputs.crosspkg || 'true' }} # The objectui tree under `ui/` (objectstack#25677). Three outputs, read - # together by the four `* UI` jobs at the bottom of this file: + # together by the five `* UI` jobs at the bottom of this file (Type Check + # UI, Test UI, Lint UI, Build & E2E UI and Live E2E UI): # ui — a path under ui/, a root file the ui jobs build from # (the lockfile, the workspace, turbo.json), or a root # the ui gates read (every root workflow, the prompts, @@ -175,7 +176,7 @@ jobs: # On merge_group `ui` and `ui_consumers` come from the queue diff # (objectstack#25867): an entry whose merged diff against # `merge_group.base_sha` touches no ui path and none of the seven - # packages skips the four ui jobs, exactly as the same diff does on its + # packages skips the five ui jobs, exactly as the same diff does on its # PR head; an entry that touches one runs them. If the step fails (the # base cannot be fetched, the diff errors), it is `continue-on-error` # there, its outputs are empty and `|| 'true'` runs every ui job. diff --git a/docs/agents/multi-agent-discipline.md b/docs/agents/multi-agent-discipline.md index 95e47e7d8f7..8f39ad43c6f 100644 --- a/docs/agents/multi-agent-discipline.md +++ b/docs/agents/multi-agent-discipline.md @@ -123,7 +123,7 @@ Arming a red PR does not queue it, it hides it: ← [`AGENTS.md` › Multi-agent working discipline](../../AGENTS.md#multi-agent-working-discipline) -A check outside those nine is advisory and rides through, and an advisory red +A check outside those ten is advisory and rides through, and an advisory red that lands rides `main`'s merge ref into every later PR until stanched. ## Two speeds: which runs carry which battery