Skip to content

test(i18n,app-shell): discover the createSafeTranslation defaults tables instead of hand-listing three (objectui#7884) #3200

test(i18n,app-shell): discover the createSafeTranslation defaults tables instead of hand-listing three (objectui#7884)

test(i18n,app-shell): discover the createSafeTranslation defaults tables instead of hand-listing three (objectui#7884) #3200

Workflow file for this run

# Live E2E — the console driven against a REAL ObjectStack backend (#2835).
#
# What runs: an allowlist of e2e/live specs (the list is whatever the
# `test:e2e:live:ci` script in package.json names — that script is the single
# source of truth; do not re-enumerate the specs here, a hand-copied list
# drifts on every promotion) 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
# ⚠️ Do NOT add a `runner.*` expression to this job-level `env:` block.
# The `runner` context does not exist yet when job-level env is evaluated
# (GitHub's "contexts availability" table allows only github / needs /
# strategy / matrix / vars / secrets / inputs here). One `${{ runner.temp }}`
# in this block fails the whole FILE at validation time — 0 jobs, 0s,
# "startup failure" — and `continue-on-error` cannot soften it because no
# job is ever created. Need a runner path? Use the `$RUNNER_TEMP`
# environment variable inside a `run:` step, or a step-level `env:` /
# `with:` (both may read `runner`). See the LIVE_BACKEND_DIR step below.
env:
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.
#
# ── Cache bookkeeping cannot void a recorded verdict (objectui#7048) ──
# THE ORDERING, carried here from the type-check split in `ci.yml`
# (objectui#6577, PR #7047) because it is what a future reader needs in
# order to judge whether these steps may be touched: the verdict is
# recorded by the checking steps; everything after them is bookkeeping,
# and bookkeeping must never discard an answer the gate already produced.
#
# Why the SPLIT rather than a timeout on one step: combined
# `actions/cache` declares `main: dist/restore/index.js` plus
# `post: dist/save/index.js`, so its save is a step the RUNNER generates
# at job end (`Post Cache live backend fixture`). No workflow syntax
# attaches `timeout-minutes` or `continue-on-error` to a generated post
# step, so an upload stall runs this job into `timeout-minutes: 40` and
# it reports `cancelled` — a lane that says nothing at all, which for an
# informational lane whose only product is a stability record is the
# worst outcome available. Measured once, on `ci.yml`'s type-check cache:
# a 1-second save took 789s (objectui#6577). `actions/cache`'s own
# `save-always` deprecation text points at this same split.
#
# ⛔ Raising `timeout-minutes: 40` is the ruled-out non-fix: a larger
# ceiling only buys a longer hang and still ends in `cancelled`.
#
# This job has TWO caches, so it gets two bounds. Together they can
# consume at most 5 + 8 = 13 minutes of the 40-minute ceiling, against a
# verdict path that measured 2m25s end to end (job 100097466897) — so
# even both bookkeeping halves stalling at once cannot reach the ceiling.
#
# The restore halves are deliberately left UNBOUNDED: a restore stall
# fails BEFORE any verdict exists — a lane that did not run, which is
# honest — rather than a recorded verdict discarded.
- name: Restore live backend fixture
id: live-backend-cache
uses: actions/cache/restore@v6
with:
path: ${{ runner.temp }}/live-backend
key: live-backend-${{ runner.os }}-${{ hashFiles('e2e/live/ci/backend.env') }}
- name: Start ObjectStack backend (published packages)
# Step-level env — `runner` IS available here (unlike job-level env).
# Must resolve to the same path as the cache step's `path:` above and
# the artifact upload's `path:` below, or the cache silently stops
# hitting; keep the three in sync.
env:
LIVE_BACKEND_DIR: ${{ runner.temp }}/live-backend
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: |
# This pipeline's exit status is load-bearing: the value it produces
# becomes the Playwright browser cache key in the next step. Without
# `pipefail` a failing `pnpm list` is masked by `jq`, and `jq -r`
# prints the string `null` and exits 0 when the field is missing — so
# either failure used to yield a *successful* step and a key that had
# silently degraded to `playwright-Linux-` / `playwright-Linux-null`.
# That wrong bucket is stable, so two Playwright versions can share
# one cache entry and restore a stale browser (objectui#6231).
# The same block is duplicated verbatim in `ci.yml` and `live-e2e.yml`
# — keep them byte-identical so they stay greppable as a pair.
set -eo pipefail
if ! version=$(pnpm 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"
# The second of this job's two caches; the split, the ordering sentence
# and the ruled-out non-fix are all documented at `Restore live backend
# fixture` above (objectui#7048). Same shape here: restore in place and
# unbounded, save last and bounded.
- name: Restore Playwright browsers
uses: actions/cache/restore@v6
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
# Same provisioning shape as `ci.yml`'s `Build & E2E`, and it carries the
# same exposure: `--with-deps` / `install-deps` run `apt-get update` with
# nothing bounding it. That is what hung `Build & E2E` three times on
# 2026-08-19 for the full job ceiling (objectui#5304); this job's ceiling
# is 40 minutes, so the same mirror stall would cost even more here. The
# browsers come from the cache step above, and the launch probe in the
# script is what proves a browser exists. See the script header for the
# measured incident log.
- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: pnpm exec playwright install chromium
- name: Ensure Chromium can launch
run: bash scripts/ensure-chromium-ready.sh
- 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
# The bookkeeping halves of the two splits documented at the restore
# steps above, placed HERE — last in the job — because that is exactly
# where the post phases they replace already ran. They are ordered
# Playwright-then-fixture to preserve the observed post-phase order
# (post steps run in reverse registration order, so `Post Cache
# Playwright browsers` ran before `Post Cache live backend fixture` —
# job 100097466897). Nothing about what this lane reports moves with them.
#
# `timeout-minutes: 8` on the browser cache is DERIVED FOR THIS CACHE,
# not inherited from the type-check site's 5 (objectui#7048 fences that
# explicitly): it is Playwright's browser binaries at ~269 MB. Restores
# of that exact archive measured 5s here (job 100097466897) and 3s in
# `ci.yml`'s `Build & E2E` (jobs 100096569775, 100094597323) ⇒ ~54-90
# MB/s in the download direction; a save is only observable after a
# version bump (an exact key hit skips it, and `Post Cache Playwright
# browsers` measured 0s on every sampled run), so the honest save is
# bounded from the restore rather than read directly — said plainly
# rather than dressed up as a direct measurement. A deliberately
# pessimistic 20x penalty on the slowest observed restore gives ~1m40s;
# 8 minutes is ~4.8x that and demands only 0.56 MB/s sustained.
#
# `timeout-minutes: 5` on the fixture cache is derived from THIS site's
# own measurement: its save measured 8s on 2026-09-02 (`Post Cache live
# backend fixture`, job 100097466897 — a real save, not a skip: the
# restore missed, which is why `Start ObjectStack backend` did the full
# clone + install). 5 minutes is ~37x that.
#
# `continue-on-error: true` on both is the other half, and without it the
# bounds would only trade a `cancelled` lane for a red one. A cache that
# failed to upload costs the next run some time; it says nothing about
# the console-x-backend integration this lane exists to observe.
#
# Behaviour preserved, spelled out so the equivalence is checkable:
# - `cache-hit != 'true'` reproduces the combined action's own "exact
# hit on the primary key ⇒ do not save" skip.
# - neither condition names a status function, so the implicit
# `success()` still applies — matching `post-if: success()`. In
# particular a failed `Run live E2E allowlist` skips both saves, just
# as the post phases skipped.
# - same `path` and same `key` as the matching restore step.
- name: Save Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v6
timeout-minutes: 8
continue-on-error: true
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
- name: Save live backend fixture
if: steps.live-backend-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v6
timeout-minutes: 5
continue-on-error: true
with:
path: ${{ runner.temp }}/live-backend
key: live-backend-${{ runner.os }}-${{ hashFiles('e2e/live/ci/backend.env') }}
# Ephemeral runner — no teardown needed; stop-backend.sh exists for
# local runs of the same scripts.