From 58cc29b4f177c3a64b98ee76f993e501c74090de Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Tue, 25 Aug 2026 12:45:47 +0800 Subject: [PATCH] test(windows): split native job into build/coverage/native-tests/observational Keep the 4-partition coverage profile, split the monolithic windows-native job into smaller required jobs (build, coverage, native-tests) plus a non-blocking observational job. Update ci-workflow.spec for the new topology. --- .github/workflows/ci.yml | 155 ++++++++++++++++++++++++++++-------- scripts/ci-workflow.spec.ts | 80 +++++++++++++------ 2 files changed, 178 insertions(+), 57 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47e8ad6494..aa36a92dc8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -397,63 +397,152 @@ jobs: if: always() run: wineserver -k 2>/dev/null || true - # Every pull request also gets a real Windows-kernel signal. This job keeps - # its own unmasked conclusion but is deliberately absent from - # all-checks-passed.needs, so it never delays or changes that required - # verdict. Under normal operation it runs on the hosted larger runner; under - # Windows failover (DSH_CI_FAILOVER_WINDOWS=selfhosted) it retargets onto the - # in-house self-hosted Windows pool. Dependabot PRs are excluded from the - # self-hosted pool and stay queued for the hosted runner — see the failover - # runbook. This Windows switch is independent of the Linux - # DSH_CI_FAILOVER_LINUX variable that retargets the three required Linux jobs - # and the all-checks-passed verdict above. - windows-native: + # Every pull request also gets real Windows-kernel signals. The former + # monolithic windows-native job is split into smaller jobs so one slow + # coverage gate does not hold up build/static results, while the total + # per-job process count stays lower. Observational checks are non-blocking. + # Dependabot PRs are excluded from the self-hosted pool and stay queued for + # the hosted runner. + windows-build: if: github.event_name == 'pull_request' runs-on: >- ${{ vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && fromJSON('["self-hosted", "dsh-win-ci", "windows"]') || 'dsh-windows-2025-16core' }} - name: windows node 24 / native complete - timeout-minutes: 120 - env: - DSH_COVERAGE_MAX_WORKERS: '6' - DSH_COVERAGE_PARTITIONS: '4' - # Instrumented process and polling fixtures can exceed Vitest's defaults - # under the complete lane's concurrent gate load. - DSH_COVERAGE_TEST_TIMEOUT_MS: '30000' - DSH_GATE_CONCURRENCY: '4' - DSH_PUBLINT_CONCURRENCY: '8' + name: windows node 24 / build + timeout-minutes: 60 steps: - uses: actions/checkout@v6 with: persist-credentials: false - - name: Enable Developer Mode (symlink support) shell: pwsh run: >- reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1" - - uses: pnpm/action-setup@v4 with: dest: ${{ runner.temp }}/setup-pnpm-js - - uses: actions/setup-node@v6 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} - - # Extracting the many-file pnpm store cache is slower than a clean - # install on hosted Windows runners, and saving it adds latency after - # the gates. The self-hosted VM's persistent store makes caching - # redundant. - name: Install (immutable) shell: pwsh run: pnpm install --frozen-lockfile - - - name: Run complete native Windows gate inventory + - name: Run blocking Windows builds shell: pwsh - run: pnpm run check:ci:windows-complete + run: pnpm run check:ci:windows-blocking + + windows-coverage: + if: github.event_name == 'pull_request' + runs-on: >- + ${{ vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted' + && github.event.pull_request.user.login != 'dependabot[bot]' + && fromJSON('["self-hosted", "dsh-win-ci", "windows"]') + || 'dsh-windows-2025-16core' }} + name: windows node 24 / coverage + timeout-minutes: 120 + env: + DSH_COVERAGE_MAX_WORKERS: '6' + DSH_COVERAGE_PARTITIONS: '4' + DSH_COVERAGE_TEST_TIMEOUT_MS: '30000' + DSH_GATE_CONCURRENCY: '3' + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Enable Developer Mode (symlink support) + shell: pwsh + run: >- + reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" + /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1" + - uses: pnpm/action-setup@v4 + with: + dest: ${{ runner.temp }}/setup-pnpm-js + - uses: actions/setup-node@v6 + with: + node-version: ${{ env.PRIMARY_NODE_VERSION }} + - name: Install (immutable) + shell: pwsh + run: pnpm install --frozen-lockfile + - name: Build before coverage + shell: pwsh + run: pnpm run build + - name: Run Windows coverage + shell: pwsh + run: pnpm run check:ci:coverage + + windows-native-tests: + if: github.event_name == 'pull_request' + runs-on: >- + ${{ vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted' + && github.event.pull_request.user.login != 'dependabot[bot]' + && fromJSON('["self-hosted", "dsh-win-ci", "windows"]') + || 'dsh-windows-2025-16core' }} + name: windows node 24 / native tests + timeout-minutes: 60 + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Enable Developer Mode (symlink support) + shell: pwsh + run: >- + reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" + /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1" + - uses: pnpm/action-setup@v4 + with: + dest: ${{ runner.temp }}/setup-pnpm-js + - uses: actions/setup-node@v6 + with: + node-version: ${{ env.PRIMARY_NODE_VERSION }} + - name: Install (immutable) + shell: pwsh + run: pnpm install --frozen-lockfile + - name: Run Windows-specific native tests + shell: pwsh + run: >- + pnpm exec vitest run + packages/shell/tool-pwsh/tests/loader.spec.ts + packages/workflow/workflow-worker-thread/tests/workflow-worker-thread.spec.ts + packages/workflow/tool-ralph/tests/integration.spec.ts + packages/subprocess/subprocess-local/tests/process-exit.spec.ts + packages/session/session-persistence-sqlite/tests/differential.spec.ts + + windows-observational: + if: github.event_name == 'pull_request' + continue-on-error: true + runs-on: >- + ${{ vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted' + && github.event.pull_request.user.login != 'dependabot[bot]' + && fromJSON('["self-hosted", "dsh-win-ci", "windows"]') + || 'dsh-windows-2025-16core' }} + name: windows node 24 / observational + timeout-minutes: 60 + env: + DSH_PUBLINT_CONCURRENCY: '8' + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Enable Developer Mode (symlink support) + shell: pwsh + run: >- + reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" + /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1" + - uses: pnpm/action-setup@v4 + with: + dest: ${{ runner.temp }}/setup-pnpm-js + - uses: actions/setup-node@v6 + with: + node-version: ${{ env.PRIMARY_NODE_VERSION }} + - name: Install (immutable) + shell: pwsh + run: pnpm install --frozen-lockfile + - name: Run Windows observational gates + shell: pwsh + run: pnpm run check:ci:windows-observational # Single stable required check for branch protection: require "all checks # passed" instead of enumerating matrix legs whose names change as lanes and @@ -479,7 +568,7 @@ jobs: && github.event.pull_request.user.login != 'dependabot[bot]' && fromJSON('["self-hosted", "linux", "x64", "vm-backup"]') || 'ubuntu-latest' }} - needs: [node-24, node-24-coverage, node-24-consumers, node-compat, python-sdk, python-runtime, windows] + needs: [node-24, node-24-coverage, node-24-consumers, node-compat, python-sdk, python-runtime, windows, windows-build, windows-coverage, windows-native-tests] if: always() && github.event_name == 'pull_request' steps: - name: Fail if any needed job did not succeed diff --git a/scripts/ci-workflow.spec.ts b/scripts/ci-workflow.spec.ts index 7f60d75352..e5aceb8e25 100644 --- a/scripts/ci-workflow.spec.ts +++ b/scripts/ci-workflow.spec.ts @@ -27,21 +27,24 @@ describe('CI workflow', () => { for (const { jobName, step } of setups) { expect(step, `${jobName} must not share pnpm/action-setup's default destination`).toMatchObject({ with: { - dest: jobName === 'windows-native' + dest: jobName.startsWith('windows-') ? nativeWindowsPnpmDestination : runnerPrivatePnpmDestination, }, }) - if (jobName === 'windows-native') expect(step).not.toMatchObject({ with: { standalone: true } }) + if (jobName.startsWith('windows-')) expect(step).not.toMatchObject({ with: { standalone: true } }) } }) - it('keeps a required Wine Windows job, a non-blocking native Windows job with failover, and a master-only standby', () => { + it('keeps required Wine and split native Windows jobs with failover, plus a master-only standby', () => { const workflow = loadWorkflow('.github/workflows/ci.yml') const masterWorkflow = loadWorkflow('.github/workflows/ci-master.yml') if (!isRecord(workflow.jobs) || !isRecord(workflow.jobs.windows) - || !isRecord(workflow.jobs['windows-native']) + || !isRecord(workflow.jobs['windows-build']) + || !isRecord(workflow.jobs['windows-coverage']) + || !isRecord(workflow.jobs['windows-native-tests']) + || !isRecord(workflow.jobs['windows-observational']) || !isRecord(workflow.jobs['node-24']) || !isRecord(workflow.jobs['node-24-coverage']) || !isRecord(workflow.jobs['node-24-consumers']) @@ -49,11 +52,14 @@ describe('CI workflow', () => { || !isRecord(masterWorkflow.jobs) || !isRecord(masterWorkflow.jobs['wine-apt-cache']) || !isRecord(masterWorkflow.jobs['serial-windows'])) { - throw new TypeError('CI workflow must define windows, windows-native, node-24, node-24-coverage, node-24-consumers, and all-checks-passed; ci-master must define wine-apt-cache and serial-windows') + throw new TypeError('CI workflow must define windows, windows-build, windows-coverage, windows-native-tests, windows-observational, node-24, node-24-coverage, node-24-consumers, and all-checks-passed; ci-master must define wine-apt-cache and serial-windows') } const windows = workflow.jobs.windows - const windowsNative = workflow.jobs['windows-native'] + const windowsBuild = workflow.jobs['windows-build'] + const windowsCoverage = workflow.jobs['windows-coverage'] + const windowsNativeTests = workflow.jobs['windows-native-tests'] + const windowsObservational = workflow.jobs['windows-observational'] const wineAptCache = masterWorkflow.jobs['wine-apt-cache'] const serialWindows = masterWorkflow.jobs['serial-windows'] const node24 = workflow.jobs['node-24'] @@ -73,24 +79,46 @@ describe('CI workflow', () => { expect(windows.if).toBe("github.event_name == 'pull_request'") expect(commandSteps.some(step => step.run.includes('wine-windows-gates.sh'))).toBe(true) - // windows-native: non-blocking native job with failover, runs windows-complete. - // Its pool is resolved by the Windows-specific switch. - expect(typeof windowsNative['runs-on']).toBe('string') - expect(windowsNative['runs-on']).toContain('DSH_CI_FAILOVER_WINDOWS') - expect(windowsNative['runs-on']).not.toContain('DSH_CI_FAILOVER_LINUX') - expect(windowsNative['runs-on']).toContain('self-hosted') - expect(windowsNative['runs-on']).toContain('dsh-win-ci') - expect(windowsNative['runs-on']).toContain('dsh-windows-2025-16core') - expect(windowsNative.name).toBe('windows node 24 / native complete') - expect(windowsNative.if).toBe("github.event_name == 'pull_request'") - expect(windowsNative.env).toMatchObject({ - DSH_COVERAGE_TEST_TIMEOUT_MS: '30000', - }) - const nativeSteps = windowsNative.steps as unknown[] - const nativeCommandSteps = nativeSteps.filter((step): step is Record & { run: string } => ( + // The split native jobs all resolve their pool through the Windows switch. + for (const [jobName, job] of [['windows-build', windowsBuild], ['windows-coverage', windowsCoverage], ['windows-native-tests', windowsNativeTests], ['windows-observational', windowsObservational]] as const) { + expect(typeof job['runs-on']).toBe('string') + expect(job['runs-on'], `${jobName} runs-on must use the Windows failover switch`).toContain('DSH_CI_FAILOVER_WINDOWS') + expect(job['runs-on'], `${jobName} runs-on must not use the Linux failover switch`).not.toContain('DSH_CI_FAILOVER_LINUX') + expect(job['runs-on']).toContain('self-hosted') + expect(job['runs-on']).toContain('dsh-win-ci') + expect(job['runs-on']).toContain('dsh-windows-2025-16core') + expect(job.if).toBe("github.event_name == 'pull_request'") + } + + // windows-build runs the blocking build/site pair. + expect(windowsBuild.name).toBe('windows node 24 / build') + const buildSteps = windowsBuild.steps as unknown[] + const buildCommands = buildSteps.filter((step): step is Record & { run: string } => ( isRecord(step) && typeof step.run === 'string' )) - expect(nativeCommandSteps.map(step => step.run)).toContain('pnpm run check:ci:windows-complete') + expect(buildCommands.map(step => step.run)).toContain('pnpm run check:ci:windows-blocking') + + // windows-coverage uses the lower 4-partition profile. + expect(windowsCoverage.name).toBe('windows node 24 / coverage') + expect(windowsCoverage.env).toMatchObject({ DSH_COVERAGE_PARTITIONS: '4' }) + const coverageSteps = windowsCoverage.steps as unknown[] + const coverageCommands = coverageSteps.filter((step): step is Record & { run: string } => ( + isRecord(step) && typeof step.run === 'string' + )) + expect(coverageCommands.map(step => step.run)).toContain('pnpm run check:ci:coverage') + + // windows-native-tests runs the Windows-specific specs. + expect(windowsNativeTests.name).toBe('windows node 24 / native tests') + const nativeTestSteps = windowsNativeTests.steps as unknown[] + const nativeTestCommands = nativeTestSteps.filter((step): step is Record & { run: string } => ( + isRecord(step) && typeof step.run === 'string' + )) + expect(nativeTestCommands.map(step => step.run).join('\n')).toContain('tool-pwsh/tests/loader.spec.ts') + expect(nativeTestCommands.map(step => step.run).join('\n')).toContain('workflow-worker-thread.spec.ts') + + // windows-observational is non-blocking. + expect(windowsObservational.name).toBe('windows node 24 / observational') + expect(windowsObservational['continue-on-error']).toBe(true) // wine-apt-cache: master-only, seeds the Wine apt cache, lives in ci-master. expect(wineAptCache.if).toBe("github.event_name == 'push' && github.ref == 'refs/heads/master'") @@ -101,9 +129,13 @@ describe('CI workflow', () => { expect(serialWindows['runs-on']).toEqual(['self-hosted', 'dsh-win-ci', 'windows']) expect(serialWindows.name).toBe('serial / windows (self-hosted standby)') - // Aggregate: Wine `windows` required, native `windows-native` excluded. + // Aggregate: Wine and the three required split native jobs are needed; + // observational stays out of the verdict. expect(aggregate.needs).toContain('windows') - expect(aggregate.needs).not.toContain('windows-native') + expect(aggregate.needs).toContain('windows-build') + expect(aggregate.needs).toContain('windows-coverage') + expect(aggregate.needs).toContain('windows-native-tests') + expect(aggregate.needs).not.toContain('windows-observational') expect(aggregate.needs).not.toContain('serial-windows') // Linux failover is a separate switch: the three required Linux workers