ci: unify Windows CI on native self-hosted runners

Replace the Wine-emulated windows job and the independent windows-native
job with a single required native Windows job that runs
check:ci:windows-complete on the hosted larger runner, falling back to
the self-hosted [self-hosted, dsh-win-ci, windows] pool under failover
(DSH_CI_FAILOVER=selfhosted). The serial-windows standby mirrors
serial-linux-selfhosted: master-only, unsharded, self-hosted, absent
from all-checks-passed.

Remove wine-apt-cache, the check:windows-wine script reference, and the
dual-lane topology the two Windows notes described.
This commit is contained in:
Chinesezjc
2026-08-10 22:30:46 +08:00
parent 72bdfeea60
commit 5d8d79ce92
3 changed files with 46 additions and 134 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
# AGENTS.md — GitHub Actions
Run jobs on Windows runners (`windows-*` labels) under native `pwsh`. The pull-request `windows` job is the deliberate exception: it runs Windows Node under Wine on hosted Linux and blocks `all checks passed`; `windows-native` runs automatically on `windows-2025` but reports independently — see the [dual-lane Agent Note](../.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md).
Run jobs on Windows runners (`windows-*` labels) under native `pwsh`. The required pull-request `windows` job and the master `serial-windows` standby both run on real Windows; the self-hosted pool uses labels `[self-hosted, dsh-win-ci, windows]`. Under failover (`DSH_CI_FAILOVER=selfhosted`) the `windows` job retargets onto the self-hosted pool.
+45 -132
View File
@@ -313,123 +313,18 @@ jobs:
- name: Run complete keyless Python suite
run: uv run --python 3.10 --group test --project python/sdk pytest
# The required pull-request Windows signal: the two blocking win32 surfaces
# (workspace build, production site) execute with real, checksum-verified
# Windows Node under Wine on standard hosted Linux. The independent
# windows-native job below keeps the complete native-kernel inventory —
# including the observational portability gates this lane does not run —
# on real windows-2025. This job only provisions runner state (caches,
# apt); scripts/wine-windows-gates.sh owns the gate logic and is the same
# script the optional local gate `pnpm run check:windows-wine` runs.
# Current topology and fidelity limits live in
# .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md
# Required pull-request Windows signal on real Windows. Under normal
# operation this runs on the hosted larger runner; under failover
# (DSH_CI_FAILOVER=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.
windows:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
name: windows node 24 / wine blocking
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: pnpm/action-setup@v4
with:
dest: ${{ runner.temp }}/setup-pnpm
- uses: actions/setup-node@v6
with:
node-version: ${{ env.PRIMARY_NODE_VERSION }}
- name: Configure pnpm store path
id: pnpm-store
run: |
store_root="$HOME/.local/share/pnpm/store"
echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"
store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)
echo "path=$store_path" >> "$GITHUB_OUTPUT"
- uses: actions/cache/restore@v4
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-
# Master's wine-apt-cache job seeds the default-branch scope every pull
# request can read; a save from this job only reaches reruns of the
# same merge ref.
- name: Compose Wine apt cache key
id: wine-cache-key
run: echo "key=wine-debs-${ImageOS:-linux}-${ImageVersion:-v0}" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v4
with:
path: ~/wine-debs
key: ${{ steps.wine-cache-key.outputs.key }}
# Runner provisioning only — a developer machine installs Wine through
# its own package manager; the gate script assumes a wine binary and
# fails loud without one. Wine from the apt cache when present; else
# download the full dependency closure once and keep it for the next
# run. The `wine` dispatcher package (not bare `wine64`) is what puts a
# binary on PATH.
- name: Install Wine
run: |
if compgen -G "$HOME/wine-debs/*.deb" > /dev/null; then
sudo apt-get install -y --no-install-recommends "$HOME"/wine-debs/*.deb
else
sudo apt-get update
sudo apt-get install -y --no-install-recommends --download-only wine
mkdir -p "$HOME/wine-debs"
cp /var/cache/apt/archives/*.deb "$HOME/wine-debs/" 2>/dev/null || true
sudo apt-get install -y --no-install-recommends wine
fi
- name: Run the Wine Windows gates
run: bash scripts/wine-windows-gates.sh
- name: Shut down wineserver
if: always()
run: wineserver -k 2>/dev/null || true
# Master seeds the Wine apt-archive cache in the default-branch scope,
# which every pull request's windows job can restore; saves from
# pull-request runs are scoped to their own merge ref and help nobody
# else. Runs in seconds when the image version already has a cache.
wine-apt-cache:
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
name: wine apt cache
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Compose Wine apt cache key
id: wine-cache-key
run: echo "key=wine-debs-${ImageOS:-linux}-${ImageVersion:-v0}" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v4
id: wine-cache
with:
path: ~/wine-debs
key: ${{ steps.wine-cache-key.outputs.key }}
- name: Download the Wine dependency closure
if: steps.wine-cache.outputs.cache-hit != 'true'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends --download-only wine
mkdir -p "$HOME/wine-debs"
cp /var/cache/apt/archives/*.deb "$HOME/wine-debs/"
du -sh "$HOME/wine-debs"
# 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.
# See the dual Wine/native pull-request CI decision:
# .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md
windows-native:
if: github.event_name == 'pull_request'
runs-on: dsh-windows-2025-16core
runs-on: >-
${{ vars.DSH_CI_FAILOVER == '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: 60
env:
@@ -455,8 +350,10 @@ jobs:
with:
node-version: ${{ env.PRIMARY_NODE_VERSION }}
# Extracting the many-file pnpm store cache is slower than a clean install,
# and saving it adds more latency after the gates.
# 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
@@ -621,14 +518,22 @@ jobs:
DSH_SNAPSHOT_MAX_CONCURRENCY: '1'
run: pnpm run check:ci
# Hot-standby drill for the in-house self-hosted Windows pool: every master
# move re-runs the complete unsharded Windows gate inventory on the persistent
# VM, continuously proving that environment can take over the required
# `windows` lane if the hosted pool degrades (the switch is setting the
# writer-manageable DSH_CI_FAILOVER variable — see the failover runbook, no
# merge required). Push-triggered, so this lane always executes the base
# branch's own workflow definition. Non-blocking for pull requests; absent
# from all-checks-passed.needs by design — the required `windows` job owns
# the PR verdict. No cache steps because the VM's persistent pnpm store
# and tool caches make them redundant (and saving here would poison the
# hosted cache namespace with self-hosted paths).
serial-windows:
# Real-Windows signal on the in-house self-hosted Windows pool. This lane
# runs the complete primary Node CI on actual Windows (in contrast to the
# Wine-emulated `windows` job), so win32-native output is exercised where
# it matters. Selector matches our self-hosted runners by label.
if: github.event_name == 'pull_request'
name: serial / windows
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
name: serial / windows (self-hosted standby)
runs-on: [self-hosted, dsh-win-ci, windows]
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
@@ -646,20 +551,30 @@ jobs:
with:
node-version: ${{ env.PRIMARY_NODE_VERSION }}
- name: Configure persistent pnpm store
shell: pwsh
run: |
$storeRoot = "$env:LOCALAPPDATA\pnpm\store"
echo "PNPM_CONFIG_STORE_DIR=$storeRoot" >> $env:GITHUB_ENV
- name: Install (immutable)
shell: pwsh
run: pnpm install --frozen-lockfile
- name: Run complete unsharded primary Node CI serially
# The persistent VM image owns Playwright's Windows system dependencies;
# this step also proves that browser provisioning remains usable for
# failover.
- name: Install Playwright Chromium
shell: pwsh
run: pnpm --filter @deepseek-ai/dsh-frontend exec playwright install chromium
- name: Run complete unsharded Windows gate inventory serially
shell: pwsh
env:
DSH_COVERAGE_MAX_WORKERS: '1'
DSH_E2E_MAX_WORKERS: '1'
DSH_GATE_CONCURRENCY: '1'
DSH_OXLINT_THREADS: '1'
DSH_PUBLINT_CONCURRENCY: '1'
DSH_SNAPSHOT_MAX_CONCURRENCY: '1'
run: pnpm run check:ci
run: pnpm run check:ci:windows-complete
# Manual, bounded comparison of the actual critical Linux and Windows lanes.
# The named pools are restricted at the organization level to this repository.
@@ -870,9 +785,7 @@ jobs:
# Single stable required check for branch protection: require "all checks
# passed" instead of enumerating matrix legs whose names change as lanes and
# node versions evolve. Every blocking job in THIS workflow must be listed in
# `needs`. The required Wine job is listed as `windows`; `windows-native` is
# deliberately absent so its independent result never delays or changes this
# verdict. (`needs` cannot reach across workflow files; e2e.yml stays its own
# `needs`. (`needs` cannot reach across workflow files; e2e.yml stays its own
# check.)
# `if: always()` is load-bearing: without it a failed dependency
# would SKIP this job, and GitHub counts a skipped required check as passing
@@ -889,7 +802,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, windows, serial-windows]
needs: [node-24, node-24-coverage, node-24-consumers, node-compat, python-sdk, windows]
if: always() && github.event_name == 'pull_request'
steps:
- name: Fail if any needed job did not succeed