From 63db9581c81ce5d2645ac404beb0fb1d10d8e49b Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Mon, 18 May 2026 18:08:11 -0500 Subject: [PATCH 01/25] ci: cache test/e2e node_modules on Windows e2e shards --- .github/workflows/test-e2e-windows-run.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/test-e2e-windows-run.yml b/.github/workflows/test-e2e-windows-run.yml index 36b1625b29a2..91c2acc54d2b 100644 --- a/.github/workflows/test-e2e-windows-run.yml +++ b/.github/workflows/test-e2e-windows-run.yml @@ -225,9 +225,25 @@ jobs: - 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: ${{ matrix.shard == 1 && 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 From 4ad0011f244d6df37eaeb690c47141eacbc4b03b Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Tue, 19 May 2026 06:26:39 -0500 Subject: [PATCH 02/25] fix(build): patch fs with graceful-fs in Win32 gulp tasks to prevent EMFILE errors Retries EMFILE (too many open files) errors during gulp vscode-win32-x64 packaging. Same fix VS Code uses in their build tooling. Co-Authored-By: Claude Sonnet 4.6 --- build/gulpfile.vscode.win32.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build/gulpfile.vscode.win32.ts b/build/gulpfile.vscode.win32.ts index fa0cb4a184a3..72da623c1997 100644 --- a/build/gulpfile.vscode.win32.ts +++ b/build/gulpfile.vscode.win32.ts @@ -21,6 +21,11 @@ import * as util from './lib/util.ts'; import { createRequire } from 'module'; const require = createRequire(import.meta.url); +// --- Start Positron --- +// Patch Node's fs with graceful-fs to retry EMFILE (too many open files) errors +// during the Windows packaging step. Same fix VS Code uses in their build tooling. +require('graceful-fs').gracefulify(require('fs')); +// --- End Positron --- const repoPath = path.dirname(import.meta.dirname); const commit = getVersion(repoPath); From cfa1b0de00c062d6a272c8606eba6d238ff4ebdb Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Tue, 19 May 2026 06:40:28 -0500 Subject: [PATCH 03/25] spike: add Windows packaged-build workflow to test graceful-fs EMFILE fix --- .../spike-windows-packaged-build.yml | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .github/workflows/spike-windows-packaged-build.yml diff --git a/.github/workflows/spike-windows-packaged-build.yml b/.github/workflows/spike-windows-packaged-build.yml new file mode 100644 index 000000000000..52efd37f1779 --- /dev/null +++ b/.github/workflows/spike-windows-packaged-build.yml @@ -0,0 +1,73 @@ +name: "Spike: Windows Packaged Build" +# One-shot workflow to verify graceful-fs fix resolves EMFILE errors in gulp vscode-win32-x64. +# Delete this file before merging. + +on: + workflow_dispatch: + +jobs: + build: + name: gulp vscode-win32-x64 + runs-on: windows-latest-8x + timeout-minutes: 90 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + POSITRON_BUILD_NUMBER: 0 + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + submodules: recursive + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + + - name: 📦 Restore caches + 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' + NPM_CONFIG_AUDIT: 'false' + NPM_CONFIG_FUND: 'false' + NPM_CONFIG_UPDATE_NOTIFIER: 'false' + 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) + 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: 📦 gulp vscode-win32-x64 + shell: bash + run: npm run gulp vscode-win32-x64 From b50ee3056b31397b2891019ffd67e5c5b34ff8fa Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Tue, 19 May 2026 09:34:52 -0500 Subject: [PATCH 04/25] spike: trigger packaged-build workflow on PR to test graceful-fs fix --- .github/workflows/spike-windows-packaged-build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/spike-windows-packaged-build.yml b/.github/workflows/spike-windows-packaged-build.yml index 52efd37f1779..c288c6057293 100644 --- a/.github/workflows/spike-windows-packaged-build.yml +++ b/.github/workflows/spike-windows-packaged-build.yml @@ -4,6 +4,10 @@ name: "Spike: Windows Packaged Build" on: workflow_dispatch: + pull_request: + branches: [main] + paths: + - 'build/gulpfile.vscode.win32.ts' jobs: build: From 9bbe6b601661c0efab3ef2efc940e6941f14b61e Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Tue, 19 May 2026 10:38:35 -0500 Subject: [PATCH 05/25] fix: prevent EMFILE in fromLocalWebpack by opening files sequentially fromLocalWebpack was constructing File objects for all extension files at once and passing them via es.readArray(), which opened thousands of ReadStreams simultaneously. On Windows, this hit the OS EMFILE (too many open files) limit when packaging positron-python's 11,784 python_files. Switch to createSequentialFileStream (already used by fromLocalNormal) which opens files one at a time via a pump queue, matching the pattern that fromLocalNormal already relied on. Co-Authored-By: Claude Sonnet 4.6 --- build/lib/extensions.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) 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 => { From b17ba8d245a57377dda6ea420708099b0309f1fc Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Tue, 19 May 2026 10:38:58 -0500 Subject: [PATCH 06/25] ci: add build/lib/extensions.ts to spike workflow paths filter Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/spike-windows-packaged-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/spike-windows-packaged-build.yml b/.github/workflows/spike-windows-packaged-build.yml index c288c6057293..d7494de14f38 100644 --- a/.github/workflows/spike-windows-packaged-build.yml +++ b/.github/workflows/spike-windows-packaged-build.yml @@ -8,6 +8,7 @@ on: branches: [main] paths: - 'build/gulpfile.vscode.win32.ts' + - 'build/lib/extensions.ts' jobs: build: From 60cee6ec9470a81c635dbdc7423dbeeca027558d Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Tue, 19 May 2026 11:16:55 -0500 Subject: [PATCH 07/25] ci: remove spike workflow and graceful-fs patch The EMFILE fix in build/lib/extensions.ts (sequential file opening via createSequentialFileStream) validated successfully on Windows CI. The spike workflow and the original graceful-fs patch are no longer needed. Co-Authored-By: Claude Sonnet 4.6 --- .../spike-windows-packaged-build.yml | 78 ------------------- build/gulpfile.vscode.win32.ts | 5 -- 2 files changed, 83 deletions(-) delete mode 100644 .github/workflows/spike-windows-packaged-build.yml diff --git a/.github/workflows/spike-windows-packaged-build.yml b/.github/workflows/spike-windows-packaged-build.yml deleted file mode 100644 index d7494de14f38..000000000000 --- a/.github/workflows/spike-windows-packaged-build.yml +++ /dev/null @@ -1,78 +0,0 @@ -name: "Spike: Windows Packaged Build" -# One-shot workflow to verify graceful-fs fix resolves EMFILE errors in gulp vscode-win32-x64. -# Delete this file before merging. - -on: - workflow_dispatch: - pull_request: - branches: [main] - paths: - - 'build/gulpfile.vscode.win32.ts' - - 'build/lib/extensions.ts' - -jobs: - build: - name: gulp vscode-win32-x64 - runs-on: windows-latest-8x - timeout-minutes: 90 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - POSITRON_BUILD_NUMBER: 0 - - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - submodules: recursive - - - name: Setup Node - uses: actions/setup-node@v6 - with: - node-version-file: .nvmrc - - - name: 📦 Restore caches - 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' - NPM_CONFIG_AUDIT: 'false' - NPM_CONFIG_FUND: 'false' - NPM_CONFIG_UPDATE_NOTIFIER: 'false' - 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) - 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: 📦 gulp vscode-win32-x64 - shell: bash - run: npm run gulp vscode-win32-x64 diff --git a/build/gulpfile.vscode.win32.ts b/build/gulpfile.vscode.win32.ts index 72da623c1997..fa0cb4a184a3 100644 --- a/build/gulpfile.vscode.win32.ts +++ b/build/gulpfile.vscode.win32.ts @@ -21,11 +21,6 @@ import * as util from './lib/util.ts'; import { createRequire } from 'module'; const require = createRequire(import.meta.url); -// --- Start Positron --- -// Patch Node's fs with graceful-fs to retry EMFILE (too many open files) errors -// during the Windows packaging step. Same fix VS Code uses in their build tooling. -require('graceful-fs').gracefulify(require('fs')); -// --- End Positron --- const repoPath = path.dirname(import.meta.dirname); const commit = getVersion(repoPath); From 5f0c28ba0ccfde80ddb47a03fdc3b11e8739f40f Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Tue, 19 May 2026 11:30:36 -0500 Subject: [PATCH 08/25] ci: implement Windows build-once for e2e shards Add test-e2e-windows-build.yml that compiles Positron once and uploads the full workspace (node_modules + compiled output) as a tar artifact. Strip the build steps from test-e2e-windows-run.yml so each shard just downloads and extracts that artifact instead of compiling independently. Update test-merge.yml (3 shards), test-full-suite.yml (4 shards), and test-pull-request.yml to call the build workflow first, then pass the artifact to the run shards. Also fix a pre-existing bug: test-merge.yml's slack-notify-win-extensions was checking needs.e2e-windows-electron.outputs.extensions_failed but the job is named e2e-windows. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/test-e2e-windows-build.yml | 167 +++++++++++++++++++ .github/workflows/test-e2e-windows-run.yml | 141 +++------------- .github/workflows/test-full-suite.yml | 13 +- .github/workflows/test-merge.yml | 11 +- .github/workflows/test-pull-request.yml | 12 +- 5 files changed, 217 insertions(+), 127 deletions(-) create mode 100644 .github/workflows/test-e2e-windows-build.yml diff --git a/.github/workflows/test-e2e-windows-build.yml b/.github/workflows/test-e2e-windows-build.yml new file mode 100644 index 000000000000..e71cb3bab85c --- /dev/null +++ b/.github/workflows/test-e2e-windows-build.yml @@ -0,0 +1,167 @@ +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 + + - 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. + 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")" + + - 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 with gzip diff --git a/.github/workflows/test-e2e-windows-run.yml b/.github/workflows/test-e2e-windows-run.yml index 91c2acc54d2b..b78b751043a3 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,119 +142,28 @@ 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: 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: ${{ matrix.shard == 1 && steps.cache-e2e.outputs.cache-hit != 'true' }} - continue-on-error: true - uses: actions/cache/save@v5 + - name: Download build artifact + uses: actions/download-artifact@v8 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 + name: positron-build-windows + path: ${{ runner.temp }}/artifacts - # 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 - 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: 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.gz" ]; then + echo "Extracting build artifact..." + 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 at ${ARTIFACTS_DIR}/positron-build.tar.gz" && 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..9d8d7b2ec9a6 100644 --- a/.github/workflows/test-merge.yml +++ b/.github/workflows/test-merge.yml @@ -31,12 +31,19 @@ jobs: max_failures: 10 # 20 max failures / 2 shards = 10 secrets: inherit + setup-windows: + name: setup-windows + 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" @@ -107,7 +114,7 @@ jobs: filter_jobs: "(e2e / ([^0-9]*)|unit|ext-host)$" slack-notify-win-extensions: - if: ${{ needs.e2e-windows-electron.outputs.extensions_failed == 'true' }} + if: ${{ needs.e2e-windows.outputs.extensions_failed == 'true' }} needs: - e2e-windows runs-on: ubuntu-latest 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 From 5f5425d9a3322a886eeb20a1d2b3d8e2cea2eeb0 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Tue, 19 May 2026 12:42:41 -0500 Subject: [PATCH 09/25] spike: test zstd availability and performance on Windows runner --- .github/workflows/spike-windows-zstd.yml | 106 +++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 .github/workflows/spike-windows-zstd.yml diff --git a/.github/workflows/spike-windows-zstd.yml b/.github/workflows/spike-windows-zstd.yml new file mode 100644 index 000000000000..55c716c489f5 --- /dev/null +++ b/.github/workflows/spike-windows-zstd.yml @@ -0,0 +1,106 @@ +name: "Spike: Windows zstd availability" + +on: + workflow_dispatch: + +jobs: + test-zstd: + name: test-zstd + runs-on: windows-latest-8x + steps: + - name: Check zstd availability + shell: bash + run: | + echo "=== zstd availability ===" + if command -v zstd >/dev/null 2>&1; then + echo "✓ zstd found: $(command -v zstd)" + zstd --version + else + echo "✗ zstd not found in PATH" + echo "PATH: $PATH" + fi + + - name: Test zstd compress + decompress roundtrip + shell: bash + run: | + WORK_DIR="$(cygpath -u "${RUNNER_TEMP}")/zstd-test" + mkdir -p "${WORK_DIR}/source/subdir" + + # Create some test files of varying sizes + echo "hello world" > "${WORK_DIR}/source/file1.txt" + dd if=/dev/urandom bs=1M count=10 2>/dev/null | base64 > "${WORK_DIR}/source/subdir/bigfile.txt" + for i in $(seq 1 100); do echo "test file $i" > "${WORK_DIR}/source/subdir/file${i}.txt"; done + + echo "Source size: $(du -sh "${WORK_DIR}/source")" + + # Compress with zstd + echo "" + echo "=== Compressing with zstd -T0 -1 ===" + START=$(date +%s%N) + tar -C "${WORK_DIR}" \ + --use-compress-program='zstd -T0 -1' \ + -cf "${WORK_DIR}/test.tar.zst" \ + source + END=$(date +%s%N) + echo "Compress time: $(( (END - START) / 1000000 ))ms" + echo "Compressed size: $(du -sh "${WORK_DIR}/test.tar.zst")" + + # Decompress with zstd -d -T0 --stdout | tar -x (same as run workflow) + echo "" + echo "=== Decompressing with zstd -d -T0 --stdout | tar -x ===" + mkdir -p "${WORK_DIR}/dest" + START=$(date +%s%N) + zstd -d -T0 --stdout "${WORK_DIR}/test.tar.zst" | \ + tar --no-same-owner --no-same-permissions -x -C "${WORK_DIR}/dest" + END=$(date +%s%N) + echo "Decompress time: $(( (END - START) / 1000000 ))ms" + + # Verify roundtrip + echo "" + echo "=== Verifying roundtrip ===" + if diff -r "${WORK_DIR}/source" "${WORK_DIR}/dest/source" >/dev/null 2>&1; then + echo "✓ Roundtrip verified — source and dest match" + else + echo "✗ Roundtrip FAILED — source and dest differ" + diff -r "${WORK_DIR}/source" "${WORK_DIR}/dest/source" | head -20 + exit 1 + fi + + - name: Compare gzip vs zstd on a larger payload + shell: bash + run: | + WORK_DIR="$(cygpath -u "${RUNNER_TEMP}")/zstd-compare" + mkdir -p "${WORK_DIR}/source" + + # ~50MB of mixed content (simulate a small workspace) + dd if=/dev/urandom bs=1M count=20 2>/dev/null | base64 > "${WORK_DIR}/source/random.txt" + for i in $(seq 1 500); do echo "node_modules style file $i with some content padding" > "${WORK_DIR}/source/module${i}.js"; done + + echo "Source size: $(du -sh "${WORK_DIR}/source")" + echo "" + + # gzip + START=$(date +%s%N) + tar -C "${WORK_DIR}" -czf "${WORK_DIR}/test.tar.gz" source + GZ_COMPRESS=$(( ($(date +%s%N) - START) / 1000000 )) + GZ_SIZE=$(du -sh "${WORK_DIR}/test.tar.gz" | cut -f1) + + START=$(date +%s%N) + tar --no-same-owner -xzf "${WORK_DIR}/test.tar.gz" -C "${WORK_DIR}/dest-gz" + GZ_DECOMPRESS=$(( ($(date +%s%N) - START) / 1000000 )) + + # zstd + mkdir -p "${WORK_DIR}/dest-zst" + START=$(date +%s%N) + tar -C "${WORK_DIR}" --use-compress-program='zstd -T0 -1' -cf "${WORK_DIR}/test.tar.zst" source + ZST_COMPRESS=$(( ($(date +%s%N) - START) / 1000000 )) + ZST_SIZE=$(du -sh "${WORK_DIR}/test.tar.zst" | cut -f1) + + START=$(date +%s%N) + zstd -d -T0 --stdout "${WORK_DIR}/test.tar.zst" | tar --no-same-owner -x -C "${WORK_DIR}/dest-zst" + ZST_DECOMPRESS=$(( ($(date +%s%N) - START) / 1000000 )) + + echo " | size | compress | decompress" + echo "----------|--------|----------|----------" + echo "gzip | ${GZ_SIZE} | ${GZ_COMPRESS}ms | ${GZ_DECOMPRESS}ms" + echo "zstd -T0 | ${ZST_SIZE} | ${ZST_COMPRESS}ms | ${ZST_DECOMPRESS}ms" From bb511a870717212ef6fbba7cbca806629e1144d1 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Tue, 19 May 2026 12:45:40 -0500 Subject: [PATCH 10/25] spike: trigger zstd test on pull_request --- .github/workflows/spike-windows-zstd.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/spike-windows-zstd.yml b/.github/workflows/spike-windows-zstd.yml index 55c716c489f5..554d471c8873 100644 --- a/.github/workflows/spike-windows-zstd.yml +++ b/.github/workflows/spike-windows-zstd.yml @@ -1,7 +1,9 @@ name: "Spike: Windows zstd availability" on: - workflow_dispatch: + pull_request: + branches: + - main jobs: test-zstd: From 4c7ba520da2d696634bc231728d2127bb17fa3f1 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Tue, 19 May 2026 12:50:31 -0500 Subject: [PATCH 11/25] spike: fix missing mkdir for gzip dest in comparison step --- .github/workflows/spike-windows-zstd.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/spike-windows-zstd.yml b/.github/workflows/spike-windows-zstd.yml index 554d471c8873..8ac33d870aa6 100644 --- a/.github/workflows/spike-windows-zstd.yml +++ b/.github/workflows/spike-windows-zstd.yml @@ -82,6 +82,7 @@ jobs: echo "" # gzip + mkdir -p "${WORK_DIR}/dest-gz" START=$(date +%s%N) tar -C "${WORK_DIR}" -czf "${WORK_DIR}/test.tar.gz" source GZ_COMPRESS=$(( ($(date +%s%N) - START) / 1000000 )) From 8c1c0313a039768895193c13a3b9ed8a836d81eb Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Tue, 19 May 2026 12:53:49 -0500 Subject: [PATCH 12/25] ci: use zstd compression for Windows build artifact --- .github/workflows/test-e2e-windows-build.yml | 27 +++++++++++++++----- .github/workflows/test-e2e-windows-run.yml | 12 ++++++--- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test-e2e-windows-build.yml b/.github/workflows/test-e2e-windows-build.yml index e71cb3bab85c..1a1e8e326643 100644 --- a/.github/workflows/test-e2e-windows-build.yml +++ b/.github/workflows/test-e2e-windows-build.yml @@ -151,12 +151,25 @@ jobs: 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. - 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")" + # 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 @@ -164,4 +177,4 @@ jobs: name: positron-build-windows path: ${{ runner.temp }}/artifacts retention-days: 1 - compression-level: 0 # already compressed with gzip + compression-level: 0 # already compressed diff --git a/.github/workflows/test-e2e-windows-run.yml b/.github/workflows/test-e2e-windows-run.yml index b78b751043a3..c9128d211d89 100644 --- a/.github/workflows/test-e2e-windows-run.yml +++ b/.github/workflows/test-e2e-windows-run.yml @@ -154,15 +154,21 @@ jobs: WORKSPACE=$(cygpath -u "${GITHUB_WORKSPACE}") WORKSPACE_PARENT="$(dirname "${WORKSPACE}")" ARTIFACTS_DIR="$(cygpath -u "${RUNNER_TEMP}")/artifacts" - if [ -f "${ARTIFACTS_DIR}/positron-build.tar.gz" ]; then - echo "Extracting build artifact..." + if [ -f "${ARTIFACTS_DIR}/positron-build.tar.zst" ]; then + 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 at ${ARTIFACTS_DIR}/positron-build.tar.gz" && exit 1 + echo "No build artifact found in ${ARTIFACTS_DIR}" && exit 1 fi - name: Download Python requirements file From dca2c71a797496be0c9aece330f516971b5c5729 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Tue, 19 May 2026 20:57:34 -0500 Subject: [PATCH 13/25] ci: bump Linux PR workers to 3 with per-worker Xvfb isolation Each Playwright worker now gets its own Xvfb display (:10 + parallelIndex) so workers don't race on a shared X server. The setup-xvfb action accepts a workers input and starts that many displays at :10..:(10+N-1). Linux PR e2e-electron workers bumped from 2 to 3, expected to drop the job from ~31m to ~25m on cold cache / ~19m on warm cache. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/actions/setup-test-env/action.yml | 6 +++ .github/actions/setup-xvfb/action.yml | 38 ++++++++++++++----- .github/workflows/test-e2e-ubuntu-run.yml | 1 + .github/workflows/test-e2e-ubuntu.yml | 1 + .github/workflows/test-pull-request.yml | 2 +- .../fixtures/test-setup/options.fixtures.ts | 3 +- test/e2e/infra/code.ts | 2 + test/e2e/infra/electron.ts | 9 +++++ 8 files changed, 51 insertions(+), 11 deletions(-) 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..f8458cddeb21 100644 --- a/.github/actions/setup-xvfb/action.yml +++ b/.github/actions/setup-xvfb/action.yml @@ -1,27 +1,47 @@ 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 & + # 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-pull-request.yml b/.github/workflows/test-pull-request.yml index 0f9d9229de2a..2a1637ae02d0 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -53,7 +53,7 @@ jobs: install_undetectable_interpreters: false install_license: false upload_logs: false - workers: 2 + workers: 3 allow_soft_fail: false secrets: inherit 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', From e7164c6a91d77720a7c927ca186d5d05992a5622 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 20 May 2026 06:29:51 -0500 Subject: [PATCH 14/25] ci: bump Linux PR workers from 3 to 4 3 workers saved ~2m off Linux PR e2e. Trying 4 to see if there's another 1-2m available before scaling hits a ceiling. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/test-pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 2a1637ae02d0..2096852ec9b0 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -53,7 +53,7 @@ jobs: install_undetectable_interpreters: false install_license: false upload_logs: false - workers: 3 + workers: 4 allow_soft_fail: false secrets: inherit From 237fc460e4e5ca82a8e1c6fa3b22b402456831d5 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 20 May 2026 17:05:19 -0500 Subject: [PATCH 15/25] ci: remove zstd validation spike workflow The spike confirmed zstd is available on windows-latest-8x runners (191ms compress vs gzip's 1228ms, 320ms decompress vs 416ms). The production change landed in test-e2e-windows-build.yml; this validator is no longer needed. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/spike-windows-zstd.yml | 109 ----------------------- 1 file changed, 109 deletions(-) delete mode 100644 .github/workflows/spike-windows-zstd.yml diff --git a/.github/workflows/spike-windows-zstd.yml b/.github/workflows/spike-windows-zstd.yml deleted file mode 100644 index 8ac33d870aa6..000000000000 --- a/.github/workflows/spike-windows-zstd.yml +++ /dev/null @@ -1,109 +0,0 @@ -name: "Spike: Windows zstd availability" - -on: - pull_request: - branches: - - main - -jobs: - test-zstd: - name: test-zstd - runs-on: windows-latest-8x - steps: - - name: Check zstd availability - shell: bash - run: | - echo "=== zstd availability ===" - if command -v zstd >/dev/null 2>&1; then - echo "✓ zstd found: $(command -v zstd)" - zstd --version - else - echo "✗ zstd not found in PATH" - echo "PATH: $PATH" - fi - - - name: Test zstd compress + decompress roundtrip - shell: bash - run: | - WORK_DIR="$(cygpath -u "${RUNNER_TEMP}")/zstd-test" - mkdir -p "${WORK_DIR}/source/subdir" - - # Create some test files of varying sizes - echo "hello world" > "${WORK_DIR}/source/file1.txt" - dd if=/dev/urandom bs=1M count=10 2>/dev/null | base64 > "${WORK_DIR}/source/subdir/bigfile.txt" - for i in $(seq 1 100); do echo "test file $i" > "${WORK_DIR}/source/subdir/file${i}.txt"; done - - echo "Source size: $(du -sh "${WORK_DIR}/source")" - - # Compress with zstd - echo "" - echo "=== Compressing with zstd -T0 -1 ===" - START=$(date +%s%N) - tar -C "${WORK_DIR}" \ - --use-compress-program='zstd -T0 -1' \ - -cf "${WORK_DIR}/test.tar.zst" \ - source - END=$(date +%s%N) - echo "Compress time: $(( (END - START) / 1000000 ))ms" - echo "Compressed size: $(du -sh "${WORK_DIR}/test.tar.zst")" - - # Decompress with zstd -d -T0 --stdout | tar -x (same as run workflow) - echo "" - echo "=== Decompressing with zstd -d -T0 --stdout | tar -x ===" - mkdir -p "${WORK_DIR}/dest" - START=$(date +%s%N) - zstd -d -T0 --stdout "${WORK_DIR}/test.tar.zst" | \ - tar --no-same-owner --no-same-permissions -x -C "${WORK_DIR}/dest" - END=$(date +%s%N) - echo "Decompress time: $(( (END - START) / 1000000 ))ms" - - # Verify roundtrip - echo "" - echo "=== Verifying roundtrip ===" - if diff -r "${WORK_DIR}/source" "${WORK_DIR}/dest/source" >/dev/null 2>&1; then - echo "✓ Roundtrip verified — source and dest match" - else - echo "✗ Roundtrip FAILED — source and dest differ" - diff -r "${WORK_DIR}/source" "${WORK_DIR}/dest/source" | head -20 - exit 1 - fi - - - name: Compare gzip vs zstd on a larger payload - shell: bash - run: | - WORK_DIR="$(cygpath -u "${RUNNER_TEMP}")/zstd-compare" - mkdir -p "${WORK_DIR}/source" - - # ~50MB of mixed content (simulate a small workspace) - dd if=/dev/urandom bs=1M count=20 2>/dev/null | base64 > "${WORK_DIR}/source/random.txt" - for i in $(seq 1 500); do echo "node_modules style file $i with some content padding" > "${WORK_DIR}/source/module${i}.js"; done - - echo "Source size: $(du -sh "${WORK_DIR}/source")" - echo "" - - # gzip - mkdir -p "${WORK_DIR}/dest-gz" - START=$(date +%s%N) - tar -C "${WORK_DIR}" -czf "${WORK_DIR}/test.tar.gz" source - GZ_COMPRESS=$(( ($(date +%s%N) - START) / 1000000 )) - GZ_SIZE=$(du -sh "${WORK_DIR}/test.tar.gz" | cut -f1) - - START=$(date +%s%N) - tar --no-same-owner -xzf "${WORK_DIR}/test.tar.gz" -C "${WORK_DIR}/dest-gz" - GZ_DECOMPRESS=$(( ($(date +%s%N) - START) / 1000000 )) - - # zstd - mkdir -p "${WORK_DIR}/dest-zst" - START=$(date +%s%N) - tar -C "${WORK_DIR}" --use-compress-program='zstd -T0 -1' -cf "${WORK_DIR}/test.tar.zst" source - ZST_COMPRESS=$(( ($(date +%s%N) - START) / 1000000 )) - ZST_SIZE=$(du -sh "${WORK_DIR}/test.tar.zst" | cut -f1) - - START=$(date +%s%N) - zstd -d -T0 --stdout "${WORK_DIR}/test.tar.zst" | tar --no-same-owner -x -C "${WORK_DIR}/dest-zst" - ZST_DECOMPRESS=$(( ($(date +%s%N) - START) / 1000000 )) - - echo " | size | compress | decompress" - echo "----------|--------|----------|----------" - echo "gzip | ${GZ_SIZE} | ${GZ_COMPRESS}ms | ${GZ_DECOMPRESS}ms" - echo "zstd -T0 | ${ZST_SIZE} | ${ZST_COMPRESS}ms | ${ZST_DECOMPRESS}ms" From e4ec09ef67598752f3315680125383e2c136a991 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 20 May 2026 17:40:41 -0500 Subject: [PATCH 16/25] ci: validate xvfb WORKERS input and guard zstd extraction Fail fast if WORKERS is not a positive integer so an invalid value can't silently start zero Xvfb displays. Also verify zstd is on PATH before extracting the .tar.zst Windows build artifact so a missing tool fails loudly instead of with a cryptic decompression error. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/actions/setup-xvfb/action.yml | 4 ++++ .github/workflows/test-e2e-windows-run.yml | 1 + 2 files changed, 5 insertions(+) diff --git a/.github/actions/setup-xvfb/action.yml b/.github/actions/setup-xvfb/action.yml index f8458cddeb21..568aae15e4e6 100644 --- a/.github/actions/setup-xvfb/action.yml +++ b/.github/actions/setup-xvfb/action.yml @@ -15,6 +15,10 @@ runs: env: WORKERS: ${{ inputs.workers }} run: | + # 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). diff --git a/.github/workflows/test-e2e-windows-run.yml b/.github/workflows/test-e2e-windows-run.yml index c9128d211d89..ce36b6ddc79d 100644 --- a/.github/workflows/test-e2e-windows-run.yml +++ b/.github/workflows/test-e2e-windows-run.yml @@ -155,6 +155,7 @@ jobs: 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}" From 64c5187e7af3404fbdc4dfdecd469eaeb4eed5ea Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 27 May 2026 09:30:16 -0500 Subject: [PATCH 17/25] ci: revert Linux PR workers to 2; keep per-worker Xvfb infrastructure 3 and 4 workers both surface monaco-workbench readiness timeouts on ubuntu-latest-8x (4 Electrons cold-starting at once exceeds the 30s checkPositronReady window). The savings (~2m vs 2 workers) is too small to justify the added flake risk. Keeping the per-worker Xvfb infrastructure so future runners with more cores can dial workers up without code changes. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/test-pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 2096852ec9b0..0f9d9229de2a 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -53,7 +53,7 @@ jobs: install_undetectable_interpreters: false install_license: false upload_logs: false - workers: 4 + workers: 2 allow_soft_fail: false secrets: inherit From 54bd5ad210f81e4e4e54dfcae00cd89963fd7683 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 27 May 2026 09:31:23 -0500 Subject: [PATCH 18/25] ci: bump test-merge shards for Linux electron (2 to 3) and Windows (3 to 4) Linux electron merge now runs 3 shards (was 2). Windows merge now runs 4 shards (was 3), benefiting from the build-once pattern shipped in this branch where setup-windows produces one artifact that all shards consume. Adjust max_failures to keep the per-shard ratio comparable (20 total / N shards rounded up). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/test-merge.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-merge.yml b/.github/workflows/test-merge.yml index 9d8d7b2ec9a6..eda4da386d24 100644 --- a/.github/workflows/test-merge.yml +++ b/.github/workflows/test-merge.yml @@ -24,11 +24,11 @@ jobs: install_undetectable_interpreters: false install_license: false upload_logs: false - shards: 2 - matrix: '{"shard":[1,2]}' + shards: 3 + matrix: '{"shard":[1,2,3]}' workers: 2 allow_soft_fail: true - max_failures: 10 # 20 max failures / 2 shards = 10 + max_failures: 7 # 20 max failures / 3 shards = 6.66, rounded up to 7 secrets: inherit setup-windows: @@ -49,9 +49,9 @@ jobs: 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: 4 + matrix: '{"shard":[1,2,3,4]}' + max_failures: 5 # 20 max failures / 4 shards = 5 e2e-ubuntu-chromium: name: e2e From 14696a5815555919e0615e935016564c4bc61726 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 27 May 2026 11:57:44 -0500 Subject: [PATCH 19/25] ci: temporarily trigger test-merge on this PR to validate shard counts REVERT THIS COMMIT BEFORE MERGING. - Add pull_request trigger to test-merge.yml, gated on changes to that file - Comment out slack-notify and slack-notify-win-extensions so the PR run doesn't spam #positron-rstats-test-results Goal: prove the new shard counts (Linux 3, Windows 4) actually run as expected before main starts using them. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/test-merge.yml | 93 +++++++++++++++++--------------- 1 file changed, 51 insertions(+), 42 deletions(-) diff --git a/.github/workflows/test-merge.yml b/.github/workflows/test-merge.yml index eda4da386d24..13428af77e33 100644 --- a/.github/workflows/test-merge.yml +++ b/.github/workflows/test-merge.yml @@ -6,6 +6,13 @@ 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: @@ -85,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.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" From c02396e98b3eb6015da439d13a9a55446e15a63d Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 27 May 2026 12:44:06 -0500 Subject: [PATCH 20/25] ci: speed up Windows e2e shards via Defender exclusion + drop Quarto setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two Windows shard wins, both targeting the 8-15min of setup overhead before tests even start: 1. Add Defender exclusions for the workspace and temp paths before extracting the build artifact. Defender's real-time scanning of the ~50k small files in node_modules trees is the dominant cost of the extract step (currently ~8 min). Excluding these paths typically cuts extract time 3-5x on Windows GitHub-hosted runners. 2. Drop the 'Set up Quarto' action. Positron bundles its own Quarto 1.9.38 into the build artifact (build/lib/quarto.ts) and uses it at runtime. The CI-installed Quarto was never used by Positron or exercised by e2e tests, which interact through the UI rather than shelling out to the quarto CLI. Saves ~2 min per shard. Both changes are reversible — Defender exclusions only apply to the ephemeral runner, and Quarto can be re-added if tests turn out to need a system Quarto we haven't found yet. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/test-e2e-windows-run.yml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test-e2e-windows-run.yml b/.github/workflows/test-e2e-windows-run.yml index ce36b6ddc79d..afd8966d365e 100644 --- a/.github/workflows/test-e2e-windows-run.yml +++ b/.github/workflows/test-e2e-windows-run.yml @@ -148,6 +148,18 @@ jobs: name: positron-build-windows path: ${{ runner.temp }}/artifacts + # Defender's real-time scanning hits every file created during extract, + # which is the dominant cost for node_modules-heavy archives (~50k small + # files). Excluding the workspace and temp paths typically cuts extract + # time 3-5x on Windows GitHub-hosted runners. + - name: Disable Defender scanning for extract paths + shell: pwsh + run: | + Add-MpPreference -ExclusionPath "${{ runner.temp }}" -ErrorAction SilentlyContinue + Add-MpPreference -ExclusionPath "${{ github.workspace }}" -ErrorAction SilentlyContinue + Add-MpPreference -ExclusionPath "C:\a" -ErrorAction SilentlyContinue + Write-Host "Defender exclusions applied" + - name: Extract build artifact shell: bash run: | @@ -274,13 +286,6 @@ jobs: - name: Setup Graphviz run: choco install graphviz -y --no-progress - - name: Set up Quarto - uses: quarto-dev/quarto-actions/setup@v2 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tinytex: true - - name: Setup AWS S3 Access uses: aws-actions/configure-aws-credentials@v6 with: From 4275a436fc06300ade5bb7201c40876e2dd83ad0 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 27 May 2026 12:46:48 -0500 Subject: [PATCH 21/25] ci: bump test-merge to 6 shards across Linux electron, Windows, and chromium Push each suite to 6 parallel shards on test-merge to see if we hit new bottlenecks (build-once artifact, predictive sharding distribution, or runner availability) before settling on a permanent ceiling. max_failures: 4 across all suites (20 / 6 = 3.33, rounded up). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/test-merge.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test-merge.yml b/.github/workflows/test-merge.yml index 13428af77e33..89bfb6f1993c 100644 --- a/.github/workflows/test-merge.yml +++ b/.github/workflows/test-merge.yml @@ -15,14 +15,14 @@ on: - .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: "" @@ -31,15 +31,15 @@ jobs: install_undetectable_interpreters: false install_license: false upload_logs: false - shards: 3 - matrix: '{"shard":[1,2,3]}' + shards: 6 + matrix: '{"shard":[1,2,3,4,5,6]}' workers: 2 allow_soft_fail: true - max_failures: 7 # 20 max failures / 3 shards = 6.66, rounded up to 7 + max_failures: 4 # 20 max failures / 6 shards = 3.33, rounded up to 4 secrets: inherit setup-windows: - name: setup-windows + name: setup uses: ./.github/workflows/test-e2e-windows-build.yml secrets: inherit with: @@ -56,13 +56,13 @@ jobs: display_name: "electron-win" allow_soft_fail: true upload_logs: false - shards: 4 - matrix: '{"shard":[1,2,3,4]}' - max_failures: 5 # 20 max failures / 4 shards = 5 + 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: "" @@ -71,11 +71,11 @@ jobs: install_undetectable_interpreters: false install_license: true upload_logs: true - 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 unit-tests: From 26731e78185bb540a2996d35517e9ad76bebc2df Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 27 May 2026 13:14:47 -0500 Subject: [PATCH 22/25] ci: extend Defender exclusion to build-windows job Apply the same Defender path exclusion to setup-windows/build-windows as we added to the e2e shards. This covers npm install (100k+ files, currently ~22 min), cache restore, compile (writes thousands of .js), and Playwright browser install (~600 MB). Realistic upside is 5-10 minutes off the 43 min build job. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/test-e2e-windows-build.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/test-e2e-windows-build.yml b/.github/workflows/test-e2e-windows-build.yml index 1a1e8e326643..db6006f8c671 100644 --- a/.github/workflows/test-e2e-windows-build.yml +++ b/.github/workflows/test-e2e-windows-build.yml @@ -47,6 +47,18 @@ jobs: 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. + # Excluding workspace + temp paths cuts these file-heavy steps + # significantly on Windows GitHub-hosted runners. Apply once, early. + - name: Disable Defender scanning for build paths + shell: pwsh + run: | + Add-MpPreference -ExclusionPath "${{ runner.temp }}" -ErrorAction SilentlyContinue + Add-MpPreference -ExclusionPath "${{ github.workspace }}" -ErrorAction SilentlyContinue + Add-MpPreference -ExclusionPath "C:\a" -ErrorAction SilentlyContinue + Write-Host "Defender exclusions applied" + - name: 📦 Restore caches if: ${{ !inputs.skip_cache }} id: restore-caches From 76a2dfabff2935dfd9a8ef9131e0248cbf281a28 Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 27 May 2026 13:46:54 -0500 Subject: [PATCH 23/25] ci: lower test-merge chromium shards from 6 to 5 6 chromium shards was a copy of the Linux/Windows bump but chromium has a smaller test footprint that doesn't justify the extra shard. 5 keeps it under the artifact-contention concern raised by the multi-shard download throttling. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/test-merge.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-merge.yml b/.github/workflows/test-merge.yml index 89bfb6f1993c..7f036b950abc 100644 --- a/.github/workflows/test-merge.yml +++ b/.github/workflows/test-merge.yml @@ -71,11 +71,11 @@ jobs: install_undetectable_interpreters: false install_license: true upload_logs: true - shards: 6 - matrix: '{"shard":[1,2,3,4,5,6]}' + shards: 5 + matrix: '{"shard":[1,2,3,4,5]}' workers: 2 allow_soft_fail: true - max_failures: 4 # 20 max failures / 6 shards = 3.33, rounded up to 4 + max_failures: 4 # 20 max failures / 5 shards = 4 secrets: inherit unit-tests: From c548bb585fff668aab0d966bfe31054cce4840cc Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 27 May 2026 14:42:04 -0500 Subject: [PATCH 24/25] ci: fix Defender exclusion paths and add verification logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The earlier Defender exclusion only covered github.workspace (D:\a\positron\positron) but the extract step writes to dirname() of that — D:\a\positron — which Defender was still scanning. That's why extract only dropped from 8m to 6m instead of the projected 2-3m. Now also exclude: - Split-Path -Parent of github.workspace (actual extract destination) - D:\a (GitHub Actions Windows runner workspace root) - Keep C:\a for safety Also log Get-MpPreference.ExclusionPath after applying to verify each path actually got registered. Applies to both build-windows and the e2e shard run. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/test-e2e-windows-build.yml | 17 +++++++++---- .github/workflows/test-e2e-windows-run.yml | 25 +++++++++++++++----- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test-e2e-windows-build.yml b/.github/workflows/test-e2e-windows-build.yml index db6006f8c671..ff28b5fa152b 100644 --- a/.github/workflows/test-e2e-windows-build.yml +++ b/.github/workflows/test-e2e-windows-build.yml @@ -49,15 +49,24 @@ jobs: # Defender's real-time scanning hits every file created during cache # restore, npm install (100k+ files), compile, and Playwright install. - # Excluding workspace + temp paths cuts these file-heavy steps - # significantly on Windows GitHub-hosted runners. Apply once, early. + # 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 "${{ github.workspace }}" -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 "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 }} diff --git a/.github/workflows/test-e2e-windows-run.yml b/.github/workflows/test-e2e-windows-run.yml index afd8966d365e..2d565ca08794 100644 --- a/.github/workflows/test-e2e-windows-run.yml +++ b/.github/workflows/test-e2e-windows-run.yml @@ -148,17 +148,30 @@ jobs: name: positron-build-windows path: ${{ runner.temp }}/artifacts - # Defender's real-time scanning hits every file created during extract, - # which is the dominant cost for node_modules-heavy archives (~50k small - # files). Excluding the workspace and temp paths typically cuts extract - # time 3-5x on Windows GitHub-hosted runners. + # 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 "${{ github.workspace }}" -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 "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 From 749b8a3deb8694d8cb31fa6dbb8a855f8614068f Mon Sep 17 00:00:00 2001 From: Marie Idleman Date: Wed, 27 May 2026 15:50:29 -0500 Subject: [PATCH 25/25] ci: restore Quarto setup on Windows e2e shards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous commit (c02396e98b) removed this on the assumption that Positron's bundled Quarto 1.9.38 was sufficient for e2e tests. That was wrong: the e2e tests under test/e2e/tests/quarto/ failed without a system Quarto installed. Restoring the action as-is. The cost is real (~2m/shard × 6 = ~12 runner-min/merge) but tests pass is non-negotiable. Worth revisiting later as a separate effort, possibly via cache or runner image, after we have more data on what specifically needs system Quarto vs the bundled binary. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/test-e2e-windows-run.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/test-e2e-windows-run.yml b/.github/workflows/test-e2e-windows-run.yml index 2d565ca08794..01f66a587654 100644 --- a/.github/workflows/test-e2e-windows-run.yml +++ b/.github/workflows/test-e2e-windows-run.yml @@ -299,6 +299,13 @@ jobs: - name: Setup Graphviz run: choco install graphviz -y --no-progress + - name: Set up Quarto + uses: quarto-dev/quarto-actions/setup@v2 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tinytex: true + - name: Setup AWS S3 Access uses: aws-actions/configure-aws-credentials@v6 with: