diff --git a/.github/actions/setup-test-env/action.yml b/.github/actions/setup-test-env/action.yml index ea2cc37324..2a3e3c9099 100644 --- a/.github/actions/setup-test-env/action.yml +++ b/.github/actions/setup-test-env/action.yml @@ -11,71 +11,99 @@ inputs: autokitteh-image: description: "AutoKitteh Docker image" required: true + workers: + description: "Number of Playwright workers (each worker gets its own backend container)" + required: false + default: "4" + +outputs: + backend-base-port: + description: "The base port for backend containers (worker 0 uses this port, worker 1 uses port+1, etc.)" + value: "9980" runs: using: "composite" steps: - - name: Start AutoKitteh container + - name: Start AutoKitteh containers (one per worker) shell: bash env: DESCOPE_PROJECT_ID: ${{ inputs.descope-project-id }} + WORKERS: ${{ inputs.workers }} run: | - echo "Starting AutoKitteh container..." - - # Start docker command in background and get its PID - CONTAINER_ID=$(timeout 900 docker run -d \ - -p 9980:9980 \ - -e AK_AUTHHTTPMIDDLEWARE__USE_DEFAULT_USER="false" \ - -e AK_AUTHLOGINHTTPSVC__DESCOPE__ENABLED="true" \ - -e AK_HTTP__CORS__ALLOWED_ORIGINS="http://localhost:8000" \ - -e AK_AUTHSESSIONS__ALLOWED_CORS_COOKIE="true" \ - -e AK_AUTHLOGINHTTPSVC__DESCOPE__PROJECT_ID="$DESCOPE_PROJECT_ID" \ - -e AK_AUTHSESSIONS__COOKIE_KEYS="78d85f1c9fd9df7d3d459a75f1db315ef634dc854ba90bc5add3e6cb6f135bd6,d9591b1ab2d0e5de1fef96a5a8a50b883430884211f16a206f84ad57897f99d5" \ - -e AK_AUTHJWTTOKENS__HMAC__SIGN_KEY="183b1c8f4c64b3450907b3859be0b94044d3c92a3116f02213585a85dd0cb154" \ - -e AK_AUTHJWTTOKENS__ALGORITHM="hmac" \ - ${{ inputs.autokitteh-image }} up --mode=dev --config db.seed_commands="insert into orgs(created_by, created_at, org_id, display_name, name, updated_by, updated_at) values('7bcbb000-0000-0000-0000-000000000000', '2025-01-06 09:47:45.348066+00:00', '01943b03-6344-702a-9471-1b0098752177', 'Tests''s Personal Org', 'tests_personal_org', '00000000-0000-0000-0000-000000000000', '2025-01-06 11:47:45.348369+02:00');insert into users(user_id, email, display_name, created_by, created_at, default_org_id, updated_by, updated_at,status) values('01943b03-6345-7606-8e13-e319ae2f1929', 'test@autokitteh.com', 'Tests User', '7bcbb000-0000-0000-0000-000000000000', '2025-01-06 09:47:45.349399+00:00', '01943b03-6344-702a-9471-1b0098752177', '00000000-0000-0000-0000-000000000000', '2025-01-06 11:47:45.349424+02:00',1);insert into org_members(created_by, created_at, org_id, user_id, status, roles) values('00000000-0000-0000-0000-000000000000', '2025-01-06 11:47:45.350038+02:00', '01943b03-6344-702a-9471-1b0098752177', '01943b03-6345-7606-8e13-e319ae2f1929', 1, '[\"admin\"]');") || { - echo "Container startup failed or timed out after 900 seconds (15 minutes)" - exit 1 - } - echo "CONTAINER_ID=$CONTAINER_ID" >> $GITHUB_ENV - echo "AutoKitteh container started successfully with ID: $CONTAINER_ID" - - - name: Setup logging + BASE_PORT=9980 + CONTAINER_IDS="" + + echo "๐Ÿš€ Starting $WORKERS AutoKitteh containers (one per Playwright worker)..." + + for ((w=0; w> $GITHUB_ENV + echo "WORKERS=$WORKERS" >> $GITHUB_ENV + echo "BASE_PORT=$BASE_PORT" >> $GITHUB_ENV + echo "โœ… All $WORKERS containers started successfully" + + - name: Setup logging for all containers shell: bash run: | - docker logs -f ${{ env.CONTAINER_ID }} > docker-logs.txt & - echo "LOGS_PID=$!" >> $GITHUB_ENV + for ((w=0; w<${{ env.WORKERS }}; w++)); do + docker logs -f "ak-worker-$w" > "docker-logs-worker-$w.txt" 2>&1 & + done + echo "๐Ÿ“‹ Logging enabled for all containers" - - name: Wait for Backend + - name: Wait for all backends shell: bash run: | source "${{ github.action_path }}/scripts/progress-bar.sh" - URL=http://localhost:9980 - echo "Waiting for AutoKitteh backend to start..." - TIMEOUT=300 # 5 minutes timeout + TIMEOUT=300 ITERATIONS=$((TIMEOUT / 2)) - for ((i=1; i<=ITERATIONS; i++)); do - if curl --output /dev/null --silent --head --fail -X POST $URL; then - printf "\rโœ… AutoKitteh backend ready in $((i * 2)) seconds! \n" - break - fi + echo "โณ Waiting for ${{ env.WORKERS }} backends to be ready..." - if [[ -t 1 ]]; then - ProgressBar ${i} ${ITERATIONS} "AutoKitteh" - else - echo "Waiting for AutoKitteh... ($((i * 2))s elapsed)" - fi + for ((w=0; w<${{ env.WORKERS }}; w++)); do + PORT=$((${{ env.BASE_PORT }} + w)) + URL="http://localhost:${PORT}" + echo "Waiting for backend on port $PORT (worker $w)..." - if [ $i -eq $ITERATIONS ]; then - printf "\nโŒ Timeout waiting for backend after $TIMEOUT seconds\n" - exit 1 - fi + for ((i=1; i<=ITERATIONS; i++)); do + if curl --output /dev/null --silent --head --fail -X POST $URL; then + echo "โœ… Backend for worker $w ready on port $PORT" + break + fi + + if [ $i -eq $ITERATIONS ]; then + echo "โŒ Timeout waiting for backend on port $PORT" + exit 1 + fi - sleep 2 + sleep 2 + done done + echo "๐ŸŽ‰ All ${{ env.WORKERS }} backends are ready!" + # - name: Install browser-specific dependencies # if: inputs.browser == 'Safari' # shell: bash diff --git a/.github/workflows/build_test_and_release.yml b/.github/workflows/build_test_and_release.yml index 141c046883..50ac43d934 100644 --- a/.github/workflows/build_test_and_release.yml +++ b/.github/workflows/build_test_and_release.yml @@ -70,16 +70,55 @@ jobs: path: ~/.cache/ms-playwright key: playwright-${{ hashFiles('package-lock.json') }} - test: - name: ๐Ÿงช Testing ${{ matrix.browser }} + test-build: + name: ๐Ÿ—๏ธ Build for Tests needs: setup runs-on: ubuntu-latest if: github.ref != 'refs/heads/main' - timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "npm" + + - name: Restore cached node_modules + uses: actions/cache/restore@v4 + with: + path: node_modules + key: node-modules-${{ hashFiles('package-lock.json') }} + + - name: Install dependencies (fallback) + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: npm ci + + - name: Build for tests + env: + VITE_DESCOPE_PROJECT_ID: ${{ secrets.VITE_DESCOPE_PROJECT_ID }} + VITE_DISPLAY_CHATBOT: true + run: npm run build + + - name: Upload test build + uses: actions/upload-artifact@v4 + with: + name: test-built-files + path: dist + retention-days: 1 + + test: + name: ๐Ÿงช ${{ matrix.browser }} (shard ${{ matrix.shard }}/4) + needs: [setup, test-build] + runs-on: ubuntu-latest + if: github.ref != 'refs/heads/main' + timeout-minutes: 30 strategy: matrix: browser: ["Chrome", "Edge", "Firefox", "Safari"] - fail-fast: true + shard: [1, 2, 3, 4] + fail-fast: false steps: - uses: actions/checkout@v4 @@ -109,6 +148,12 @@ jobs: path: ~/.cache/ms-playwright key: playwright-${{ hashFiles('package-lock.json') }} + - name: Download test build + uses: actions/download-artifact@v4 + with: + name: test-built-files + path: ./dist + - name: Cache Docker images id: cache-docker uses: ./.github/actions/cache-docker-images @@ -122,7 +167,7 @@ jobs: if: always() run: | { - echo "## ๐Ÿ—‚๏ธ Docker Cache Metrics (${{ matrix.browser }})" + echo "## ๐Ÿ—‚๏ธ Docker Cache Metrics (${{ matrix.browser }} shard ${{ matrix.shard }})" echo "" echo "| Metric | Value |" echo "|--------|-------|" @@ -140,6 +185,7 @@ jobs: descope-project-id: ${{ secrets.VITE_DESCOPE_PROJECT_ID }} browser: ${{ matrix.browser }} autokitteh-image: ${{ vars.AUTOKITTEH_IMAGE }} + workers: "4" - name: Generate connection test data run: npm run generate:connection-test-data @@ -147,16 +193,13 @@ jobs: - name: Run Playwright tests env: TESTS_JWT_AUTH_TOKEN: ${{ secrets.TESTS_JWT_AUTH_TOKEN }} - VITE_DESCOPE_PROJECT_ID: ${{ secrets.VITE_DESCOPE_PROJECT_ID }} - VITE_DISPLAY_CHATBOT: true - CI: true - run: npx playwright test --project=${{ matrix.browser }} + run: npx playwright test --project=${{ matrix.browser }} --shard=${{ matrix.shard }}/4 - name: Upload test artifacts if: always() uses: ./.github/actions/upload-test-artifacts with: - browser: ${{ matrix.browser }} + browser: ${{ matrix.browser }}-shard-${{ matrix.shard }} run-id: ${{ github.run_id }} visual-regression: diff --git a/e2e/fixtures.ts b/e2e/fixtures.ts index 4beb5ea73a..0da29b5fbc 100644 --- a/e2e/fixtures.ts +++ b/e2e/fixtures.ts @@ -6,15 +6,45 @@ import { OrgConnectionsPage, ConnectionsConfig, DashboardPage, ProjectPage } fro // eslint-disable-next-line @typescript-eslint/naming-convention const RATE_LIMIT_DELAY = process.env.E2E_RATE_LIMIT_DELAY ? parseInt(process.env.E2E_RATE_LIMIT_DELAY, 10) : 0; +// eslint-disable-next-line @typescript-eslint/naming-convention +const BACKEND_BASE_PORT = 9980; + type PageFixtureArgs = { page: Page }; type UseFixture = (fixture: T) => Promise; -const test = base.extend<{ - connectionsConfig: ConnectionsConfig; - dashboardPage: DashboardPage; - orgConnectionsPage: OrgConnectionsPage; - projectPage: ProjectPage; -}>({ +const test = base.extend< + { + connectionsConfig: ConnectionsConfig; + dashboardPage: DashboardPage; + orgConnectionsPage: OrgConnectionsPage; + projectPage: ProjectPage; + }, + { workerBackendPort: number } +>({ + workerBackendPort: [ + // eslint-disable-next-line no-empty-pattern + async ({}, use, workerInfo) => { + const port = BACKEND_BASE_PORT + workerInfo.workerIndex; + await use(port); + }, + { scope: "worker" }, + ], + + page: async ({ page, workerBackendPort }, use) => { + if (workerBackendPort !== BACKEND_BASE_PORT) { + await page.route(`http://localhost:${BACKEND_BASE_PORT}/**`, async (route) => { + const originalUrl = route.request().url(); + const newUrl = originalUrl.replace( + `http://localhost:${BACKEND_BASE_PORT}`, + `http://localhost:${workerBackendPort}` + ); + const response = await route.fetch({ url: newUrl }); + await route.fulfill({ response }); + }); + } + await use(page); + }, + connectionsConfig: async ({ page }: PageFixtureArgs, use: UseFixture) => { await use(new ConnectionsConfig(page)); }, diff --git a/playwright.config.ts b/playwright.config.ts index 8786f584a5..8913ee0438 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -26,7 +26,7 @@ export default defineConfig({ maxFailures: process.env.CI ? 1 : undefined, - workers: process.env.CI ? 1 : 2, + workers: process.env.CI ? 4 : 2, /* Configure projects for major browsers */ projects: [ @@ -88,7 +88,7 @@ export default defineConfig({ timeout: 60 * 1000 * 6, // 6 minutes timeout for each test - retries: process.env.CI ? 2 : 0, // 2 retries for CI, 0 for local + retries: process.env.CI ? 1 : 0, // 1 retry for CI, 0 for local /* Visual regression test settings */ expect: { @@ -119,15 +119,15 @@ export default defineConfig({ viewport: { width: 1920, height: 1080 }, - actionTimeout: 3000, + actionTimeout: 6000, }, webServer: { - command: `npm run build && npm run preview`, + command: process.env.CI ? `npm run preview` : `npm run build && npm run preview`, port: previewPort, reuseExistingServer: !process.env.CI, stderr: "pipe", stdout: "pipe", - timeout: 5 * 60 * 1000, // 300,000 ms = 5 minutes (CI builds can be slow) + timeout: process.env.CI ? 60 * 1000 : 5 * 60 * 1000, }, });