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
39 changes: 39 additions & 0 deletions .changeset/live-e2e-lane.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
---

ci: the live e2e suite finally runs in CI — against a real, published backend

Release-nothing: adds `.github/workflows/live-e2e.yml`, `e2e/live/ci/*`, one
root script, and no package code.

`e2e/live/**` holds 20+ Playwright specs covering the interaction-critical
paths, and none of them ran in CI — they need a real ObjectStack backend and no
job provided one. That is how framework#3528 shipped: a lazily-loaded widget's
suspension tore down its own flow dialog, a failure only a real browser against
a real backend can see. The specs existed, passed when a human remembered to
run them, and guarded nothing.

The new lane boots `objectstack dev` from PUBLISHED `@objectstack/*` packages
(pinned in `e2e/live/ci/backend.env`, matched to the `@objectstack/spec`
version in `pnpm-lock.yaml`) serving the showcase app checked out at the
commit its release tag points to — so every PR smoke-tests "this console x the
released backend" as a matched pair, with nothing built from framework source.
The showcase metadata pin mirrors the framework repo's `.objectui-sha` console
pin, in the opposite direction.

Two deliberate limits, per the maintainer's ruling on #2835:

- **Informational, non-required**: `continue-on-error: true` keeps the lane
out of the merge gate until it has proven stable (objectstack#4850 is the
prior art for a new lane's flake ejecting unrelated PRs from the queue).
- **Allowlist start**: only `screen-flow`, `action-modal` and `master-detail`
run (`pnpm test:e2e:live:ci`) — grow the list a few proven specs at a time
instead of switching all 20+ on and inheriting whatever flake exists.

Also removes ci.yml's stale `dev-server` job — `apps/dev-server` left the tree
long ago, `pnpm --filter @object-ui/dev-server build` matches nothing, and the
job has been green by vacuity ever since. And re-enables
`DashboardRenderer.designMode` (skipped since 2026-05-01 as `TODO(#ci-hang)`):
on today's dependency tree the suite passes standalone (24/24), and the
re-enabling PR's own sharded Test jobs gate the full-suite behaviour, so the
skip was outliving whatever transitive dependency caused the hang.
30 changes: 0 additions & 30 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -337,33 +337,3 @@ jobs:
- name: Build Site
if: steps.docs-changes.outputs.should_run == 'true'
run: pnpm turbo run build --filter='@object-ui/site'

# Guard against fixture / @objectstack/spec drift: ensure the in-repo
# debug backend (apps/dev-server) still compiles its `objectstack.config.ts`
# into a valid artifact. Cheap (~10s after install) and protects the
# contributor on-ramp documented in apps/console/README.md.
dev-server:
name: Dev-server fixture build
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Checkout code
uses: actions/checkout@v7
with:
submodules: true

- name: Enable Corepack
run: corepack enable

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

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

- name: Build dev-server fixtures
run: pnpm --filter @object-ui/dev-server build
168 changes: 168 additions & 0 deletions .github/workflows/live-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Live E2E — the console driven against a REAL ObjectStack backend (#2835).
#
# What runs: an allowlist of e2e/live specs (screen-flow, action-modal,
# master-detail — `pnpm test:e2e:live:ci`) against `objectstack dev` booted
# from PUBLISHED @objectstack/* packages serving the showcase app. Every PR
# therefore smoke-tests "this console x the released backend" — the class of
# bug only a real browser against a real backend can catch (framework#3528:
# a lazily-loaded widget's suspension unwound to the host's route boundary
# and tore down the flow dialog; zero unit test could see it).
#
# ⚠️ INFORMATIONAL, NON-REQUIRED lane — `continue-on-error: true` keeps a
# failure here from failing the workflow run, so it never blocks a merge and
# never ejects unrelated PRs from the queue (objectstack#4850 is the prior
# art for why a new lane must prove itself outside the merge gate first).
# Do NOT add this job to required checks, and do not remove
# `continue-on-error`, until the lane has run clean for long enough to trust
# (watch the nightly schedule). Failures still surface: red step + uploaded
# report + job summary.
#
# Growing the allowlist: add specs to `test:e2e:live:ci` in package.json a
# few at a time, only after they prove flake-free here — do not switch all
# 20+ live specs on at once and inherit whatever flake exists (#2835's
# sequencing note).
#
# Backend pins live in e2e/live/ci/backend.env — the published package
# version MUST match the @objectstack/spec version in pnpm-lock.yaml (a
# mismatched pair proves nothing; bump both in the same PR).
name: Live E2E

on:
pull_request:
branches: [main, develop]
paths-ignore:
- '**/*.md'
- 'content/**'
- 'docs/**'
- 'apps/site/**'
- '.changeset/**'
# Nightly on main: accumulates the stability record that decides when the
# lane may be promoted to a required check.
schedule:
- cron: '30 6 * * *'
workflow_dispatch:

concurrency:
group: live-e2e-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
live-e2e:
name: Live E2E (informational)
runs-on: ubuntu-latest
timeout-minutes: 40
# Non-blocking by construction — see the header comment before touching.
continue-on-error: true

env:
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 code
uses: actions/checkout@v7
with:
submodules: true

- name: Enable Corepack
run: corepack enable

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

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

# The backend fixture (showcase metadata + published @objectstack/*
# node_modules) only changes when the pins do — cache it on the pin file.
# start-backend.sh's stamp check makes a cache hit skip clone + install.
- name: Cache live backend fixture
uses: actions/cache@v6
with:
path: ${{ runner.temp }}/live-backend
key: live-backend-${{ runner.os }}-${{ hashFiles('e2e/live/ci/backend.env') }}

- name: Start ObjectStack backend (published packages)
run: bash e2e/live/ci/start-backend.sh

- name: Build console
# Absolute base so `vite preview`'s SPA fallback serves deep links
# (/apps/showcase_app/…) with resolvable asset URLs — the default
# relative base ('./') 404s them (same reason ci.yml's e2e job pins
# VITE_BASE_PATH). Vite, not the package build: the SPA bundle is the
# only artifact the tests consume.
env:
VITE_BASE_PATH: /
run: pnpm --filter @object-ui/console exec vite build

- name: Serve console (vite preview)
# `preview.proxy` defaults to `server.proxy`, so /api is proxied to
# DEV_PROXY_TARGET just like the dev server would.
working-directory: apps/console
run: |
DEV_PROXY_TARGET="$LIVE_API_URL" nohup pnpm exec vite preview --port 5190 --strictPort \
> "$RUNNER_TEMP/console-preview.log" 2>&1 &
echo $! > "$RUNNER_TEMP/console-preview.pid"
for i in $(seq 1 60); do
curl -sf -o /dev/null "$LIVE_APP_URL" && exit 0
sleep 1
done
echo "console preview never became ready" >&2
tail -50 "$RUNNER_TEMP/console-preview.log" >&2
exit 1

- name: Get Playwright version
id: playwright-version
run: echo "version=$(pnpm list @playwright/test --depth=0 --json | jq -r '.[0].devDependencies["@playwright/test"].version')" >> $GITHUB_OUTPUT

- name: Cache Playwright browsers
uses: actions/cache@v6
id: playwright-cache
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 exec playwright install --with-deps chromium

- name: Install Playwright system dependencies
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: pnpm exec playwright install-deps chromium

- name: Run live E2E allowlist
run: pnpm test:e2e:live:ci

- name: Report failure in job summary
if: failure()
run: |
{
echo "## Live E2E failed (informational lane — does not block merge)"
echo ""
echo "Backend: published \`@objectstack/*\` per \`e2e/live/ci/backend.env\`."
echo "See the \`live-e2e-artifacts\` upload for the Playwright report and server logs."
echo "If this failure reproduces on re-run it is a real console x backend integration bug — treat it as such even though the lane cannot block your merge."
} >> "$GITHUB_STEP_SUMMARY"

- name: Upload report and server logs
uses: actions/upload-artifact@v7
if: ${{ !cancelled() && failure() }}
with:
name: live-e2e-artifacts
path: |
playwright-report/
test-results/
${{ runner.temp }}/live-backend/backend.log
${{ runner.temp }}/console-preview.log
retention-days: 14

# Ephemeral runner — no teardown needed; stop-backend.sh exists for
# local runs of the same scripts.
17 changes: 17 additions & 0 deletions e2e/live/ci/backend.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Pins for the CI live-e2e backend (see start-backend.sh and
# .github/workflows/live-e2e.yml).
#
# OBJECTSTACK_VERSION — the published @objectstack/* npm version the backend
# runs on. MUST stay aligned with the @objectstack/spec version resolved in
# this repo's pnpm-lock.yaml: the whole point of the lane is to smoke-test
# "this console x that published backend" as a matched pair. Testing a
# mismatched pair (backend rc.N vs console pinned rc.M) proves nothing —
# bump this in the same PR that bumps the console's @objectstack pins.
#
# OBJECTSTACK_REF — the objectstack-ai/objectstack commit the showcase app
# metadata is checked out from. Always the commit the release tag
# `@objectstack/cli@${OBJECTSTACK_VERSION}` points at, so the app source and
# the published packages it runs on come from the same tree. (Mirror image
# of the framework repo's `.objectui-sha` console pin.)
OBJECTSTACK_VERSION=17.0.0-rc.2
OBJECTSTACK_REF=89d2a4eb3f3b6b8f8c0fbc4cb3953cbe8218dc66
113 changes: 113 additions & 0 deletions e2e/live/ci/start-backend.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/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.
#
# What it does (idempotent; a stamp file skips prepare when pins are unchanged):
# 1. Sparse-checkout `examples/app-showcase` from objectstack-ai/objectstack
# at the pinned OBJECTSTACK_REF (backend.env).
# 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).
# 4. `objectstack dev --seed-admin --fresh` on $LIVE_BACKEND_PORT with a
# throwaway sqlite db, then poll the seeded sign-in until it answers.
#
# Env overrides:
# LIVE_BACKEND_DIR scratch dir (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

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=backend.env
source "$SCRIPT_DIR/backend.env"

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"
WANT_STAMP="$OBJECTSTACK_REF $OBJECTSTACK_VERSION"

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
fi
echo "[live-backend] prepare: showcase@${OBJECTSTACK_REF:0:12} on published @objectstack/*@$OBJECTSTACK_VERSION"
rm -rf "$APP_DIR" "$BACKEND_DIR/src" "$STAMP"

# Shallow, sparse fetch of the pinned 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).
node -e '
const fs = require("fs");
const file = process.argv[1], pin = process.argv[2];
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 };
fs.writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n");
' "$APP_DIR/package.json" "$OBJECTSTACK_VERSION"

(cd "$APP_DIR" && npm install --no-audit --no-fund --loglevel=error)
echo "$WANT_STAMP" > "$STAMP"
}

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.
if [ -f "$BACKEND_DIR/backend.pid" ]; then
local old_pid
old_pid="$(cat "$BACKEND_DIR/backend.pid")"
if kill -0 "$old_pid" 2>/dev/null && grep -qa objectstack "/proc/$old_pid/cmdline" 2>/dev/null; then
echo "[live-backend] already running (pid $old_pid)"
return
fi
rm -f "$BACKEND_DIR/backend.pid"
fi
echo "[live-backend] starting objectstack dev on :$PORT (log: $BACKEND_DIR/backend.log)"
# --fresh: throwaway sqlite db, auto-removed on SIGTERM. OS_CLOUD_URL=off:
# no marketplace/cloud calls — the lane must be hermetic.
(cd "$APP_DIR" && OS_CLOUD_URL=off nohup ./node_modules/.bin/objectstack dev \
--seed-admin --fresh -p "$PORT" > "$BACKEND_DIR/backend.log" 2>&1 & \
echo $! > "$BACKEND_DIR/backend.pid")

# Ready = the seeded admin can actually sign in (same call the e2e
# global-setup makes). Seeding runs at boot, so plain TCP is not enough.
local deadline=$((SECONDS + 300))
until curl -sf -o /dev/null -X POST "http://localhost:$PORT/api/v1/auth/sign-in/email" \
-H 'Content-Type: application/json' \
-d '{"email":"admin@objectos.ai","password":"admin123"}'; do
if ! kill -0 "$(cat "$BACKEND_DIR/backend.pid")" 2>/dev/null; then
echo "[live-backend] backend process died — last 100 log lines:" >&2
tail -100 "$BACKEND_DIR/backend.log" >&2
exit 1
fi
if [ "$SECONDS" -ge "$deadline" ]; then
echo "[live-backend] not ready after 300s — last 100 log lines:" >&2
tail -100 "$BACKEND_DIR/backend.log" >&2
exit 1
fi
sleep 2
done
echo "[live-backend] ready: seeded sign-in answered on :$PORT (pid $(cat "$BACKEND_DIR/backend.pid"))"
}

prepare
start
Loading
Loading