Skip to content

Commit d55acb7

Browse files
ci(live-e2e): derive the Live E2E UI ceiling from its first population (40 → 45) and pin it; the saved-view-filter header names the workspace backend (#30869)
<!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiNjyvCdXYygFqntMNR7S2dW)_ Fixes #27228 Ruling-ref: 6464191239 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:必需检查 `Live E2E UI` 的超时上限从 40 分钟改为 45 分钟,用 3,395 次真实运行(main 的 push、定时运行,加上合并队列)重新推导,并在测试里钉住;另把 `saved-view-filter.spec.ts` 头注释里已退役的后端版本钉改成现在真正用的工作区后端。 - 为什么改:旧的 40 分钟只来自一次本地冷启动估算,且没有测试钉住,会悄悄漂移;太松时一个卡死的后端会占住合并队列 40 分钟。注释则在误导下一个读者用错后端。 - 风险与回滚:风险低。实测最慢一次成功运行是合并队列里的 822 秒(13.7 分钟,慢机器),按规则要 ≥3 倍,40 分钟只有 2.92 倍,所以取 45;代价是真卡死时多占队列 5 分钟。回滚就是把 `ci.yml` 和测试里的 45 改回 40。 - 先看:`.github/workflows/ci.yml`(`live-e2e-ui` 上方的推导注释与 `timeout-minutes`)→ `ui/scripts/__tests__/turbo-cache-save-bound.test.ts`(`CEILINGS` 与新的对照用例)→ `ui/e2e/live/saved-view-filter.spec.ts`(只改注释)。 - 两分钟验证:`pnpm --dir ui exec vitest run scripts/__tests__/turbo-cache-save-bound.test.ts`,预期 46 个用例全绿,其中含 `live-e2e-ui keeps its derived ceiling of 45 minutes`。 - 需要你做的:本 PR 改 `.github/workflows/ci.yml`(Tier H),需要你一次授权的 APPROVED 评审;之后由席位落地。 ## What changes 1. **`Live E2E UI` ceiling, 40 → 45, from its first population.** The rule is the one above `lint-ui` (#25869 row F13): the smallest round number that is both at least 3x the slowest successful run and at least that run + 15 min. - Population: every completed `CI` run with event `push` or `schedule` on `main`, or `merge_group`, created between #27212's merge (2026-10-28T11:47:32Z) and 2026-11-06T05:50:51Z. That is 3,395 runs (1,177 push, 53 schedule, 2,165 merge_group). The queue runs count because this is a required context and a queue entry whose run is cancelled is ejected; the sibling ceilings were derived over every CI run too. Every run carries the job: 3,205 success, 181 skipped, 7 failure (316 to 529 s), 2 cancelled (91 s, 0 s). None reached a ceiling. - Duration is `completed_at` minus `started_at`, cross-checked against the last step's end. Six queue jobs on 2026-11-01 carry a late platform stamp: four read about 4,000 s and two read below zero, while their steps span 387 to 513 s. Each of the 40 slowest reported jobs, and those six, is read to its last step's end instead. - Successful jobs: n=3205, min 68 s, median 407 s, p95 563 s, p99 589 s, max **822 s** (13.7 min, job 125022934909 in run 41364879269: a queue run on a slow runner, with a 615 s closure build and 94 s of specs). So max(41.1, 28.7) => **45**. The next slowest, to the last step, are 772, 738, 731, 702 and 699 s (reported 775, 740, 733, 704 and 701 s), all queue runs. Push and schedule alone top out at 624 s and would give 35. - `pull_request` runs are not in the population: they were not read in full, which the sibling rule ("every successful job") asks for. A sample of 93 successful ones on the same slow days (2026-11-01 to 2026-11-03) tops out at 675 s, under the deciding 822 s, so 45 holds either way. The next re-derivation should read them in full. - The slow tail is the cold case. Those jobs spend 406 to 615 s in the closure-build step, above the 384 s cold closure build in the local run that set the old 40, so the measured max replaces that estimate as the slowest successful run. - The derivation comment in `ci.yml` now carries these numbers and the queries. 2. **The pin.** `turbo-cache-save-bound.test.ts` adds `live-e2e-ui: 45` to `CEILINGS`, beside the four sibling ceilings, so `it.each(CEILINGS)` holds it. It adds a control: a copy of the workflow with the ceiling at 40, or with no ceiling at all, reads as moved for exactly that job, and the real workflow reads as moved for none. The job saves no cache, so it is not added to `CACHE_JOBS`. 3. **`saved-view-filter.spec.ts` header, comment only.** It named `OBJECTSTACK_VERSION` in `backend.env` as the backend. It now names the workspace backend: `ui/e2e/live/ci/start-backend.sh`, which boots this tree's built `@objectstack/cli` on `LIVE_BACKEND_APP` (`examples/app-showcase`). It says the 400/200 readings were first measured against the published release the lane pinned at the time. No assertion, timeout or retry moves. No other job's ceiling changes. ## Tests - `pnpm --dir ui exec vitest run --maxWorkers=2 scripts/__tests__/turbo-cache-save-bound.test.ts`: 46/46 passed (at `640fd99ad0`). - The full derived gate list is in Verification. ## Acceptance notes - `ui/e2e/live/ci/backend.env` sets one variable, `LIVE_BACKEND_APP=examples/app-showcase`. It names `OBJECTSTACK_VERSION` only in a comment that says the pin retired and why. Nothing in it reads as a live pin, and it is left unchanged. - The job's `BUDGET:` note now also gives the specs step's reading in the 40 slowest jobs of this population (43 to 94 s, median 70 s). That is still under its 2-minute budget. - Method caveat for the next re-derivation: `completed_at` minus `started_at` can carry a late platform stamp. Six queue jobs on 2026-11-01 read about 4,000 s or below zero; the four long ones (runs 41286803335, 41286803828, 41286905470, 41286940359) span 472, 387, 508 and 513 s to their last step. The 40-minute timeout never fired on them. Read the slow tail to the last step's end. ## Verification All local readings are at head `640fd99ad0`: the first-round commit `359c392f7b`, one merge of `origin/main` `ae82d057e1` (`c21e59e84c`), and the merge-queue re-derivation on top. There was no rebase. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 62 commands for the 3 changed paths, the same list as round 1. - All 62 exited 0 on this head. The workspace build from round 1 was in place (`pnpm build --concurrency=2`, 118/118 tasks); in round 1, four gates first exited 3 and the ui suite exited 1 until that build existed. The ui suite: Test Files 170 passed, 2 skipped (172); Tests 4981 passed, 2 skipped. - `--ran` reconciliation: `62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN`, a derived zero (all 62 recorded an exit code). - No changeset: nothing here publishes (the workflow, a test under the private `ui/` root, and a comment in an e2e spec), so the PR carries `skip-changeset`. - Dogfood: not run. The title has no `!` and `Clause-②: no` carries no `(narrowing)` arm. - CI families: see the PR's check-runs. <details><summary>Each derived command and its exit code at 640fd99</summary> ```text node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-dts-emitted.mjs --self-test :: exit 0 node scripts/check-self-test-wired.mjs :: exit 0 node scripts/check-self-test-wired.mjs --self-test :: exit 0 node scripts/check-self-test-workflow-commands.mjs :: exit 0 node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0 node scripts/check-step-collectors.mjs :: exit 0 node scripts/check-step-collectors.mjs --self-test :: exit 0 node scripts/check-whole-set-label-write.mjs :: exit 0 node scripts/check-whole-set-label-write.mjs --self-test :: exit 0 node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0 node scripts/ci/scheduled-full-run.mjs --self-test :: exit 0 node scripts/docs-audit/check-drift-comment.mjs :: exit 0 node scripts/pm/ci-failure.mjs --self-test :: exit 0 pnpm --dir ui exec vitest run scripts/ :: exit 0 pnpm check:agent-test-spelling :: exit 0 pnpm check:aggregator-roster :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:declared-population-live :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:dts-closure :: exit 0 pnpm check:dual-build-cjs-loads :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:lean-entry-closure :: exit 0 pnpm check:node-version :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:pm-close-cards :: exit 0 pnpm check:pm-dispatch-gates :: exit 0 pnpm check:pm-expected-skips :: exit 0 pnpm check:pm-fleet-write-dispatch :: exit 0 pnpm check:pm-fleet-write-execute :: exit 0 pnpm check:pm-fleet-write-validate :: exit 0 pnpm check:pm-governed-merges :: exit 0 pnpm check:pm-issue-transfer :: exit 0 pnpm check:pm-post-stamped :: exit 0 pnpm check:pm-record-recognisers :: exit 0 pnpm check:pnpm-acquisition :: exit 0 pnpm check:pnpm-filter-targets :: exit 0 pnpm check:published-files :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:registry-log-declared :: exit 0 pnpm check:required-contexts :: exit 0 pnpm check:rest-log-spy-declared :: exit 0 pnpm check:shard-attestation :: exit 0 pnpm check:sourcemap-no-sources-content :: exit 0 pnpm check:stall-guard-budget :: exit 0 pnpm check:stall-guard-headroom :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 pnpm check:workflow-status-functions :: exit 0 pnpm check:workflow-step-name-quoting :: exit 0 ``` </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- signed: domain:devx#2 · marchtian · session_017ihL2ebbZkr2QAEG1x7UC8 --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9038187 commit d55acb7

3 files changed

Lines changed: 82 additions & 24 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4650,23 +4650,57 @@ jobs:
46504650
# showcase app's) than the seven packages that job hands on, so it restores
46514651
# Build Core's turbo seed instead.
46524652
#
4653-
# BUDGET: the "Run the live e2e specs" step stays under 2 minutes (14-24 s,
4654-
# median 21 s, over the 35 runs above). The widened gate puts this step on
4655-
# most pull requests and on every queue entry, so a spec that would push it
4656-
# over stays in `test:e2e:live:nightly` instead of joining the `:ci`
4657-
# allowlist.
4653+
# BUDGET: the "Run the live e2e specs" step stays under 2 minutes. It took
4654+
# 14-24 s (median 21 s) over the 35 runs above, and 43-94 s (median 70 s)
4655+
# in the 40 slowest jobs of the ceiling's population below. The widened
4656+
# gate puts this step on most pull requests and on every queue entry, so a
4657+
# spec that would push it over stays in `test:e2e:live:nightly` instead of
4658+
# joining the `:ci` allowlist.
46584659
#
4659-
# Ceiling: there is no CI population yet, so it is derived from one cold
4660-
# local run on 4 shared cores (#27154, 2026-10-28), by the rule above Lint UI:
4661-
# install 60 s + closure build 384 s + Console build 119 s + backend boot
4662-
# 19 s + specs 26 s + browser provisioning and setup ~120 s ≈ 730 s
4663-
# (12.2 min) => max(36.5, 27.2) => 40. Re-derive from the first population.
4660+
# Ceiling, by the rule above Lint UI, re-derived from the first CI population
4661+
# (#27228). Population: every completed `CI` run with event `push` or
4662+
# `schedule` on `main`, or `merge_group`, created between #27212's merge
4663+
# (2026-10-28T11:47:32Z) and 2026-11-06T05:50:51Z: 3,395 runs (1,177 push,
4664+
# 53 schedule, 2,165 merge_group). The queue runs count because this is a
4665+
# required context and a queue entry whose run is cancelled is ejected.
4666+
# Read from
4667+
# GET repos/{owner}/{repo}/actions/workflows/ci.yml/runs
4668+
# ?branch=main&event=push|schedule&status=completed&created=A..B
4669+
# GET repos/{owner}/{repo}/actions/workflows/ci.yml/runs
4670+
# ?event=merge_group&status=completed&created=A..B
4671+
# in one- or two-day windows (one listing answers at most 1,000 runs), and
4672+
# this job's check-run on each head sha from
4673+
# GET repos/{owner}/{repo}/commits/{sha}/check-runs?check_name=Live E2E UI
4674+
# kept when its job URL names one of those runs. Every run carries the job:
4675+
# 3,205 success, 181 skipped, 7 failure (316-529 s), 2 cancelled (91 s,
4676+
# 0 s); none reached a ceiling.
4677+
# Duration is `completed_at` minus `started_at`, except where the platform
4678+
# stamped one of them late: six queue jobs on 2026-11-01 read ~4,000 s or
4679+
# below zero while their steps span 387-513 s, so each of the 40 slowest
4680+
# reported jobs (and those six) is read to its last step's end instead.
4681+
# - success: n=3205, min 68 s, median 407 s, p95 563 s, p99 589 s,
4682+
# max 822 s (13.7 min; job 125022934909, a queue run on a slow runner:
4683+
# 615 s closure build, 94 s specs) => max(41.1, 28.7) => 45.
4684+
# The next slowest, to the last step, are 772, 738, 731, 702 and 699 s
4685+
# (reported 775, 740, 733, 704 and 701 s), all queue runs; push and
4686+
# schedule alone top out at 624 s and would give 35.
4687+
# `pull_request` runs are not in the population: they were not read in
4688+
# full, which the sibling rule ("every successful job") asks for. A sample
4689+
# of 93 successful ones on the same slow days (2026-11-01 to 2026-11-03)
4690+
# tops out at 675 s, under the 822 s that decides the value, so 45 holds
4691+
# either way; the next re-derivation should read them in full.
4692+
# The slow tail is the cold case: those jobs spend 406-615 s in the
4693+
# closure build, above the 384 s of the cold local run on 4 shared cores
4694+
# (#27154) that set the old 40 (≈ 730 s => max(36.5, 27.2) => 40), so the
4695+
# measured max, not that estimate, is the slowest successful run.
4696+
# Re-derive when the job's work changes; ui/scripts/__tests__/
4697+
# turbo-cache-save-bound.test.ts holds the value.
46644698
live-e2e-ui:
46654699
name: Live E2E UI
46664700
needs: filter
46674701
if: ${{ !cancelled() && needs.filter.outputs.ui_tree != 'false' && (needs.filter.outputs.ui != 'false' || needs.filter.outputs.ui_consumers != 'false' || needs.filter.outputs.core != 'false') }}
46684702
runs-on: ubuntu-latest
4669-
timeout-minutes: 40
4703+
timeout-minutes: 45
46704704
permissions:
46714705
contents: read
46724706
env:

‎ui/e2e/live/saved-view-filter.spec.ts‎

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ import { test, expect } from '@playwright/test';
66
*
77
* This is the empirical pin for the defect, run against the REAL stack because
88
* only the server can answer the question the issue asked: does it accept a
9-
* rule array in `$filter`? It does not. Measured against the published backend
10-
* backend this lane pins — `OBJECTSTACK_VERSION` in `e2e/live/ci/backend.env`,
11-
* once held to the lockfile by objectui's CI-guide pin test (objectui#7689; deleted by
12-
* objectstack#26058), with the showcase-app commit derived from it at boot from
13-
* the matching `@objectstack/cli` release tag (objectui#7964). That pin has
14-
* moved since this measurement was first taken, which is why it is named by
15-
* FILE and not by value: a version literal written here goes stale behind the
16-
* pin silently, and the whole point of objectui#7689 is that it did — for two
17-
* minor versions, in the pin itself. The showcase app is the one that release
18-
* checks out:
9+
* rule array in `$filter`? It does not. First measured against the published
10+
* release the lane then pinned; the lane now runs against the WORKSPACE
11+
* backend: `ui/e2e/live/ci/start-backend.sh` boots this tree's own built
12+
* `@objectstack/cli` (`objectstack dev --seed-admin --fresh`) on the app that
13+
* `LIVE_BACKEND_APP` names in `ui/e2e/live/ci/backend.env`
14+
* (`examples/app-showcase`), so the HEAD Console meets the HEAD backend and no
15+
* published version is pinned anywhere (objectstack#27154; backend.env says
16+
* why the pin retired). The backend is named by those two files, never by a
17+
* version, so this note cannot go stale behind it. The showcase app is that
18+
* directory:
1919
*
2020
* GET /api/v1/data/showcase_task
2121
* ?$filter=[{"field":"status","operator":"equals","value":"in_progress"}]
@@ -35,8 +35,8 @@ import { test, expect } from '@playwright/test';
3535
*
3636
* In the `test:e2e:live:ci` allowlist since objectui#3472, per live-e2e.yml's
3737
* promotion policy (prove flake-free first, then admit): 3/3 consecutive green
38-
* runs against the pinned live pair, plus a 5/5 run of the enlarged allowlist
39-
* to show it does not disturb the incumbent specs.
38+
* runs against the live pair as it was then pinned, plus a 5/5 run of the
39+
* enlarged allowlist to show it does not disturb the incumbent specs.
4040
*/
4141
const APP = process.env.SHOWCASE_APP_NAME || 'showcase_app';
4242

‎ui/scripts/__tests__/turbo-cache-save-bound.test.ts‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
22
import fs from 'node:fs';
33
import { parse as parseYaml } from 'yaml';
44

5-
import { rootCiPath, UI_JOBS } from './helpers/root-ci';
5+
import { rootCiPath, UI_JOBS, UI_LIVE_E2E_JOB } from './helpers/root-ci';
66

77
/**
88
* objectui#6577 — cache bookkeeping voided a verdict the gate had already
@@ -69,14 +69,26 @@ const CACHE_JOBS = [DEPS_JOB, ...Object.values(UI_JOBS)];
6969
* was "does not rise" (#6577's ruled-out non-fix).
7070
* - lint-ui, build-e2e-ui: #25869 row F13, the ceiling comment above lint-ui.
7171
* - ui-deps: the derivation on that job.
72+
* - live-e2e-ui: the derivation on that job, by the lint-ui rule over its
73+
* first population, `main`'s push and schedule runs plus the merge-queue
74+
* runs (objectstack#27228). It saves no cache, so it
75+
* is pinned here for its ceiling alone, not one of `CACHE_JOBS`.
7276
*/
7377
const CEILINGS: Record<string, number> = {
7478
[DEPS_JOB]: 25,
7579
[UI_JOBS.typeCheck]: 40,
7680
[UI_JOBS.lint]: 40,
7781
[UI_JOBS.buildE2e]: 35,
82+
[UI_LIVE_E2E_JOB]: 45,
7883
};
7984

85+
/** The pinned jobs whose ceiling in `wf` is not the pinned value, as `key: found ≠ pinned`. */
86+
function movedCeilings(wf: { jobs: Record<string, Job> }): string[] {
87+
return Object.entries(CEILINGS)
88+
.filter(([key, pinned]) => wf.jobs[key]?.['timeout-minutes'] !== pinned)
89+
.map(([key, pinned]) => `${key}: ${wf.jobs[key]?.['timeout-minutes']} ≠ ${pinned}`);
90+
}
91+
8092
function job(key: string): Job {
8193
const found = workflow.jobs[key];
8294
expect(found, `ci.yml must still define a \`${key}:\` job`).toBeDefined();
@@ -196,6 +208,18 @@ describe('root ci.yml — the ui jobs\' cache saves are bounded and non-fatal (o
196208
// check still reports `cancelled` (objectui#6577, objectui#7048). Moving
197209
// one is a deliberate act: re-derive it beside the job, move it here.
198210
expect(job(key)['timeout-minutes'], `the \`${key}\` job ceiling moved`).toBe(ceiling);
211+
expect(movedCeilings(workflow).filter((m) => m.startsWith(`${key}:`))).toEqual([]);
212+
});
213+
214+
it('the ceiling pin fires on a moved Live E2E UI ceiling — the control', () => {
215+
// The value it carried before the re-derivation, and a job that lost its
216+
// ceiling altogether: each must read as moved, and nothing else may.
217+
const moved = structuredClone(workflow);
218+
moved.jobs[UI_LIVE_E2E_JOB]['timeout-minutes'] = 40;
219+
expect(movedCeilings(moved)).toEqual([`${UI_LIVE_E2E_JOB}: 40 ≠ 45`]);
220+
delete moved.jobs[UI_LIVE_E2E_JOB]['timeout-minutes'];
221+
expect(movedCeilings(moved)).toEqual([`${UI_LIVE_E2E_JOB}: undefined ≠ 45`]);
222+
expect(movedCeilings(workflow)).toEqual([]);
199223
});
200224
});
201225

0 commit comments

Comments
 (0)