name: 'Cross-Platform E2E Tests' on: push: branches: - master paths-ignore: - '**/*.md' - 'docs/**' - '.plans/**' - '.codex/**' - '.claude/**' - 'apps/website/**' pull_request: branches: - master paths-ignore: - '**/*.md' - 'docs/**' - '.plans/**' - '.codex/**' - '.claude/**' - 'apps/website/**' workflow_dispatch: # Superseded PR pushes cancel their still-running E2E matrix (the most # expensive per-PR runner time). Non-PR runs get a unique group (run_id): # GitHub keeps at most one pending run per group even with # cancel-in-progress: false, so a shared ref group could silently drop a # queued master run between two rapid pushes. concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} permissions: contents: read jobs: electron-e2e-tests: name: Electron E2E on ${{ matrix.os }} (${{ matrix.shard }}/${{ matrix.shard-total }}) runs-on: ${{ matrix.os }} # The sequential Electron suite is split into Playwright shards (one # runner each, split by spec file). Measured on 2026-09-26, a third of # the suite is 6-12 minutes of test time on top of 3-7 minutes of # setup. macOS uses two shards because its runners are the scarcest: # three macOS shards per run queued for 45-57 minutes on master. Half # of the suite is about 15 minutes of test time on macOS, so it gets # a longer timeout to leave room for retries. timeout-minutes: ${{ matrix.os == 'macos-latest' && 40 || 30 }} env: IPTVNATOR_ALLOW_PRIVATE_NETWORK_URLS: '1' NX_SKIP_NX_CACHE: true strategy: fail-fast: false matrix: include: - { os: ubuntu-latest, shard: 1, shard-total: 3 } - { os: ubuntu-latest, shard: 2, shard-total: 3 } - { os: ubuntu-latest, shard: 3, shard-total: 3 } - { os: macos-latest, shard: 1, shard-total: 2 } - { os: macos-latest, shard: 2, shard-total: 2 } - { os: windows-latest, shard: 1, shard-total: 3 } - { os: windows-latest, shard: 2, shard-total: 3 } - { os: windows-latest, shard: 3, shard-total: 3 } steps: - uses: actions/checkout@v7 - name: Install pnpm uses: pnpm/action-setup@v6.1.0 - name: Setup Node.js uses: actions/setup-node@v7 with: node-version-file: '.nvmrc' cache: 'pnpm' - name: Install Dependencies run: pnpm install --frozen-lockfile - name: Build Backend run: pnpm nx build electron-backend # Process-lifecycle checks are independent of the shard split; # run them once per OS. - name: Verify Electron process cleanup if: matrix.shard == 1 run: pnpm exec tsx --test apps/electron-backend-e2e/src/performance/electron-process-lifecycle.spec.ts apps/electron-backend-e2e/src/performance/electron-process-termination.spec.ts # The suite drives Electron through Playwright's _electron API and # never launches a Playwright browser (or records video, which # would need Playwright's ffmpeg), so no `playwright install`. # The Ubuntu runner image already ships Electron's shared # libraries and xvfb; fail fast with a clear message if an image # update ever drops one. Electron 42+ downloads its binary on the # first `require('electron')` (previously inside # `_electron.launch()`), so resolve it the same way first. - name: Check Electron runtime dependencies if: runner.os == 'Linux' run: | command -v xvfb-run || { echo "::error::xvfb-run is missing on the runner"; exit 1; } node -e "require('electron')" || { echo "::error::Electron binary download failed"; exit 1; } electron_bin="$(node -p "require('electron')")" [ -x "$electron_bin" ] || { echo "::error::Electron binary not found at $electron_bin"; exit 1; } ldd_status=0 libs="$(ldd "$electron_bin")" || ldd_status=$? missing="$(grep 'not found' <<< "$libs" || true)" if [ -n "$missing" ]; then echo "::error::Electron is missing shared libraries:" echo "$missing" exit 1 fi [ "$ldd_status" -eq 0 ] || { echo "::error::ldd failed on $electron_bin (exit $ldd_status)"; exit 1; } - name: Run Electron E2E Tests (Linux) if: runner.os == 'Linux' run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" pnpm nx run electron-backend-e2e:e2e -- --shard=${{ matrix.shard }}/${{ matrix.shard-total }} env: CI: true - name: Run Electron E2E Tests (Windows/Mac) if: runner.os != 'Linux' run: pnpm nx run electron-backend-e2e:e2e -- --shard=${{ matrix.shard }}/${{ matrix.shard-total }} env: CI: true # Each shard's results.json carries config.shard; the summary job # below merges the shards of one OS into a single semantic summary. - name: Upload Electron Test Results if: always() uses: actions/upload-artifact@v7 with: name: playwright-report-electron-${{ matrix.os }}-${{ matrix.shard }} path: | dist/playwright-report/electron-backend-e2e/ dist/test-results/electron-backend-e2e/ retention-days: 7 electron-e2e-summary: name: Electron E2E summary runs-on: ubuntu-latest needs: electron-e2e-tests # Summarize failed shards too, but not a cancelled (superseded) run. if: ${{ !cancelled() }} timeout-minutes: 10 steps: - uses: actions/checkout@v7 - name: Setup Node.js uses: actions/setup-node@v7 with: node-version-file: '.nvmrc' # One download per OS keeps each OS's shards in their own directory # (the artifact paths inside the shards are identical). - name: Download shard reports (Ubuntu) uses: actions/download-artifact@v8 with: pattern: playwright-report-electron-ubuntu-latest-* path: dist/e2e-shards/ubuntu-latest - name: Download shard reports (macOS) uses: actions/download-artifact@v8 with: pattern: playwright-report-electron-macos-latest-* path: dist/e2e-shards/macos-latest - name: Download shard reports (Windows) uses: actions/download-artifact@v8 with: pattern: playwright-report-electron-windows-latest-* path: dist/e2e-shards/windows-latest # The script fails when a shard's results.json is missing or # duplicated, so a partial run is never summarized as complete. - name: Summarize Electron E2E semantic coverage per OS run: | status=0 for os in ubuntu-latest macos-latest windows-latest; do echo "## Electron E2E on $os" >> "$GITHUB_STEP_SUMMARY" node tools/coverage/e2e-semantic-summary.mjs --project=electron-backend-e2e --input="dist/e2e-shards/$os" --output-dir="coverage/e2e/$os" || status=1 done exit "$status" - name: Upload Electron semantic summaries if: always() uses: actions/upload-artifact@v7 with: name: e2e-semantic-summary-electron path: coverage/e2e/ retention-days: 7 web-e2e-tests: name: Web E2E on Ubuntu Chromium runs-on: ubuntu-latest env: NX_SKIP_NX_CACHE: true steps: - uses: actions/checkout@v7 - name: Install pnpm uses: pnpm/action-setup@v6.1.0 - name: Setup Node.js uses: actions/setup-node@v7 with: node-version-file: '.nvmrc' cache: 'pnpm' - name: Install Dependencies run: pnpm install --frozen-lockfile - name: Install Chromium run: pnpm exec playwright install --with-deps chromium - name: Run Web E2E Tests run: pnpm nx run web-e2e:e2e -- --project=chromium env: CI: true - name: Summarize Web E2E semantic coverage if: always() run: pnpm run coverage:e2e:summary -- --project=web-e2e - name: Upload Web Test Results if: always() uses: actions/upload-artifact@v7 with: name: playwright-report-web-ubuntu path: | dist/playwright-report/web-e2e/ dist/test-results/web-e2e/ coverage/e2e/ retention-days: 7