From f29042e731ec5a096b6aad70136681b0f4fb84cb Mon Sep 17 00:00:00 2001 From: Omri Katz <9701896+omridevk@users.noreply.github.com> Date: Wed, 12 Aug 2026 18:10:22 +0300 Subject: [PATCH] fix(e2e): deploy packed apps before the webServer phase and without fresh resolution (#435) pnpm --filter deploy --legacy discards the workspace lockfile and does a fully independent resolution of the whole 2700+ package workspace graph, previously run inside playwright's webServer.command alongside next dev, all under one 180s timeout. On CI the resolution phase alone (individual registry metadata requests, 10-30s each) could exhaust the budget before next dev (which boots in under a second) ever started. - deployPackedApp() (e2e/e2e-utils/src/deploy.ts) runs the legacy deploy with --prefer-offline, which skips the per-package staleness revalidation GET that was the actual bottleneck, while still falling back to a live fetch for anything not cached (unlike --offline, which hard-errors the moment a workspace-wide "latest" specifier floats to a version nothing has fetched yet - reproduced locally resolving @neodrag/solid via a newer @tanstack/react-devtools). CONCIV_DEPLOY_FRESH=1 opts back into a live resolve, mirroring #378's CONCIV_PACKED_FRESH=1. - Each app's test:e2e script now runs the deploy as a pretest step (`pnpm --filter @conciv/e2e-utils run deploy && playwright test`), before playwright's webServer phase even starts. webServer.command is now just `cd && next dev`, and e2eConfig() grew a webServerTimeout option so nextjs/nextjs-component can tighten their webServer budget to 60s without touching the other e2e apps' 180s default. Co-Authored-By: Claude Fable 5 --- e2e/e2e-utils/package.json | 4 ++- e2e/e2e-utils/src/config.ts | 22 ++++++++++---- e2e/e2e-utils/src/deploy-cli.ts | 19 ++++++++++++ e2e/e2e-utils/src/deploy.ts | 36 +++++++++++++++++++++++ e2e/e2e-utils/src/ports.ts | 4 +++ e2e/nextjs-component/package.json | 2 +- e2e/nextjs-component/playwright.config.ts | 11 ++----- e2e/nextjs/package.json | 2 +- e2e/nextjs/playwright.config.ts | 11 ++----- pnpm-lock.yaml | 3 ++ turbo.json | 2 +- 11 files changed, 91 insertions(+), 25 deletions(-) create mode 100644 e2e/e2e-utils/src/deploy-cli.ts create mode 100644 e2e/e2e-utils/src/deploy.ts diff --git a/e2e/e2e-utils/package.json b/e2e/e2e-utils/package.json index 3dfbb0b2f..4d3a71a17 100644 --- a/e2e/e2e-utils/package.json +++ b/e2e/e2e-utils/package.json @@ -11,13 +11,15 @@ "./widget": "./src/widget.ts" }, "scripts": { - "typecheck": "tsc -p tsconfig.json --noEmit" + "typecheck": "tsc -p tsconfig.json --noEmit", + "deploy": "tsx src/deploy-cli.ts" }, "dependencies": { "@playwright/test": "^1.61.1" }, "devDependencies": { "@types/node": "^26.1.0", + "tsx": "^4.22.4", "typescript": "^6.0.3" } } diff --git a/e2e/e2e-utils/src/config.ts b/e2e/e2e-utils/src/config.ts index 24dff332d..79671c517 100644 --- a/e2e/e2e-utils/src/config.ts +++ b/e2e/e2e-utils/src/config.ts @@ -1,25 +1,33 @@ +import {tmpdir} from 'node:os' +import {join} from 'node:path' import {defineConfig, devices, type ReporterDescription} from '@playwright/test' import {E2E_PORTS, HARNESS_E2E_PORTS, type E2EApp, type HarnessApp} from './ports.js' +const DEFAULT_WEB_SERVER_TIMEOUT = 180_000 + function reporters(): ReporterDescription[] { if (!process.env.GITHUB_ACTIONS) return [['line']] return [['line'], ['json', {outputFile: 'test-results.json'}]] } -function serverEntry(command: string, port: number) { +function serverEntry(command: string, port: number, webServerTimeout: number) { return { command, url: `http://localhost:${port}`, reuseExistingServer: !process.env.CI, stdout: 'pipe' as const, - timeout: 180_000, + timeout: webServerTimeout, env: {CONCIV_E2E: '1'}, } } +export function deployDir(app: E2EApp): string { + return join(tmpdir(), 'conciv-e2e-deploy', app) +} + export function e2eConfig( app: E2EApp, - opts: {command: (port: number) => string; timeout?: number}, + opts: {command: (port: number) => string; timeout?: number; webServerTimeout?: number}, ): ReturnType { const port = E2E_PORTS[app] const timeout = opts.timeout ?? 90_000 @@ -30,7 +38,11 @@ export function e2eConfig( expect: {timeout}, reporter: reporters(), use: {baseURL: `http://localhost:${port}`}, - webServer: serverEntry(`rm -rf .conciv && ${opts.command(port)}`, port), + webServer: serverEntry( + `rm -rf .conciv && ${opts.command(port)}`, + port, + opts.webServerTimeout ?? DEFAULT_WEB_SERVER_TIMEOUT, + ), projects: [{name: 'chromium', use: {...devices['Desktop Chrome']}}], }) } @@ -61,7 +73,7 @@ export function harnessMatrixConfig(opts: { timeout: 150_000, reporter: reporters(), webServer: entries.map(([harness, port]) => - serverEntry(`rm -rf .conciv-${harness} && ${opts.command(harness, port)}`, port), + serverEntry(`rm -rf .conciv-${harness} && ${opts.command(harness, port)}`, port, DEFAULT_WEB_SERVER_TIMEOUT), ), projects: entries.map(([harness, port]) => ({ name: harness, diff --git a/e2e/e2e-utils/src/deploy-cli.ts b/e2e/e2e-utils/src/deploy-cli.ts new file mode 100644 index 000000000..4bb32a5d5 --- /dev/null +++ b/e2e/e2e-utils/src/deploy-cli.ts @@ -0,0 +1,19 @@ +import {deployPackedApp} from './deploy.js' +import {isE2EApp} from './ports.js' + +async function main(): Promise { + const [, , app, pnpmFilter] = process.argv + if (app === undefined || pnpmFilter === undefined) { + throw new Error('usage: deploy-cli.ts ') + } + if (!isE2EApp(app)) { + throw new Error(`"${app}" is not a known e2e app`) + } + const target = await deployPackedApp(app, pnpmFilter) + console.log(`deployed ${pnpmFilter} to ${target}`) +} + +main().catch((error: unknown) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/e2e/e2e-utils/src/deploy.ts b/e2e/e2e-utils/src/deploy.ts new file mode 100644 index 000000000..519d201e1 --- /dev/null +++ b/e2e/e2e-utils/src/deploy.ts @@ -0,0 +1,36 @@ +import {execFile} from 'node:child_process' +import {existsSync, mkdirSync, rmSync} from 'node:fs' +import {dirname, join} from 'node:path' +import {promisify} from 'node:util' +import {deployDir} from './config.js' +import type {E2EApp} from './ports.js' + +const execFileAsync = promisify(execFile) + +function findWorkspaceRoot(startDir: string): string { + let dir = startDir + for (let depth = 0; depth < 12; depth++) { + if (existsSync(join(dir, 'pnpm-workspace.yaml'))) return dir + const parent = dirname(dir) + if (parent === dir) break + dir = parent + } + throw new Error(`workspace root (pnpm-workspace.yaml) not found above ${startDir}`) +} + +function resolveFresh(): boolean { + return process.env.CONCIV_DEPLOY_FRESH === '1' +} + +export async function deployPackedApp(app: E2EApp, pnpmFilter: string): Promise { + const target = deployDir(app) + rmSync(target, {recursive: true, force: true}) + mkdirSync(dirname(target), {recursive: true}) + const resolveFlags = resolveFresh() ? [] : ['--prefer-offline'] + const workspaceRoot = findWorkspaceRoot(process.cwd()) + await execFileAsync('pnpm', ['--filter', pnpmFilter, 'deploy', '--legacy', '--prod=false', ...resolveFlags, target], { + cwd: workspaceRoot, + maxBuffer: 64 * 1024 * 1024, + }) + return target +} diff --git a/e2e/e2e-utils/src/ports.ts b/e2e/e2e-utils/src/ports.ts index 06e9622c9..d5012133c 100644 --- a/e2e/e2e-utils/src/ports.ts +++ b/e2e/e2e-utils/src/ports.ts @@ -16,6 +16,10 @@ export const E2E_PORTS = { export type E2EApp = keyof typeof E2E_PORTS +export function isE2EApp(name: string): name is E2EApp { + return Object.hasOwn(E2E_PORTS, name) +} + export const HARNESS_E2E_PORTS = { claude: 5271, codex: 5272, diff --git a/e2e/nextjs-component/package.json b/e2e/nextjs-component/package.json index fa3af59bb..8d4975847 100644 --- a/e2e/nextjs-component/package.json +++ b/e2e/nextjs-component/package.json @@ -6,7 +6,7 @@ "dev": "next dev", "build": "next build", "start": "next start", - "test:e2e": "playwright test" + "test:e2e": "pnpm --filter @conciv/e2e-utils run deploy nextjs-component conciv-e2e-nextjs-component && playwright test" }, "dependencies": { "@conciv/extension-terminal": "workspace:*", diff --git a/e2e/nextjs-component/playwright.config.ts b/e2e/nextjs-component/playwright.config.ts index 312d39264..07b9fcd97 100644 --- a/e2e/nextjs-component/playwright.config.ts +++ b/e2e/nextjs-component/playwright.config.ts @@ -1,11 +1,6 @@ -import {e2eConfig} from '@conciv/e2e-utils/config' +import {deployDir, e2eConfig} from '@conciv/e2e-utils/config' export default e2eConfig('nextjs-component', { - command: (port) => - [ - 'DEPLOY_DIR="$(mktemp -d)/app"', - `pnpm --filter conciv-e2e-nextjs-component deploy --legacy --prod=false "$DEPLOY_DIR"`, - `cd "$DEPLOY_DIR"`, - `pnpm exec next dev --port ${port}`, - ].join(' && '), + command: (port) => `cd "${deployDir('nextjs-component')}" && pnpm exec next dev --port ${port}`, + webServerTimeout: 60_000, }) diff --git a/e2e/nextjs/package.json b/e2e/nextjs/package.json index c72c8e153..bb585b74e 100644 --- a/e2e/nextjs/package.json +++ b/e2e/nextjs/package.json @@ -6,7 +6,7 @@ "dev": "next dev", "build": "next build", "start": "next start", - "test:e2e": "playwright test", + "test:e2e": "pnpm --filter @conciv/e2e-utils run deploy nextjs conciv-e2e-nextjs && playwright test", "update-packed-lockfile": "tsx packed/update-lockfile.ts" }, "dependencies": { diff --git a/e2e/nextjs/playwright.config.ts b/e2e/nextjs/playwright.config.ts index f9039e67d..5bbc146a5 100644 --- a/e2e/nextjs/playwright.config.ts +++ b/e2e/nextjs/playwright.config.ts @@ -1,11 +1,6 @@ -import {e2eConfig} from '@conciv/e2e-utils/config' +import {deployDir, e2eConfig} from '@conciv/e2e-utils/config' export default e2eConfig('nextjs', { - command: (port) => - [ - 'DEPLOY_DIR="$(mktemp -d)/app"', - `pnpm --filter conciv-e2e-nextjs deploy --legacy --prod=false "$DEPLOY_DIR"`, - `cd "$DEPLOY_DIR"`, - `pnpm exec next dev --port ${port}`, - ].join(' && '), + command: (port) => `cd "${deployDir('nextjs')}" && pnpm exec next dev --port ${port}`, + webServerTimeout: 60_000, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d76ef670b..8770d30e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -755,6 +755,9 @@ importers: '@types/node': specifier: ^26.1.0 version: 26.1.0 + tsx: + specifier: ^4.22.4 + version: 4.22.4 typescript: specifier: ^6.0.3 version: 6.0.3 diff --git a/turbo.json b/turbo.json index 6ee92555a..761ca010c 100644 --- a/turbo.json +++ b/turbo.json @@ -59,7 +59,7 @@ "test:e2e": { "dependsOn": ["^build"], "cache": false, - "env": ["CI", "GITHUB_ACTIONS", "CONCIV_E2E", "VITEST_MAX_FORKS", "VITEST_MAX_WORKERS"] + "env": ["CI", "GITHUB_ACTIONS", "CONCIV_E2E", "CONCIV_DEPLOY_FRESH", "VITEST_MAX_FORKS", "VITEST_MAX_WORKERS"] } } }