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.
This commit is contained in:
Chinesezjc
2026-08-25 12:45:47 +08:00
parent 55ef5aad06
commit 58cc29b4f1
2 changed files with 178 additions and 57 deletions
+122 -33
View File
@@ -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
+56 -24
View File
@@ -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<string, unknown> & { 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<string, unknown> & { 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<string, unknown> & { 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<string, unknown> & { 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