diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54cc5cd97f..becb5b7109 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,97 @@ 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. + # 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 \ + || 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 \ + || 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 + 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. + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: timeline-viewport-gate-evidence + path: /tmp/timeline-gate-*.json + if-no-files-found: error + 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);