From 04a627e619daa163b0b19745286c548234751a7f Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Wed, 29 Jul 2026 19:51:59 +0200 Subject: [PATCH 1/4] ci(studio): run the timeline viewport gate on studio changes The gate has existed since the row virtualization stack landed but nothing under `.github/` referenced it, so it only ever ran when someone ran it by hand. That is how the flag-off scroll regression reached eight merged-ready PRs without anything noticing. Adds a `studio-timeline-viewport` job that boots two Studio dev servers, one per flag state, and runs both arms of the gate against them. Two servers are needed because row virtualization is read from `import.meta.env` at module load, so one process cannot serve both builds. Scoped to a new `studio` paths filter rather than the broad `code` one: the gate only says anything about `packages/studio`, `packages/core` and `packages/studio-server`. Adds a `ci` tier. It applies the constrained budgets without any emulation, because a hosted runner is already slower and noisier than the machine the strict numbers were recorded on, while the existing `low-resource` tier would throttle it a further 4x and measure the throttle rather than the build. The fixture composition is tracked under `tests/e2e/fixtures` but Studio resolves projects from the gitignored `data/projects`, so the job copies it into place instead of a project directory being committed. Both arms run in about 7 seconds each locally, so the job cost is almost entirely dependency install and the workspace build it shares with `studio-load-smoke`. --- .github/workflows/ci.yml | 82 +++++++++++++++++++ .../tests/e2e/timeline-virtualization.mjs | 11 ++- 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54cc5cd97f..943f7020d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,7 @@ jobs: skills: ${{ steps.filter.outputs.skills }} codex_plugin: ${{ steps.filter.outputs.codex_plugin }} gcp_beginframe: ${{ steps.filter.outputs.gcp_beginframe }} + studio: ${{ steps.filter.outputs.studio }} steps: # Force git-based change detection instead of the pull_request REST API. # The API path can fail the whole workflow on transient listFiles @@ -77,6 +78,12 @@ jobs: - "scripts/package-codex-plugin.mjs" - "package.json" - ".github/workflows/ci.yml" + studio: + - "packages/studio/**" + - "packages/core/**" + - "packages/studio-server/**" + - "bun.lock" + - ".github/workflows/ci.yml" gcp_beginframe: - "packages/gcp-cloud-run/Dockerfile" - "packages/aws-lambda/scripts/probe-beginframe.ts" @@ -496,6 +503,81 @@ jobs: kill $SERVER_PID 2>/dev/null || true + studio-timeline-viewport: + name: "Studio: timeline viewport gate" + needs: [changes] + if: needs.changes.outputs.studio == 'true' + runs-on: ubuntu-latest + timeout-minutes: 12 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + - run: bun install --frozen-lockfile + # Same reason as studio-load-smoke: vite.config.ts is loaded by Node and + # resolves the workspace packages through their "node" export condition. + - run: bun run --filter '@hyperframes/{parsers,lint,studio-server}' build + - run: bun run --cwd packages/core build + - run: bun run --cwd packages/core build:hyperframes-runtime + - name: Install the fixture as a Studio project + # Studio resolves projects from packages/studio/data/projects, which is + # gitignored. The fixture composition is tracked under tests/e2e, so + # copy it into place rather than committing a project directory. + run: | + mkdir -p packages/studio/data/projects + cp -R packages/studio/tests/e2e/fixtures/timeline-virtualization \ + packages/studio/data/projects/timeline-virtualization + - name: Run both arms of the timeline viewport gate + run: | + set -euo pipefail + + # Two servers, because row virtualization is read from import.meta.env + # at module load: one process cannot serve both builds. + bun run --cwd packages/studio dev -- --port 5313 --strictPort & + DEFAULT_PID=$! + VITE_STUDIO_TIMELINE_ROW_VIRTUALIZATION_ENABLED=1 \ + bun run --cwd packages/studio dev -- --port 5314 --strictPort & + VIRTUALIZED_PID=$! + trap 'kill $DEFAULT_PID $VIRTUALIZED_PID 2>/dev/null || true' EXIT + + for i in $(seq 1 60); do + if curl -sf http://localhost:5313/ >/dev/null 2>&1 \ + && curl -sf http://localhost:5314/ >/dev/null 2>&1; then break; fi + sleep 1 + done + if ! curl -sf http://localhost:5313/ >/dev/null 2>&1 \ + || ! curl -sf http://localhost:5314/ >/dev/null 2>&1; then + echo "FAIL: studio dev servers did not start" + exit 1 + fi + + # The default build first. It is the one users get, and the arm that + # caught the regression this gate exists for. + STUDIO_URL="http://localhost:5313/#project/timeline-virtualization" \ + TIMELINE_ROW_VIRTUALIZATION=off \ + TIMELINE_ELEMENT_COUNT=1000 \ + TIMELINE_TIER=ci \ + node packages/studio/tests/e2e/timeline-virtualization.mjs \ + | tee /tmp/timeline-gate-default.json + + STUDIO_URL="http://localhost:5314/#project/timeline-virtualization" \ + TIMELINE_ROW_VIRTUALIZATION=on \ + TIMELINE_ELEMENT_COUNT=50000 \ + TIMELINE_TIER=ci \ + node packages/studio/tests/e2e/timeline-virtualization.mjs \ + | tee /tmp/timeline-gate-virtualized.json + - name: Upload gate evidence + # The gate's whole output is machine-readable evidence, and a red run is + # exactly when someone needs to read it. Keep it on failure too. + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: timeline-viewport-gate-evidence + path: /tmp/timeline-gate-*.json + if-no-files-found: warn + smoke-global-install: name: "Smoke: global install" needs: [changes, build] diff --git a/packages/studio/tests/e2e/timeline-virtualization.mjs b/packages/studio/tests/e2e/timeline-virtualization.mjs index 504d5e1d9d..f48c82ee75 100644 --- a/packages/studio/tests/e2e/timeline-virtualization.mjs +++ b/packages/studio/tests/e2e/timeline-virtualization.mjs @@ -7,6 +7,13 @@ * STUDIO_URL=http://127.0.0.1:5190/#project/timeline-virtualization \ * node packages/studio/tests/e2e/timeline-virtualization.mjs * + * TIMELINE_TIER selects the budget set and the emulation applied. "primary" is + * a developer machine and holds the strict budgets. "low-resource" and + * "high-dpr" add CPU throttling and a 2x scale factor respectively. "ci" is a + * shared runner: no emulation, but the constrained budgets, because a hosted + * runner is already slower and noisier than the machine the strict numbers were + * recorded on. Throttling it further would measure the throttle, not the build. + * * TIMELINE_ROW_VIRTUALIZATION selects which build is under test and defaults to * "off", the product default. The gate previously only ever ran against a server * with row virtualization enabled, so the configuration users actually get was @@ -34,12 +41,12 @@ if (!STUDIO_URL) { } if ( ![1_000, 50_000].includes(ELEMENT_COUNT) || - !["primary", "low-resource", "high-dpr"].includes(TIER) || + !["primary", "low-resource", "high-dpr", "ci"].includes(TIER) || !["off", "on"].includes(ROW_VIRTUALIZATION) ) { console.error( "TIMELINE_ELEMENT_COUNT must be 1000 or 50000; " + - "TIMELINE_TIER must be primary, low-resource, or high-dpr; " + + "TIMELINE_TIER must be primary, low-resource, high-dpr, or ci; " + "TIMELINE_ROW_VIRTUALIZATION must be off or on", ); process.exit(2); From faff05c0f07e5e0cb8cef798028fe310ba304d6f Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Wed, 29 Jul 2026 23:45:20 +0200 Subject: [PATCH 2/4] fix(ci): preserve both timeline gate evidence arms --- .github/workflows/ci.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 943f7020d2..730203f1b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -555,19 +555,29 @@ jobs: # The default build first. It is the one users get, and the arm that # caught the regression this gate exists for. + # Capture both statuses so either failure still leaves two evidence files. + DEFAULT_STATUS=0 STUDIO_URL="http://localhost:5313/#project/timeline-virtualization" \ TIMELINE_ROW_VIRTUALIZATION=off \ TIMELINE_ELEMENT_COUNT=1000 \ TIMELINE_TIER=ci \ node packages/studio/tests/e2e/timeline-virtualization.mjs \ - | tee /tmp/timeline-gate-default.json + | tee /tmp/timeline-gate-default.json \ + || DEFAULT_STATUS=$? + VIRTUALIZED_STATUS=0 STUDIO_URL="http://localhost:5314/#project/timeline-virtualization" \ TIMELINE_ROW_VIRTUALIZATION=on \ TIMELINE_ELEMENT_COUNT=50000 \ TIMELINE_TIER=ci \ node packages/studio/tests/e2e/timeline-virtualization.mjs \ - | tee /tmp/timeline-gate-virtualized.json + | tee /tmp/timeline-gate-virtualized.json \ + || VIRTUALIZED_STATUS=$? + + if (( DEFAULT_STATUS != 0 || VIRTUALIZED_STATUS != 0 )); then + echo "FAIL: default=${DEFAULT_STATUS}, virtualized=${VIRTUALIZED_STATUS}" + exit 1 + fi - name: Upload gate evidence # The gate's whole output is machine-readable evidence, and a red run is # exactly when someone needs to read it. Keep it on failure too. From 3601afc543a82c87d17d447a07efc48170ad87c9 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Thu, 30 Jul 2026 05:08:07 +0200 Subject: [PATCH 3/4] ci(studio): report timeline gate arm statuses --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 730203f1b4..347281bce6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -574,6 +574,12 @@ jobs: | tee /tmp/timeline-gate-virtualized.json \ || VIRTUALIZED_STATUS=$? + { + echo "### Timeline viewport gate" + echo "- Default arm exit: ${DEFAULT_STATUS}" + echo "- Virtualized arm exit: ${VIRTUALIZED_STATUS}" + } >> "$GITHUB_STEP_SUMMARY" + if (( DEFAULT_STATUS != 0 || VIRTUALIZED_STATUS != 0 )); then echo "FAIL: default=${DEFAULT_STATUS}, virtualized=${VIRTUALIZED_STATUS}" exit 1 From 6e3a64b9d8c5e23207633862205009e366567bdf Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Thu, 30 Jul 2026 05:16:15 +0200 Subject: [PATCH 4/4] ci(studio): require timeline gate evidence artifacts --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 347281bce6..becb5b7109 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -592,7 +592,7 @@ jobs: with: name: timeline-viewport-gate-evidence path: /tmp/timeline-gate-*.json - if-no-files-found: warn + if-no-files-found: error smoke-global-install: name: "Smoke: global install"