diff --git a/.github/actions/setup-test-env/action.yml b/.github/actions/setup-test-env/action.yml index 7c3b3097008e..cffa555a49e8 100644 --- a/.github/actions/setup-test-env/action.yml +++ b/.github/actions/setup-test-env/action.yml @@ -7,6 +7,10 @@ inputs: aws-region: description: "The AWS region for S3" required: true + workers: + description: "Number of Playwright workers (used to size per-worker Xvfb displays)" + required: false + default: "1" runs: using: "composite" @@ -25,3 +29,5 @@ runs: - name: Setup Xvfb uses: ./.github/actions/setup-xvfb + with: + workers: ${{ inputs.workers }} diff --git a/.github/actions/setup-xvfb/action.yml b/.github/actions/setup-xvfb/action.yml index 859cf9371075..568aae15e4e6 100644 --- a/.github/actions/setup-xvfb/action.yml +++ b/.github/actions/setup-xvfb/action.yml @@ -1,27 +1,51 @@ name: "Setup Xvfb" -description: "Configure and start Xvfb virtual framebuffer" +description: "Configure and start Xvfb virtual framebuffer, one display per Playwright worker" + +inputs: + workers: + description: "Number of Xvfb displays to start (one per Playwright worker)" + required: false + default: "1" runs: using: "composite" steps: - name: Setup Xvfb shell: bash + env: + WORKERS: ${{ inputs.workers }} run: | - # Start Xvfb in the background - /usr/bin/Xvfb :10 -ac -screen 0 2560x1440x24 > /tmp/Xvfb.out 2>&1 & + # Fail fast on invalid input - otherwise `seq 0 -1` silently starts zero + # Xvfb instances and the ready-check reports success with no displays. + [[ "$WORKERS" =~ ^[1-9][0-9]*$ ]] || { echo "WORKERS must be a positive integer, got: '$WORKERS'"; exit 1; } + + # Start one Xvfb instance per worker starting at :10, so Playwright workers + # don't race on a shared X server. test/e2e/infra/electron.ts maps each + # worker's parallelIndex to DISPLAY=:(10+parallelIndex). + for i in $(seq 0 $((WORKERS - 1))); do + display=":$((10 + i))" + /usr/bin/Xvfb "$display" -ac -screen 0 2560x1440x24 > "/tmp/Xvfb.$i.out" 2>&1 & + done - # Wait until Xvfb is ready + # Wait until every display is ready (default DISPLAY=:10 used by non-Playwright steps). export DISPLAY=:10 - for i in {1..10}; do - if xdpyinfo > /dev/null 2>&1; then - echo "Xvfb is ready" + for attempt in {1..15}; do + ready=true + for i in $(seq 0 $((WORKERS - 1))); do + if ! DISPLAY=":$((10 + i))" xdpyinfo > /dev/null 2>&1; then + ready=false + break + fi + done + if [ "$ready" = true ]; then + echo "All $WORKERS Xvfb display(s) ready" break fi - echo "Waiting for Xvfb to start..." + echo "Waiting for Xvfb displays to start..." sleep 1 done - # Export the DISPLAY variable for subsequent steps + # Export the default DISPLAY for any subsequent step that doesn't go through Playwright. echo "DISPLAY=:10" >> $GITHUB_ENV # Start a proper dbus session bus for Electron/Chromium diff --git a/.github/workflows/test-e2e-ubuntu-run.yml b/.github/workflows/test-e2e-ubuntu-run.yml index 1a790fc3e1fe..22092d3c2146 100644 --- a/.github/workflows/test-e2e-ubuntu-run.yml +++ b/.github/workflows/test-e2e-ubuntu-run.yml @@ -241,6 +241,7 @@ jobs: with: aws-role-to-assume: ${{ secrets.QA_AWS_RO_ROLE }} aws-region: ${{ secrets.QA_AWS_REGION }} + workers: ${{ inputs.workers }} # - name: Log Shard Distribution Info # run: | diff --git a/.github/workflows/test-e2e-ubuntu.yml b/.github/workflows/test-e2e-ubuntu.yml index 6b9484835db1..c93bddb4dc03 100644 --- a/.github/workflows/test-e2e-ubuntu.yml +++ b/.github/workflows/test-e2e-ubuntu.yml @@ -230,6 +230,7 @@ jobs: with: aws-role-to-assume: ${{ secrets.QA_AWS_RO_ROLE }} aws-region: ${{ secrets.QA_AWS_REGION }} + workers: ${{ inputs.workers }} # Preloading ensures the Node.js binary is fully built and ready before # any parallel processes start, preventing runtime conflicts diff --git a/.github/workflows/test-e2e-windows-build.yml b/.github/workflows/test-e2e-windows-build.yml new file mode 100644 index 000000000000..ff28b5fa152b --- /dev/null +++ b/.github/workflows/test-e2e-windows-build.yml @@ -0,0 +1,201 @@ +name: "E2E: Build Positron (Windows)" + +on: + workflow_call: + inputs: + commit: + description: "Commit SHA to test" + required: false + default: "" + type: string + skip_cache: + required: false + description: "Skip restoring caches (use when testing a specific commit)" + type: boolean + default: false + save_cache: + required: false + description: "Whether to save caches after the build (only one workflow per run should save to avoid race conditions)" + type: boolean + default: false + +permissions: + id-token: write + contents: read + +jobs: + build-windows: + name: build-windows + timeout-minutes: 60 + runs-on: windows-latest-8x + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + POSITRON_BUILD_NUMBER: 0 + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + submodules: recursive + + - name: Checkout specific commit + if: ${{ inputs.commit != '' }} + run: git checkout ${{ inputs.commit }} + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + + # Defender's real-time scanning hits every file created during cache + # restore, npm install (100k+ files), compile, and Playwright install. + # Cover workspace, parent, and both possible drive roots since the + # GitHub Actions Windows runner uses D:\a (and sometimes C:\a). + - name: Disable Defender scanning for build paths + shell: pwsh + run: | + $workspace = "${{ github.workspace }}" + $parent = Split-Path -Parent $workspace + Add-MpPreference -ExclusionPath "${{ runner.temp }}" -ErrorAction SilentlyContinue + Add-MpPreference -ExclusionPath "$workspace" -ErrorAction SilentlyContinue + Add-MpPreference -ExclusionPath "$parent" -ErrorAction SilentlyContinue + Add-MpPreference -ExclusionPath "D:\a" -ErrorAction SilentlyContinue + Add-MpPreference -ExclusionPath "C:\a" -ErrorAction SilentlyContinue + Write-Host "Defender exclusions applied:" + Write-Host " - ${{ runner.temp }}" + Write-Host " - $workspace" + Write-Host " - $parent" + Write-Host " - D:\a, C:\a" + (Get-MpPreference).ExclusionPath | ForEach-Object { Write-Host " [verified] $_" } + + - name: 📦 Restore caches + if: ${{ !inputs.skip_cache }} + id: restore-caches + uses: ./.github/actions/restore-build-caches + + - name: Install node dependencies + if: steps.restore-caches.outputs.cache-npm-core-hit != 'true' || + steps.restore-caches.outputs.cache-npm-extensions-volatile-hit != 'true' || + steps.restore-caches.outputs.cache-npm-extensions-stable-hit != 'true' + uses: nick-fields/retry@v4 + env: + npm_config_arch: x64 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + POSITRON_GITHUB_RO_PAT: ${{ github.token }} + POSITRON_PARALLEL_INSTALL: '0' # Disabled on Windows due to native module file locking races + POSITRON_NPM_CONCURRENCY: '30' # Unused when parallel install is disabled + NPM_CONFIG_AUDIT: 'false' # Skip security audit during install (speeds up install) + NPM_CONFIG_FUND: 'false' # Skip funding messages + NPM_CONFIG_UPDATE_NOTIFIER: 'false' # Skip update notifications + POSITRON_EXTENSIONS_FILTER: ${{ (steps.restore-caches.outputs.cache-npm-extensions-volatile-hit == 'true' && steps.restore-caches.outputs.cache-npm-extensions-stable-hit != 'true' && 'stable') || (steps.restore-caches.outputs.cache-npm-extensions-volatile-hit != 'true' && steps.restore-caches.outputs.cache-npm-extensions-stable-hit == 'true' && 'volatile') || '' }} + with: + timeout_minutes: 90 + max_attempts: 3 + retry_wait_seconds: 5 + shell: bash + command: | + set -e + npm --prefix build ci --no-audit --no-fund --fetch-timeout 120000 + npm ci --no-audit --no-fund --fetch-timeout 120000 + + - name: Download binaries (Ark, Kallichore) with retry + uses: nick-fields/retry@v4 + env: + POSITRON_GITHUB_RO_PAT: ${{ github.token }} + with: + timeout_minutes: 20 + max_attempts: 3 + retry_wait_seconds: 10 + shell: bash + command: | + set -e + cd extensions/positron-r && npm rebuild --foreground-scripts + cd ../positron-supervisor && npm rebuild --foreground-scripts + + - name: Compile Positron and Download Electron + run: npm exec -- npm-run-all --max-old-space-size=8192 -p compile "electron x64" + + - name: Install Playwright browsers + run: npm run playwright-install + + - name: Restore E2E test dependencies cache + id: cache-e2e + uses: actions/cache/restore@v5 + with: + path: test/e2e/node_modules + key: ${{ runner.os }}-e2e-v1-${{ hashFiles('test/e2e/package-lock.json') }} + + - name: Install E2E test dependencies + if: steps.cache-e2e.outputs.cache-hit != 'true' + run: npm --prefix test/e2e ci --no-audit --no-fund + + - name: Save E2E test dependencies cache + if: ${{ steps.cache-e2e.outputs.cache-hit != 'true' }} + continue-on-error: true + uses: actions/cache/save@v5 + with: + path: test/e2e/node_modules + key: ${{ runner.os }}-e2e-v1-${{ hashFiles('test/e2e/package-lock.json') }} + + - name: Compile E2E Tests + run: npm --prefix test/e2e run compile + + # Downloads Builtin Extensions (needed for integration & e2e testing) + - name: Download Builtin Extensions + shell: bash + run: npm run prelaunch + + - name: 💾 Save caches + if: ${{ inputs.save_cache }} + uses: ./.github/actions/save-build-caches + with: + cache-npm-core-hit: ${{ steps.restore-caches.outputs.cache-npm-core-hit }} + cache-npm-extensions-volatile-hit: ${{ steps.restore-caches.outputs.cache-npm-extensions-volatile-hit }} + cache-npm-extensions-stable-hit: ${{ steps.restore-caches.outputs.cache-npm-extensions-stable-hit }} + cache-builtins-hit: ${{ steps.restore-caches.outputs.cache-builtins-hit }} + cache-playwright-hit: ${{ steps.restore-caches.outputs.cache-playwright-hit }} + extensions-volatile-hash: ${{ steps.restore-caches.outputs.extensions-volatile-hash }} + extensions-stable-hash: ${{ steps.restore-caches.outputs.extensions-stable-hash }} + package-locks-hash: ${{ steps.restore-caches.outputs.package-locks-hash }} + node-version: ${{ steps.restore-caches.outputs.node-version }} + playwright-version: ${{ steps.restore-caches.outputs.playwright-version }} + + - name: Create Build Artifact + shell: bash + run: | + WORKSPACE=$(cygpath -u "${GITHUB_WORKSPACE}") + WORKSPACE_PARENT="$(dirname "${WORKSPACE}")" + WORKSPACE_NAME="$(basename "${WORKSPACE}")" + ARTIFACTS_DIR="$(cygpath -u "${RUNNER_TEMP}")/artifacts" + mkdir -p "${ARTIFACTS_DIR}" + echo "Creating build artifact from ${WORKSPACE}..." + # Tar the full workspace so shards skip npm ci and compile entirely. + # Exclude .git (shards restore their own via checkout) and npm/pip caches. + # Prefer zstd (faster compress+decompress, smaller output) with gzip fallback. + if command -v zstd >/dev/null 2>&1; then + echo "Using zstd (multithreaded) for compression" + tar -C "${WORKSPACE_PARENT}" \ + --exclude="${WORKSPACE_NAME}/.git" \ + --exclude="${WORKSPACE_NAME}/.npm-cache" \ + --use-compress-program='zstd -T0 -1' \ + -cf "${ARTIFACTS_DIR}/positron-build.tar.zst" \ + "${WORKSPACE_NAME}" + echo "Artifact size: $(du -sh "${ARTIFACTS_DIR}/positron-build.tar.zst")" + else + echo "zstd not available, falling back to gzip" + tar -C "${WORKSPACE_PARENT}" \ + --exclude="${WORKSPACE_NAME}/.git" \ + --exclude="${WORKSPACE_NAME}/.npm-cache" \ + -czf "${ARTIFACTS_DIR}/positron-build.tar.gz" \ + "${WORKSPACE_NAME}" + echo "Artifact size: $(du -sh "${ARTIFACTS_DIR}/positron-build.tar.gz")" + fi + + - name: Upload Build Artifact + uses: actions/upload-artifact@v7 + with: + name: positron-build-windows + path: ${{ runner.temp }}/artifacts + retention-days: 1 + compression-level: 0 # already compressed diff --git a/.github/workflows/test-e2e-windows-run.yml b/.github/workflows/test-e2e-windows-run.yml index 36b1625b29a2..01f66a587654 100644 --- a/.github/workflows/test-e2e-windows-run.yml +++ b/.github/workflows/test-e2e-windows-run.yml @@ -16,16 +16,6 @@ on: required: false default: "" type: string - skip_cache: - required: false - description: "Skip restoring caches (use when testing a specific commit)" - type: boolean - default: false - save_cache: - required: false - description: "Whether to save caches after the run (only one job per workflow should save to avoid race conditions)" - type: boolean - default: false grep: required: false description: "Only run tests matching this regex. Supports tags (comma-separated), titles, filenames. Confirm pattern matching locally with: npx playwright test --grep=" @@ -152,103 +142,60 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v7 - - name: 📦 Restore caches - if: ${{ !inputs.skip_cache }} - id: restore-caches - uses: ./.github/actions/restore-build-caches - - - name: Install node dependencies - if: steps.restore-caches.outputs.cache-npm-core-hit != 'true' || - steps.restore-caches.outputs.cache-npm-extensions-volatile-hit != 'true' || - steps.restore-caches.outputs.cache-npm-extensions-stable-hit != 'true' - uses: nick-fields/retry@v4 - env: - npm_config_arch: x64 - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - ELECTRON_SKIP_BINARY_DOWNLOAD: 1 - POSITRON_GITHUB_RO_PAT: ${{ github.token }} - POSITRON_PARALLEL_INSTALL: '0' # Disabled on Windows due to native module file locking races - POSITRON_NPM_CONCURRENCY: '30' # Unused when parallel install is disabled - NPM_CONFIG_AUDIT: 'false' # Skip security audit during install (speeds up install) - NPM_CONFIG_FUND: 'false' # Skip funding messages - NPM_CONFIG_UPDATE_NOTIFIER: 'false' # Skip update notifications - # Extension filter logic: - # volatile=hit, stable=miss → filter=stable (install stable only) - # volatile=miss, stable=hit → filter=volatile (install volatile only) - # both hit OR both miss → filter='' (install all or skip via parent condition) - POSITRON_EXTENSIONS_FILTER: ${{ (steps.restore-caches.outputs.cache-npm-extensions-volatile-hit == 'true' && steps.restore-caches.outputs.cache-npm-extensions-stable-hit != 'true' && 'stable') || (steps.restore-caches.outputs.cache-npm-extensions-volatile-hit != 'true' && steps.restore-caches.outputs.cache-npm-extensions-stable-hit == 'true' && 'volatile') || '' }} - with: - timeout_minutes: 90 - max_attempts: 3 - retry_wait_seconds: 5 - shell: bash - # Install strategy: - # 1. Pre-install build/ so postinstall can access gulp-merge-json - # 2. Run root npm ci which triggers postinstall (installs 60+ extensions in parallel, concurrency: 30) - # 3. Postinstall skips build/ since it's already installed (avoids duplicate work) - # Note: test/e2e is installed in separate step below (not cached, always runs) - # See https://github.com/posit-dev/positron/issues/3481 for why retry is needed (windows-process-tree failures) - command: | - set -e # Exit immediately if any command fails - - # Pre-install build directory (required by postinstall for gulp-merge-json) - npm --prefix build ci --no-audit --no-fund --fetch-timeout 120000 - - # Run root npm ci (triggers postinstall which installs all extensions) - npm ci --no-audit --no-fund --fetch-timeout 120000 - - echo "npm ci completed successfully" - - - name: Download binaries (Ark, Kallichore) with retry - uses: nick-fields/retry@v4 - env: - POSITRON_GITHUB_RO_PAT: ${{ github.token }} - with: - timeout_minutes: 20 - max_attempts: 3 - retry_wait_seconds: 10 - shell: bash - command: | - set -e # Exit immediately if any command fails - - # Download platform-specific binaries for Windows - # Wrapped in retry due to occasional GitHub API timeouts - cd extensions - echo "Downloading Ark binary..." - cd positron-r && npm rebuild --foreground-scripts - echo "Downloading Kallichore binary..." - cd ../positron-supervisor && npm rebuild --foreground-scripts - - - name: Compile Positron and Download Electron - run: npm exec -- npm-run-all --max-old-space-size=8192 -p compile "electron x64" - - - name: Install Playwright browsers - run: npm run playwright-install - - - name: Install E2E test dependencies - run: npm --prefix test/e2e ci --no-audit --no-fund - - - name: Compile E2E Tests - run: npm --prefix test/e2e run compile - - # Downloads Builtin Extensions (needed for integration & e2e testing) - - shell: bash - run: npm run prelaunch - - - name: 💾 Save caches - if: ${{ inputs.save_cache && matrix.shard == 1 }} - uses: ./.github/actions/save-build-caches + - name: Download build artifact + uses: actions/download-artifact@v8 with: - cache-npm-core-hit: ${{ steps.restore-caches.outputs.cache-npm-core-hit }} - cache-npm-extensions-volatile-hit: ${{ steps.restore-caches.outputs.cache-npm-extensions-volatile-hit }} - cache-npm-extensions-stable-hit: ${{ steps.restore-caches.outputs.cache-npm-extensions-stable-hit }} - cache-builtins-hit: ${{ steps.restore-caches.outputs.cache-builtins-hit }} - cache-playwright-hit: ${{ steps.restore-caches.outputs.cache-playwright-hit }} - extensions-volatile-hash: ${{ steps.restore-caches.outputs.extensions-volatile-hash }} - extensions-stable-hash: ${{ steps.restore-caches.outputs.extensions-stable-hash }} - package-locks-hash: ${{ steps.restore-caches.outputs.package-locks-hash }} - node-version: ${{ steps.restore-caches.outputs.node-version }} - playwright-version: ${{ steps.restore-caches.outputs.playwright-version }} + name: positron-build-windows + path: ${{ runner.temp }}/artifacts + + # Defender's real-time scanning hits every file created during extract. + # The tar step writes to dirname(github.workspace) — one level up from + # the repo checkout — so the parent must be excluded, not just the + # checkout itself. Also handle Windows GitHub runners using D:\a as the + # actions root. + - name: Disable Defender scanning for extract paths + shell: pwsh + run: | + $workspace = "${{ github.workspace }}" + $parent = Split-Path -Parent $workspace + Add-MpPreference -ExclusionPath "${{ runner.temp }}" -ErrorAction SilentlyContinue + Add-MpPreference -ExclusionPath "$workspace" -ErrorAction SilentlyContinue + Add-MpPreference -ExclusionPath "$parent" -ErrorAction SilentlyContinue + # GitHub Actions on Windows typically runs on D:\, but C:\ has been + # seen too. Cover both runner working roots. + Add-MpPreference -ExclusionPath "D:\a" -ErrorAction SilentlyContinue + Add-MpPreference -ExclusionPath "C:\a" -ErrorAction SilentlyContinue + Write-Host "Defender exclusions applied:" + Write-Host " - ${{ runner.temp }}" + Write-Host " - $workspace" + Write-Host " - $parent (extract destination)" + Write-Host " - D:\a, C:\a" + # Print current exclusions to verify + (Get-MpPreference).ExclusionPath | ForEach-Object { Write-Host " [verified] $_" } + + - name: Extract build artifact + shell: bash + run: | + WORKSPACE=$(cygpath -u "${GITHUB_WORKSPACE}") + WORKSPACE_PARENT="$(dirname "${WORKSPACE}")" + ARTIFACTS_DIR="$(cygpath -u "${RUNNER_TEMP}")/artifacts" + if [ -f "${ARTIFACTS_DIR}/positron-build.tar.zst" ]; then + command -v zstd >/dev/null 2>&1 || { echo "zstd not found but .tar.zst artifact exists"; exit 1; } + echo "Extracting zstd archive with parallel decompression..." + zstd -d -T0 --stdout "${ARTIFACTS_DIR}/positron-build.tar.zst" | \ + tar --no-same-owner --no-same-permissions -x -C "${WORKSPACE_PARENT}" + echo "Cleaning up archive to free disk space" + rm -f "${ARTIFACTS_DIR}/positron-build.tar.zst" + elif [ -f "${ARTIFACTS_DIR}/positron-build.tar.gz" ]; then + echo "Extracting gzip archive..." + tar --no-same-owner --no-same-permissions \ + -C "${WORKSPACE_PARENT}" \ + -xzf "${ARTIFACTS_DIR}/positron-build.tar.gz" + echo "Cleaning up archive to free disk space" + rm -f "${ARTIFACTS_DIR}/positron-build.tar.gz" + else + echo "No build artifact found in ${ARTIFACTS_DIR}" && exit 1 + fi - name: Download Python requirements file id: download-python-reqs diff --git a/.github/workflows/test-full-suite.yml b/.github/workflows/test-full-suite.yml index e0d6782bed1a..8cdd0fd816d2 100644 --- a/.github/workflows/test-full-suite.yml +++ b/.github/workflows/test-full-suite.yml @@ -167,15 +167,24 @@ jobs: max_failures: 6 # 20 max failures / 4 shards = 5, rounded to 6 to avoid premature failure of the entire suite enable_diagnostic_logging: ${{ inputs.enable_diagnostic_logging }} + setup-windows: + name: setup-windows + needs: [validate-commit] + if: ${{ !failure() && !cancelled() && (github.event_name == 'schedule' || inputs.run_e2e_windows) }} + uses: ./.github/workflows/test-e2e-windows-build.yml + secrets: inherit + with: + commit: ${{ inputs.commit != 'latest' && inputs.commit || '' }} + skip_cache: ${{ inputs.commit != 'latest' }} + e2e-windows: name: e2e - needs: [validate-commit] + needs: [validate-commit, setup-windows] if: ${{ !failure() && !cancelled() && (github.event_name == 'schedule' || inputs.run_e2e_windows) }} uses: ./.github/workflows/test-e2e-windows-run.yml secrets: inherit with: commit: ${{ inputs.commit != 'latest' && inputs.commit || '' }} - skip_cache: ${{ inputs.commit != 'latest' }} grep: ${{ inputs.grep || '' }} project: "e2e-windows" display_name: "electron-win" diff --git a/.github/workflows/test-merge.yml b/.github/workflows/test-merge.yml index dd9a543657e9..7f036b950abc 100644 --- a/.github/workflows/test-merge.yml +++ b/.github/workflows/test-merge.yml @@ -6,16 +6,23 @@ on: - main - "prerelease/**" - "release/**" + # TEMPORARY: trigger once on this PR to validate the new shard counts. + # Revert this block before merging. + pull_request: + branches: + - main + paths: + - .github/workflows/test-merge.yml jobs: - setup: + setup-ubuntu: name: setup uses: ./.github/workflows/test-e2e-ubuntu-build.yml secrets: inherit e2e-electron: name: e2e - needs: [setup] + needs: [setup-ubuntu] uses: ./.github/workflows/test-e2e-ubuntu-run.yml with: grep: "" @@ -24,31 +31,38 @@ jobs: install_undetectable_interpreters: false install_license: false upload_logs: false - shards: 2 - matrix: '{"shard":[1,2]}' + shards: 6 + matrix: '{"shard":[1,2,3,4,5,6]}' workers: 2 allow_soft_fail: true - max_failures: 10 # 20 max failures / 2 shards = 10 + max_failures: 4 # 20 max failures / 6 shards = 3.33, rounded up to 4 + secrets: inherit + + setup-windows: + name: setup + uses: ./.github/workflows/test-e2e-windows-build.yml secrets: inherit + with: + save_cache: true e2e-windows: name: e2e + needs: [setup-windows] uses: ./.github/workflows/test-e2e-windows-run.yml secrets: inherit with: - save_cache: true grep: "" project: "e2e-windows" display_name: "electron-win" allow_soft_fail: true upload_logs: false - shards: 3 - matrix: '{"shard":[1,2,3]}' - max_failures: 7 # 20 max failures / 3 shards = 6.66, rounded up to 7 + shards: 6 + matrix: '{"shard":[1,2,3,4,5,6]}' + max_failures: 4 # 20 max failures / 6 shards = 3.33, rounded up to 4 e2e-ubuntu-chromium: name: e2e - needs: [setup] + needs: [setup-ubuntu] uses: ./.github/workflows/test-e2e-ubuntu-run.yml with: grep: "" @@ -57,11 +71,11 @@ jobs: install_undetectable_interpreters: false install_license: true upload_logs: true - shards: 2 - matrix: '{"shard":[1,2]}' + shards: 5 + matrix: '{"shard":[1,2,3,4,5]}' workers: 2 allow_soft_fail: true - max_failures: 10 # 20 max failures / 2 shards = 10 + max_failures: 4 # 20 max failures / 5 shards = 4 secrets: inherit unit-tests: @@ -78,45 +92,47 @@ jobs: with: pull_request: false - slack-notify: - if: always() - needs: - [ - setup, - unit-tests, - ext-host-tests, - e2e-electron, - e2e-windows, - e2e-ubuntu-chromium, - ] - runs-on: ubuntu-latest - steps: - - name: Checkout - if: always() - uses: actions/checkout@v6 - with: - sparse-checkout: .github/actions/notify-e2e-insights - - - name: Notify E2E Test Insights - if: always() - uses: ./.github/actions/notify-e2e-insights - with: - webhook_secret: ${{ secrets.E2E_INSIGHTS_WEBHOOK_SECRET }} - connect_api_key: ${{ secrets.CONNECT_API_KEY }} - repo_id: "positron" - filter_jobs: "(e2e / ([^0-9]*)|unit|ext-host)$" - - slack-notify-win-extensions: - if: ${{ needs.e2e-windows-electron.outputs.extensions_failed == 'true' }} - needs: - - e2e-windows - runs-on: ubuntu-latest - steps: - - name: Notify Slack - uses: midleman/slack-workflow-status@v3.1.3 - with: - gh_repo_token: ${{ secrets.GITHUB_TOKEN }} - slack_token: ${{ secrets.SLACK_TOKEN_TEST_STATUS }} - slack_channel: "#positron-rstats-test-results" - filter_jobs: "windows" - notify_on: "always" +# TEMPORARY: slack notifications disabled while validating shard counts on this PR. +# Restore both jobs before merging. +# slack-notify: +# if: always() +# needs: +# [ +# setup, +# unit-tests, +# ext-host-tests, +# e2e-electron, +# e2e-windows, +# e2e-ubuntu-chromium, +# ] +# runs-on: ubuntu-latest +# steps: +# - name: Checkout +# if: always() +# uses: actions/checkout@v6 +# with: +# sparse-checkout: .github/actions/notify-e2e-insights +# +# - name: Notify E2E Test Insights +# if: always() +# uses: ./.github/actions/notify-e2e-insights +# with: +# webhook_secret: ${{ secrets.E2E_INSIGHTS_WEBHOOK_SECRET }} +# connect_api_key: ${{ secrets.CONNECT_API_KEY }} +# repo_id: "positron" +# filter_jobs: "(e2e / ([^0-9]*)|unit|ext-host)$" +# +# slack-notify-win-extensions: +# if: ${{ needs.e2e-windows.outputs.extensions_failed == 'true' }} +# needs: +# - e2e-windows +# runs-on: ubuntu-latest +# steps: +# - name: Notify Slack +# uses: midleman/slack-workflow-status@v3.1.3 +# with: +# gh_repo_token: ${{ secrets.GITHUB_TOKEN }} +# slack_token: ${{ secrets.SLACK_TOKEN_TEST_STATUS }} +# slack_channel: "#positron-rstats-test-results" +# filter_jobs: "windows" +# notify_on: "always" diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 64da42903fff..0f9d9229de2a 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -57,13 +57,21 @@ jobs: allow_soft_fail: false secrets: inherit - e2e-windows-electron: + setup-windows-electron: needs: pr-tags if: ${{ needs.pr-tags.outputs.win_tag_found == 'true' }} + name: setup-windows + uses: ./.github/workflows/test-e2e-windows-build.yml + with: + save_cache: true + secrets: inherit + + e2e-windows-electron: + needs: [pr-tags, setup-windows-electron] + if: ${{ needs.pr-tags.outputs.win_tag_found == 'true' }} name: e2e uses: ./.github/workflows/test-e2e-windows-run.yml with: - save_cache: true grep: ${{ needs.pr-tags.outputs.tags }} display_name: "electron-win" upload_logs: false diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index e3f521c31165..8692acce66ec 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -184,14 +184,6 @@ function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string, // --- Start PWB: from Positron --- // Replace vsce.listFiles with listExtensionFiles to queue the work listExtensionFiles({ cwd: extensionPath, packageManager: packageManager, packagedDependencies }).then(fileNames => { - const files = fileNames - .map(fileName => path.join(extensionPath, fileName)) - .map(filePath => new File({ - path: filePath, - stat: fs.statSync(filePath), - base: extensionPath, - contents: fs.createReadStream(filePath) - })); // check for a webpack configuration files, then invoke webpack // and merge its output with the files stream. @@ -263,12 +255,9 @@ function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string, }); }); - es.merge(...webpackStreams, es.readArray(files)) - // .pipe(es.through(function (data) { - // // debug - // console.log('out', data.path, data.contents.length); - // this.emit('data', data); - // })) + // Use createSequentialFileStream to open one file at a time, preventing EMFILE errors + // when extensions with many files (e.g. positron-python's python_files) are packaged. + es.merge(...webpackStreams, createSequentialFileStream(extensionPath, fileNames)) .pipe(result); }).catch(err => { diff --git a/test/e2e/fixtures/test-setup/options.fixtures.ts b/test/e2e/fixtures/test-setup/options.fixtures.ts index b27f69fc13e3..0ede6a4eba4e 100644 --- a/test/e2e/fixtures/test-setup/options.fixtures.ts +++ b/test/e2e/fixtures/test-setup/options.fixtures.ts @@ -76,7 +76,8 @@ export function OptionsFixture() { } : {}), // --- End Positron --- useExternalServer: project.useExternalServer, - externalServerUrl: project.externalServerUrl + externalServerUrl: project.externalServerUrl, + workerIndex: workerInfo.parallelIndex, }; options.userDataDir = getRandomUserDataDir(options); diff --git a/test/e2e/infra/code.ts b/test/e2e/infra/code.ts index b4c88d9e91b8..c7fca4fab341 100644 --- a/test/e2e/infra/code.ts +++ b/test/e2e/infra/code.ts @@ -56,6 +56,8 @@ export interface LaunchOptions { readonly externalServerUrl?: string; /** Video recording configuration for demo scripts */ readonly recordVideo?: { dir: string; size?: { width: number; height: number } }; + /** Playwright parallelIndex for per-worker resource isolation (e.g. Xvfb display on Linux) */ + readonly workerIndex?: number; // --- End Positron --- } diff --git a/test/e2e/infra/electron.ts b/test/e2e/infra/electron.ts index 575a2e9cd278..52424ab7915b 100644 --- a/test/e2e/infra/electron.ts +++ b/test/e2e/infra/electron.ts @@ -24,6 +24,15 @@ export async function resolveElectronConfiguration(options: LaunchOptions): Prom const { codePath, workspacePath, extensionsPath, userDataDir, remote, logger, logsPath, crashesPath, extraArgs } = options; const env = { ...process.env }; + // --- Start Positron --- + // Per-worker virtual display isolation on Linux. Each Playwright worker + // gets its own Xvfb display (:10 + parallelIndex) so workers don't race + // on a shared X server. Requires CI to start N Xvfb displays beginning at :10. + if (typeof options.workerIndex === 'number' && process.platform === 'linux') { + env.DISPLAY = `:${10 + options.workerIndex}`; + } + // --- End Positron --- + const args: string[] = [ '--skip-release-notes', '--skip-welcome',