name: CI master on: push: branches: [master] workflow_dispatch: inputs: suite: description: Manual CI suite to run required: true default: larger-runner-benchmark type: choice options: - larger-runner-benchmark - consolidated-runner-benchmark # New master pushes and manual runs replace obsolete checks in this workflow/ref. # Standby drills share cancellation; use completed runs as readiness evidence. concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: read env: PRIMARY_NODE_VERSION: '24' # CI runs must never report to the production telemetry endpoint baked # into apps/cli/cordis.yml (AppCLIEntry disables the row when set). DSH_TELEMETRY_DISABLED: '1' jobs: # These native runtime carriers are post-merge checks; release keeps all targets. python-runtime: if: github.event_name == 'push' && github.ref == 'refs/heads/master' name: python runtime / macOS and Linux ARM64 uses: ./.github/workflows/build-exe-for-python-sdk.yml with: targets: node24-linux-arm64,node24-macos-arm64,node24-macos-x64 ci: true secrets: DEEPSEEK_API_KEY_EXTERNAL: ${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }} # Wine runs once here, independently of the native serial standby aggregates. windows: if: github.event_name == 'push' && github.ref == 'refs/heads/master' runs-on: ubuntu-latest name: windows node 24 / wine timeout-minutes: 15 steps: - uses: actions/checkout@v6 with: persist-credentials: false - uses: pnpm/action-setup@v4 with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }} - 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 CI restores and seeds the image-specific Wine dependency cache. - 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 # The restored archive is the full --download-only closure of # `wine` for this runner image, so installing the .debs directly # with dpkg needs no repository access. apt-get would instead # re-download the same 100+ MB closure from the mirror, which has # stalled the job past its budget on a degraded runner network. # If the archive cannot satisfy the closure, fall back to the apt # network install. if ! sudo DEBIAN_FRONTEND=noninteractive dpkg -i "$HOME"/wine-debs/*.deb; then sudo DEBIAN_FRONTEND=noninteractive dpkg --configure -a || true sudo apt-get install -y --no-install-recommends "$HOME"/wine-debs/*.deb fi 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 # Hot-standby drill for the in-house self-hosted pool: every master move # re-runs the complete unsharded aggregate on the persistent 64-core VM, # continuously proving that environment can take over a required lane if # the hosted pools degrade (the switch is then setting the writer-manageable # DSH_CI_FAILOVER_LINUX variable — see the failover runbook, no merge required). # Push-triggered, so this lane always executes the base branch's own # workflow definition. This workflow never listens to pull_request, so the # drill does not appear in PR checks. 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-linux-selfhosted: if: github.event_name == 'push' && github.ref == 'refs/heads/master' name: serial / linux (self-hosted standby) runs-on: [self-hosted, linux, x64, vm-backup] steps: # DSH_ARCHIVE_BASE_REF below compares the frozen-archive gate against # github.event.before, so full history is required: depth 2 would miss it # on multi-commit or force pushes; full fetch is cheap here because # checkout resolves against the VM's local mirror. - uses: actions/checkout@v6 with: fetch-depth: 0 # Redirect the Node compile cache (enabled by pnpm and TypeScript) off # the root partition's /tmp before the first pnpm call in this lane — # see .agents/notes/implemented/process/2026-08-28-ci-node-compile-cache-data-disk.md. - name: Redirect Node compile cache to runner temp run: echo "NODE_COMPILE_CACHE=${{ runner.temp }}/node-compile-cache" >> "$GITHUB_ENV" - uses: pnpm/action-setup@v4 with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }} - uses: actions/setup-node@v6 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} - name: Configure persistent pnpm store run: echo "PNPM_CONFIG_STORE_DIR=$HOME/.local/share/pnpm/store" >> "$GITHUB_ENV" - name: Install (immutable) run: pnpm install --frozen-lockfile # The persistent VM image owns Playwright's Linux system packages; this # step also proves that browser provisioning remains usable for failover. - name: Install Playwright Chromium run: pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install chromium - name: Prepare bubblewrap (unrestrict userns) run: bash scripts/prepare-ci-bubblewrap.sh - name: Run complete unsharded primary Node CI serially env: DSH_ARCHIVE_BASE_REF: ${{ github.event.before }} 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:linux-primary # TODO(hosted-serial-ci): Re-enable the one remaining disabled hosted serial # reference job (serial-macos) before release. Re-enabling serial-macos does # not restore a Linux hosted-cache producer: decide whether to add a master # seeder or remove the restore-only steps if cold starts become a concern. serial-macos: if: false name: serial / macos runs-on: macos-latest steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4 with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }} - uses: actions/setup-node@v6 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} - name: Install (immutable) run: pnpm install --frozen-lockfile - name: Run complete unsharded primary Node CI serially env: DSH_COVERAGE_MAX_WORKERS: '1' DSH_E2E_MAX_WORKERS: '1' DSH_GATE_CONCURRENCY: '1' DSH_PUBLINT_CONCURRENCY: '1' 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_WINDOWS variable — see the failover # runbook, no merge required). Push-triggered, so this lane always executes # the base branch's own workflow definition. This workflow never listens to # pull_request, so the drill does not appear in PR checks. 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: 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: 120 steps: - uses: actions/checkout@v6 - 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-${{ github.run_id }}-${{ github.run_attempt }} - uses: actions/setup-node@v6 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} - name: Configure persistent pnpm store shell: pwsh # The store must share the ReFS workspace volume for the clone # import method below; LOCALAPPDATA (C:) would cross volumes and # break block clone. See 2026-08-30-windows-refs-store-block-clone-install. run: | $storeRoot = "F:\.pnpm-store" echo "PNPM_CONFIG_STORE_DIR=$storeRoot" >> $env:GITHUB_ENV - name: Install (immutable) shell: pwsh # See 2026-08-30-windows-refs-store-block-clone-install for the # ReFS block-clone rationale; use clone only on ReFS. run: >- $drive = (Split-Path -Qualifier $env:GITHUB_WORKSPACE).TrimEnd(':'); $fs = (Get-Volume -DriveLetter $drive).FileSystem; if ($fs -eq 'ReFS') { corepack pnpm install --frozen-lockfile --package-import-method=clone } else { pnpm install --frozen-lockfile } - name: Run complete unsharded Windows gate inventory serially shell: pwsh env: DSH_COVERAGE_MAX_WORKERS: '1' DSH_COVERAGE_TEST_TIMEOUT_MS: '90000' DSH_GATE_CONCURRENCY: '1' DSH_PUBLINT_CONCURRENCY: '1' 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. larger-runner-benchmark: if: github.event_name == 'workflow_dispatch' && inputs.suite == 'larger-runner-benchmark' # Default measures the repository's own fleet tiers; under the matching # platform's blacksmith failover value the tiers Blacksmith offers (up to # 32 vCPU) move onto their Blacksmith equivalents, while the 64/96-core # rows keep the fleet labels because Blacksmith has no such tier. runs-on: >- ${{ (matrix.platform == 'linux' && vars.DSH_CI_FAILOVER_LINUX == 'blacksmith' || matrix.platform == 'windows' && vars.DSH_CI_FAILOVER_WINDOWS == 'blacksmith') && matrix.blacksmith || matrix.runner }} timeout-minutes: 15 strategy: fail-fast: false max-parallel: 12 matrix: include: - platform: linux cores: '4' runner: dsh-ubuntu-24-04-4core blacksmith: blacksmith-4vcpu-ubuntu-2404 workload: typecheck - platform: linux cores: '8' runner: dsh-ubuntu-24-04-8core blacksmith: blacksmith-8vcpu-ubuntu-2404 workload: typecheck - platform: linux cores: '16' runner: dsh-ubuntu-24-04-16core blacksmith: blacksmith-16vcpu-ubuntu-2404 workload: typecheck - platform: linux cores: '32' runner: dsh-ubuntu-24-04-32core blacksmith: blacksmith-32vcpu-ubuntu-2404 workload: typecheck - platform: linux cores: '64' runner: dsh-ubuntu-24-04-64core blacksmith: '' workload: typecheck - platform: linux cores: '96' runner: dsh-ubuntu-24-04-96core blacksmith: '' workload: typecheck - platform: windows cores: '4' runner: dsh-windows-2025-4core blacksmith: blacksmith-4vcpu-windows-2025 workload: production-site - platform: windows cores: '8' runner: dsh-windows-2025-8core blacksmith: blacksmith-8vcpu-windows-2025 workload: production-site - platform: windows cores: '16' runner: dsh-windows-2025-16core blacksmith: blacksmith-16vcpu-windows-2025 workload: production-site - platform: windows cores: '32' runner: dsh-windows-2025-32core blacksmith: blacksmith-32vcpu-windows-2025 workload: production-site - platform: windows cores: '64' runner: dsh-windows-2025-64core blacksmith: '' workload: production-site - platform: windows cores: '96' runner: dsh-windows-2025-96core blacksmith: '' workload: production-site steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4 with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }} # The benchmark's Windows lanes deliberately skip the store cache like # the independent native Windows job; an empty input disables caching. - uses: actions/setup-node@v6 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} cache: ${{ matrix.platform == 'linux' && 'pnpm' || '' }} - name: Report runner capacity run: >- node -e "const os = require('node:os'); console.log(JSON.stringify({ arch: process.arch, cpus: os.cpus().length, memoryGiB: Math.round(os.totalmem() / 2 ** 30) }))" - name: Install (immutable) run: pnpm install --frozen-lockfile - name: Run critical Linux typecheck lane if: matrix.platform == 'linux' run: pnpm run typecheck - name: Run critical Windows production-site lane if: matrix.platform == 'windows' run: pnpm run docs:build # Manual comparison of the intended low-fanout topology. Linux runs the # complete unsharded primary aggregate with bounded in-runner parallelism; # Windows runs both blocking build targets concurrently through run-gates. consolidated-runner-benchmark: if: github.event_name == 'workflow_dispatch' && inputs.suite == 'consolidated-runner-benchmark' # Default measures the repository's own fleet tiers; under the matching # platform's blacksmith failover value the tiers Blacksmith offers (up to # 32 vCPU) move onto their Blacksmith equivalents, while the 64/96-core # rows keep the fleet labels because Blacksmith has no such tier. runs-on: >- ${{ (matrix.platform == 'linux' && vars.DSH_CI_FAILOVER_LINUX == 'blacksmith' || matrix.platform == 'windows' && vars.DSH_CI_FAILOVER_WINDOWS == 'blacksmith') && matrix.blacksmith || matrix.runner }} timeout-minutes: 15 strategy: fail-fast: false max-parallel: 12 matrix: include: - platform: linux cores: '4' runner: dsh-ubuntu-24-04-4core blacksmith: blacksmith-4vcpu-ubuntu-2404 workers: '4' - platform: linux cores: '8' runner: dsh-ubuntu-24-04-8core blacksmith: blacksmith-8vcpu-ubuntu-2404 workers: '8' - platform: linux cores: '16' runner: dsh-ubuntu-24-04-16core blacksmith: blacksmith-16vcpu-ubuntu-2404 workers: '16' - platform: linux cores: '32' runner: dsh-ubuntu-24-04-32core blacksmith: blacksmith-32vcpu-ubuntu-2404 workers: '32' - platform: linux cores: '64' runner: dsh-ubuntu-24-04-64core blacksmith: '' workers: '32' - platform: linux cores: '96' runner: dsh-ubuntu-24-04-96core blacksmith: '' workers: '32' - platform: windows cores: '4' runner: dsh-windows-2025-4core blacksmith: blacksmith-4vcpu-windows-2025 workers: '2' - platform: windows cores: '8' runner: dsh-windows-2025-8core blacksmith: blacksmith-8vcpu-windows-2025 workers: '2' - platform: windows cores: '16' runner: dsh-windows-2025-16core blacksmith: blacksmith-16vcpu-windows-2025 workers: '2' - platform: windows cores: '32' runner: dsh-windows-2025-32core blacksmith: blacksmith-32vcpu-windows-2025 workers: '2' - platform: windows cores: '64' runner: dsh-windows-2025-64core blacksmith: '' workers: '2' - platform: windows cores: '96' runner: dsh-windows-2025-96core blacksmith: '' workers: '2' steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4 with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }} # Unlike the larger-runner suite, both platforms cache the store here: # the consolidated topology measures cache mechanics as workload. - uses: actions/setup-node@v6 with: node-version: ${{ env.PRIMARY_NODE_VERSION }} cache: pnpm - name: Report runner capacity run: >- node -e "const os = require('node:os'); console.log(JSON.stringify({ arch: process.arch, cpus: os.cpus().length, memoryGiB: Math.round(os.totalmem() / 2 ** 30) }))" - name: Install and prepare Linux if: matrix.platform == 'linux' run: | pnpm install --frozen-lockfile & install_pid=$! bash scripts/prepare-ci-bubblewrap.sh & sandbox_pid=$! install_status=0 wait "$install_pid" || install_status=$? sandbox_status=0 wait "$sandbox_pid" || sandbox_status=$? if (( install_status != 0 )); then exit "$install_status"; fi exit "$sandbox_status" - name: Install (immutable) if: matrix.platform == 'windows' shell: pwsh run: pnpm install --frozen-lockfile - name: Run complete unsharded primary Node CI concurrently if: matrix.platform == 'linux' env: DSH_COVERAGE_MAX_WORKERS: ${{ matrix.workers }} DSH_GATE_CONCURRENCY: ${{ matrix.workers }} DSH_OXLINT_THREADS: ${{ matrix.workers }} DSH_PUBLINT_CONCURRENCY: ${{ matrix.workers }} DSH_SNAPSHOT_MAX_CONCURRENCY: ${{ matrix.workers }} run: pnpm run check:ci - name: Run blocking Windows builds concurrently if: matrix.platform == 'windows' shell: pwsh env: DSH_GATE_CONCURRENCY: ${{ matrix.workers }} run: pnpm run check:ci:windows-blocking