ci: replace the 33-way test matrix with full and package-manager lanes

The test job ran the entire suite on 3 OSes x 3 Node lines x 4 package
managers, 33 full runs per push. Windows lanes took 28 to 39 minutes and
macOS 14 to 19, so one push queued about 1.5 hours of wall clock behind
runner concurrency, and timing-sensitive tests got 33 chances to flake.
The package-manager axis only ever changed one environment variable that
nine suites read.

Now there are 13 lanes. Five run the full suite: Linux on Node 18, 20
and 22, plus Windows and macOS on Node 22. Eight are package-manager
lanes on Node 22 that run only the suites which read
CLAUDE_CODE_PACKAGE_MANAGER or CLAUDE_PACKAGE_MANAGER; the file list is
grepped at run time so it cannot drift as tests are added. Full lanes
use the runner's 4-process pool on Linux and macOS; Windows stays serial
because its process spawn and temp-dir cleanup are the flaky part.
This commit is contained in:
Affaan Mustafa authored and Affaan Mustafa committed 2026-10-20 10:47:58 -04:00
1 parent 9471ef4724
commit 07971588f8
2 files changed
+48 -17

No files matched your search

+45 -13
View File
@@ -18,22 +18,35 @@ permissions:
jobs: jobs:
test: test:
name: Test (${{ matrix.os }}, Node ${{ matrix.node }}, ${{ matrix.pm }}) name: Test (${{ matrix.os }}, Node ${{ matrix.node }}, ${{ matrix.pm }}, ${{ matrix.scope }})
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
# The complete cross-platform suite reaches its final installer cases at # The serial Windows full lane reaches its final installer cases at about
# 30 minutes on hosted Windows Node 18; retain a bounded completion window. # 30 minutes; retain a bounded completion window.
timeout-minutes: 45 timeout-minutes: 45
strategy: strategy:
fail-fast: false fail-fast: false
# Two kinds of lane. `full` runs the whole suite; `pm` runs only the suites
# that read CLAUDE_CODE_PACKAGE_MANAGER, which is all the package-manager
# axis ever changed. The old 33-way cross product ran the full suite 33
# times per push, with Windows lanes at 30 to 40 minutes each.
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest] include:
node: ['18.x', '20.x', '22.x'] # Full suite: Linux on every supported Node line, current LTS on Windows and macOS.
pm: [npm, pnpm, yarn, bun] - { os: ubuntu-latest, node: '18.x', pm: npm, scope: full }
exclude: - { os: ubuntu-latest, node: '20.x', pm: npm, scope: full }
# Bun has limited Windows support - { os: ubuntu-latest, node: '22.x', pm: npm, scope: full }
- os: windows-latest - { os: windows-latest, node: '22.x', pm: npm, scope: full }
pm: bun - { os: macos-latest, node: '22.x', pm: npm, scope: full }
# Package-manager lanes on current LTS. Bun has limited Windows support.
- { os: ubuntu-latest, node: '22.x', pm: pnpm, scope: pm }
- { os: ubuntu-latest, node: '22.x', pm: yarn, scope: pm }
- { os: ubuntu-latest, node: '22.x', pm: bun, scope: pm }
- { os: windows-latest, node: '22.x', pm: pnpm, scope: pm }
- { os: windows-latest, node: '22.x', pm: yarn, scope: pm }
- { os: macos-latest, node: '22.x', pm: pnpm, scope: pm }
- { os: macos-latest, node: '22.x', pm: yarn, scope: pm }
- { os: macos-latest, node: '22.x', pm: bun, scope: pm }
steps: steps:
- name: Checkout - name: Checkout
@@ -125,18 +138,37 @@ jobs:
*) echo "Unsupported package manager: ${{ matrix.pm }}" && exit 1 ;; *) echo "Unsupported package manager: ${{ matrix.pm }}" && exit 1 ;;
esac esac
# Run tests # Run tests. Full lanes run every file through a 4-process pool on Linux
# and macOS; Windows stays serial because its process spawn and temp-dir
# cleanup are the flaky part. pm lanes derive their file list from the
# suites that actually read the package-manager variable, so the lane
# cannot drift out of date as tests are added.
- name: Run tests - name: Run tests
run: node tests/run-all.js shell: bash
run: |
if [ "${{ matrix.scope }}" = "pm" ]; then
ECC_TEST_ONLY=$(grep -rlE --include='*.test.js' 'CLAUDE(_CODE)?_PACKAGE_MANAGER' tests | sed 's#^tests/##' | sort | paste -sd, -)
export ECC_TEST_ONLY
echo "pm lane files: $ECC_TEST_ONLY"
fi
# Suites that run scripts/build-opencode.js, or rename a checkout path
# to a .backup-<pid> copy and restore it, mutate the shared checkout;
# suites that assert a wall-clock budget cannot share cores with the
# pool. Both groups run last, one at a time.
ECC_TEST_SERIAL=$(grep -rlE --include='*.test.js' 'build-opencode|\.backup-|dist\.backup|time budget' tests | sed 's#^tests/##' | sort | paste -sd, -)
export ECC_TEST_SERIAL
echo "serial after the pool: $ECC_TEST_SERIAL"
node tests/run-all.js
env: env:
CLAUDE_CODE_PACKAGE_MANAGER: ${{ matrix.pm }} CLAUDE_CODE_PACKAGE_MANAGER: ${{ matrix.pm }}
ECC_TEST_JOBS: ${{ runner.os == 'Windows' && '1' || '4' }}
# Upload test artifacts on failure # Upload test artifacts on failure
- name: Upload test artifacts - name: Upload test artifacts
if: failure() if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: test-results-${{ matrix.os }}-node${{ matrix.node }}-${{ matrix.pm }} name: test-results-${{ matrix.os }}-node${{ matrix.node }}-${{ matrix.pm }}-${{ matrix.scope }}
path: | path: |
tests/ tests/
!tests/node_modules/ !tests/node_modules/
+3 -4
View File
@@ -274,10 +274,9 @@ test('uses one shell-free focused runner across Linux, macOS, and Windows', () =
const packageJson = read(files.packageJson); const packageJson = read(files.packageJson);
const platformRunner = read(files.platformRunner); const platformRunner = read(files.platformRunner);
assert.match( for (const os of ['ubuntu-latest', 'windows-latest', 'macos-latest']) {
ci, assert.match(ci, new RegExp(`os:\\s*${os},[^}]*scope:\\s*full`), `${os} must run the full suite`);
/os:\s*\[ubuntu-latest,\s*windows-latest,\s*macos-latest\]/ }
);
assert.match( assert.match(
packageJson, packageJson,
/"test:plugin-setup-platform":\s*"node docker\/plugin-setup\/run-platform-tests\.js"/ /"test:plugin-setup-platform":\s*"node docker\/plugin-setup\/run-platform-tests\.js"/