From e389900f7d9834b36fd2f6f7bd698798005289d2 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Mon, 10 Aug 2026 17:56:36 +0800 Subject: [PATCH 01/10] ci: run real-windows check on self-hosted runners Enable serial-windows to execute the complete check:ci on the in-house self-hosted Windows pool instead of the disabled hosted windows-2025 lane. Point the job at [self-hosted, dsh-win-ci, windows] and wire it into all-checks-passed so it gates pull requests. The Wine-emulated windows job remains the required win32 signal for PRs that cannot reach the self-hosted pool; this lane adds a real native-Windows run where one is available. --- .github/workflows/ci.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eebdafaa48..26a3d5be38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -622,9 +622,13 @@ jobs: run: pnpm run check:ci serial-windows: - if: false + # 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 - runs-on: windows-2025 + runs-on: [self-hosted, dsh-win-ci, windows] steps: - uses: actions/checkout@v6 @@ -885,7 +889,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] + needs: [node-24, node-24-coverage, node-24-consumers, node-compat, python-sdk, windows, serial-windows] if: always() && github.event_name == 'pull_request' steps: - name: Fail if any needed job did not succeed From 2a9f56b0b783b5f90a4da27accbcd402eba315fb Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Mon, 10 Aug 2026 21:31:06 +0800 Subject: [PATCH 02/10] ci: retrigger windows self-hosted validation From 9d8c70ce58170efade17e3078de87f3cd6b4ab5f Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Mon, 10 Aug 2026 21:40:19 +0800 Subject: [PATCH 03/10] ci: fix mirror sync lock, retrigger validation From 72bdfeea608a2a1276d320136314b24aedef376a Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Mon, 10 Aug 2026 21:45:59 +0800 Subject: [PATCH 04/10] ci: unconditional mirror fetch in job hook From 5d8d79ce929dec262af124a819e68b9e99a6466a Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Mon, 10 Aug 2026 22:30:46 +0800 Subject: [PATCH 05/10] 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. --- .github/AGENTS.md | 2 +- .github/workflows/ci.yml | 177 ++++++++++----------------------------- package.json | 1 - 3 files changed, 46 insertions(+), 134 deletions(-) diff --git a/.github/AGENTS.md b/.github/AGENTS.md index c18f5b5948..b2721fb84e 100644 --- a/.github/AGENTS.md +++ b/.github/AGENTS.md @@ -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. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26a3d5be38..8a0482e103 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/package.json b/package.json index d417354739..9c4d98f0c3 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "check:ci:windows-blocking": "tsx scripts/run-gates.ts ci-windows-blocking", "check:ci:windows-complete": "tsx scripts/run-gates.ts ci-windows-complete", "check:ci:windows-observational": "tsx scripts/run-gates.ts ci-windows-observational", - "check:windows-wine": "bash scripts/wine-windows-gates.sh", "check:node-compat": "tsx scripts/run-gates.ts node-compat", "knip": "knip --treat-config-hints-as-errors", "publint": "tsx scripts/publint-all.ts", From 0a2c0a92acca59b772b06133b1594fbc51e83aed Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Mon, 10 Aug 2026 22:59:14 +0800 Subject: [PATCH 06/10] ci: address review feedback for Windows CI unification - Rewrite ci-workflow.spec.ts to assert new single-job native Windows topology - Remove check:windows-wine from root AGENTS.md command list - Archive 2026-08-08-native-windows-pull-request-ci (dual-lane topology superseded) with Archived: metadata and inbound-link updates - Update cross-references in serial-cross-platform-ci-reference, portable-required-pull-request-ci, and pnpm-action-setup notes - Add Windows pool coverage to the failover runbook (EN + zh) - Remove dead wine-windows-gates.sh - Remove unnecessary Playwright Chromium install from serial-windows - Fix .github/AGENTS.md windows-* label wording - Fix PR description before-state accuracy --- ...8-native-windows-pull-request-ci.i18n.yaml | 6 + ...26-08-08-native-windows-pull-request-ci.md | 1 + ...08-08-native-windows-pull-request-ci.zh.md | 1 + ...rial-cross-platform-ci-reference.i18n.yaml | 4 +- ...7-21-serial-cross-platform-ci-reference.md | 4 +- ...1-serial-cross-platform-ci-reference.zh.md | 4 +- ...ortable-required-pull-request-ci.i18n.yaml | 4 +- ...07-23-portable-required-pull-request-ci.md | 6 +- ...23-portable-required-pull-request-ci.zh.md | 6 +- .../2026-07-26-ci-failover-runbook.i18n.yaml | 4 +- .../process/2026-07-26-ci-failover-runbook.md | 8 +- .../2026-07-26-ci-failover-runbook.zh.md | 8 +- ...n-setup-for-symmetric-ci-caching.i18n.yaml | 4 +- ...m-action-setup-for-symmetric-ci-caching.md | 4 +- ...ction-setup-for-symmetric-ci-caching.zh.md | 4 +- ...8-native-windows-pull-request-ci.i18n.yaml | 6 - .github/AGENTS.md | 2 +- .github/workflows/ci.yml | 7 - AGENTS.md | 1 - scripts/ci-workflow.spec.ts | 56 ++-- scripts/wine-windows-gates.sh | 282 ------------------ 21 files changed, 78 insertions(+), 344 deletions(-) create mode 100644 .agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml rename .agents/notes/{implemented => archived}/process/2026-08-08-native-windows-pull-request-ci.md (99%) rename .agents/notes/{implemented => archived}/process/2026-08-08-native-windows-pull-request-ci.zh.md (99%) delete mode 100644 .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml delete mode 100755 scripts/wine-windows-gates.sh diff --git a/.agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml b/.agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml new file mode 100644 index 0000000000..0a565b17d1 --- /dev/null +++ b/.agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.md +2026-08-08-native-windows-pull-request-ci.md: 3f69f335f6d873124fbbc1fd96d06ff2b3c8dc43 +2026-08-08-native-windows-pull-request-ci.zh.md: 136a7e8791da141cc12ba77d148f47b28db4790b diff --git a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md b/.agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.md similarity index 99% rename from .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md rename to .agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.md index 6a62fddb79..3f69f335f6 100644 --- a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md +++ b/.agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.md @@ -1,6 +1,7 @@ # Agent Note: Dual Wine and native Windows pull-request CI Status: implemented +Archived: 2026-08-10 English | [中文](2026-08-08-native-windows-pull-request-ci.zh.md) diff --git a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md b/.agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.zh.md similarity index 99% rename from .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md rename to .agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.zh.md index 990b8ed143..136a7e8791 100644 --- a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md +++ b/.agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.zh.md @@ -1,6 +1,7 @@ # Agent Note: Wine 与原生 Windows 双通道拉取请求 CI Status: implemented +Archived: 2026-08-10 [English](2026-08-08-native-windows-pull-request-ci.md) | 中文 diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml index 33bffe92ff..a65b22ba3f 100644 --- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md -2026-07-21-serial-cross-platform-ci-reference.md: 07dc430e6fed3fe75a006ca03523bd6e4fc969d0 -2026-07-21-serial-cross-platform-ci-reference.zh.md: 8bbb60cdead2957069de22ecaddf01c6cd9fb305 +2026-07-21-serial-cross-platform-ci-reference.md: b7b90f84095fe317f92c051f66da0bf8ee080b19 +2026-07-21-serial-cross-platform-ci-reference.zh.md: d62d7d754eb0343d8b1dd86adae7f501c04b177b diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md index 07dc430e6f..b7b90f8409 100644 --- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md +++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md @@ -16,7 +16,7 @@ Real-kernel sandbox proofs require specific hosted operating systems and archite ## Decision -[CI](../../../../.github/workflows/ci.yml) gives pull-request and master-push events complementary responsibilities. Pull requests run consolidated Linux and Wine-hosted Windows jobs plus the Node compatibility and Python contracts on standard GitHub-hosted capacity; an independent native Windows job reports the complete Windows inventory without participating in the required aggregate. On a push to `master`, the active reference is `serial / linux (self-hosted standby)` on the in-house `vm-backup` pool — the hot-standby drill that continuously re-proves the failover target described in the [failover runbook](2026-07-26-ci-failover-runbook.md). The standard-hosted `serial / linux`, `serial / macos`, and `serial / windows` definitions remain disabled under `TODO(hosted-serial-ci)` until their portable capacity can be restored. The separate job definitions intentionally keep their short checkout, runtime setup, and immutable install sequences visible instead of hiding operating systems behind a matrix or reusable workflow. `workflow_dispatch` is reserved for runner benchmarks. +[CI](../../../../.github/workflows/ci.yml) gives pull-request and master-push events complementary responsibilities. Pull requests run consolidated Linux and native Windows jobs plus the Node compatibility and Python contracts. On a push to `master`, the active references are `serial / linux (self-hosted standby)` and `serial / windows (self-hosted standby)` on the in-house self-hosted pools — the hot-standby drills that continuously re-prove the failover targets described in the [failover runbook](2026-07-26-ci-failover-runbook.md). The standard-hosted `serial / linux`, `serial / macos`, and `serial / windows` definitions remain disabled under `TODO(hosted-serial-ci)` until their portable capacity can be restored. The separate job definitions intentionally keep their short checkout, runtime setup, and immutable install sequences visible instead of hiding operating systems behind a matrix or reusable workflow. `workflow_dispatch` is reserved for runner benchmarks. Each reference job runs `pnpm run check:ci` without any shard selector. `DSH_GATE_CONCURRENCY=1` makes the top-level aggregate execute one ready gate at a time; coverage, snapshot replay, built-bin smoke, and publication validation also receive worker counts of one. The reference jobs may run beside one another, but each host's repository gates are serial and complete. Linux installs bubblewrap before replaying snapshots, and Windows enables Developer Mode before installing the symlinked workspace. @@ -28,7 +28,7 @@ The standalone [Sandbox](../../../../.github/workflows/sandbox.yml) workflow bel Master reference jobs are diagnostic and do not participate in the pull request's required `all checks passed` result. The CI and Sandbox workflows keep their cross-platform references on master pushes. Performance is evaluated from completed hosted-job timestamps and reported as a measurement; it is not encoded as a `timeout-minutes` value. -The portable reference uses GitHub's standard `ubuntu-latest`, `macos-latest`, and `windows-2025` labels. The required pull-request Windows job runs under Wine on `ubuntu-latest`, while the independent pull-request native job uses standard `windows-2025` under the [dual Windows decision](2026-08-08-native-windows-pull-request-ci.md); when enabled, `serial / windows` remains a second complete, unsharded native-kernel oracle. Required pull-request jobs use portable standard capacity under the [required-CI decision](2026-07-23-portable-required-pull-request-ci.md). Higher-core hosted runners remain manual benchmarks because a correctness path must remain runnable without repository-external runner configuration. +The portable reference uses GitHub's standard `ubuntu-latest`, `macos-latest`, and `windows-2025` labels. The required pull-request Windows job runs native on the enterprise larger runner under normal operation, failing over to the self-hosted pool under `DSH_CI_FAILOVER=selfhosted`; the archived [dual Windows decision](../archived/process/2026-08-08-native-windows-pull-request-ci.md) records the prior Wine/native topology. Required pull-request jobs use portable standard capacity under the [required-CI decision](2026-07-23-portable-required-pull-request-ci.md). Higher-core hosted runners remain manual benchmarks because a correctness path must remain runnable without repository-external runner configuration. ## Alternatives considered diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md index 8bbb60cdea..d62d7d754e 100644 --- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md +++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md @@ -16,7 +16,7 @@ Status: implemented ## 决策 -[CI](../../../../.github/workflows/ci.yml) 为拉取请求事件与 master 推送事件赋予互补的职责。拉取请求在 GitHub 标准托管容量上运行合并后的 Linux 和由 Wine 承载的 Windows 作业,以及 Node 兼容性与 Python 约定;一个独立的原生 Windows 作业会报告完整的 Windows 清单,但不参与必需聚合流程。向 `master` 推送时,当前启用的参考作业是公司自有 `vm-backup` 池上的 `serial / linux (self-hosted standby)`——该热备演练持续验证[故障切换手册](2026-07-26-ci-failover-runbook.md)所描述的切换目标。标准托管的 `serial / linux`、`serial / macos` 和 `serial / windows` 定义仍处于禁用状态,并由 `TODO(hosted-serial-ci)` 标记,直到其可移植容量恢复。各自独立的作业定义有意显式保留简短的代码检出、运行时设置和依赖锁定的安装步骤,而不是用矩阵或可复用工作流隐藏操作系统差异。`workflow_dispatch` 仅用于运行器基准测试。 +[CI](../../../../.github/workflows/ci.yml) 为拉取请求事件与 master 推送事件赋予互补的职责。拉取请求运行合并后的 Linux 与原生 Windows 作业,以及 Node 兼容性与 Python 约定。向 `master` 推送时,当前启用的参考作业是公司自有自托管池上的 `serial / linux (self-hosted standby)` 与 `serial / windows (self-hosted standby)`——这些热备演练持续验证[故障切换手册](2026-07-26-ci-failover-runbook.md)所描述的切换目标。标准托管的 `serial / linux`、`serial / macos` 和 `serial / windows` 定义仍处于禁用状态,并由 `TODO(hosted-serial-ci)` 标记,直到其可移植容量恢复。各自独立的作业定义有意显式保留简短的代码检出、运行时设置和依赖锁定的安装步骤,而不是用矩阵或可复用工作流隐藏操作系统差异。`workflow_dispatch` 仅用于运行器基准测试。 每个参考作业均在不设置任何分片选择器的情况下运行 `pnpm run check:ci`。`DSH_GATE_CONCURRENCY=1` 使顶层聚合每次只执行一个已经就绪的门禁;覆盖率、快照回放、built-bin 冒烟测试和发布验证的 worker 数量也设为 1。各参考作业可以彼此并行,但每台主机上的仓库门禁都串行运行且完整执行。Linux 在回放快照前安装 bubblewrap,Windows 则在安装采用符号链接的工作区前启用开发人员模式。 @@ -28,7 +28,7 @@ macOS 参考流程使用 fork 进程运行常规 Vitest 项目。macOS arm64 上 master 分支的参考作业仅用于诊断,不参与拉取请求所要求的 `all checks passed` 结果。CI 与 Sandbox 工作流把跨平台参考流程保留在 master 推送上。系统根据已完成托管作业的时间戳评估性能,并将其报告为测量结果,而不是写成 `timeout-minutes` 值。 -可移植的参考流程使用 GitHub 标准的 `ubuntu-latest`、`macos-latest` 和 `windows-2025` 标签。拉取请求必需的 Windows 作业在 `ubuntu-latest` 上通过 Wine 运行,而独立的拉取请求原生作业依据[双 Windows 决策](2026-08-08-native-windows-pull-request-ci.md)使用标准 `windows-2025`;`serial / windows` 启用时,仍作为第二个完整且未分片的原生内核标尺。依据[必需 CI 决策](2026-07-23-portable-required-pull-request-ci.md),拉取请求必需作业使用可移植的标准容量。更高核心数的托管运行器仍仅用于手动基准测试,因为正确性路径必须无需仓库外部的运行器配置即可运行。 +可移植的参考流程使用 GitHub 标准的 `ubuntu-latest`、`macos-latest` 和 `windows-2025` 标签。拉取请求必需的 Windows 作业在正常运行下使用企业级大型运行器,在 `DSH_CI_FAILOVER=selfhosted` 时切换至自托管池;已归档的[双 Windows 决策](../archived/process/2026-08-08-native-windows-pull-request-ci.md)记录此前的 Wine/原生拓扑。依据[必需 CI 决策](2026-07-23-portable-required-pull-request-ci.md),拉取请求必需作业使用可移植的标准容量。更高核心数的托管运行器仍仅用于手动基准测试,因为正确性路径必须无需仓库外部的运行器配置即可运行。 ## 曾考虑的替代方案 diff --git a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.i18n.yaml b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.i18n.yaml index e67565f381..4738f87fda 100644 --- a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md -2026-07-23-portable-required-pull-request-ci.md: b7c637408afa32611c4e70073ddec9aaabfba7f5 -2026-07-23-portable-required-pull-request-ci.zh.md: ee88412d982da5de613d8dfdbdcdd9977c899d2d +2026-07-23-portable-required-pull-request-ci.md: 6f7a0e0e6354e9bea632d1b00881a44acd921f9e +2026-07-23-portable-required-pull-request-ci.zh.md: 0ec92f11efa55ed155ada1ba858cd1878398f1ec diff --git a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md index b7c637408a..6f7a0e0e63 100644 --- a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md +++ b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md @@ -12,9 +12,9 @@ Billing health, a runner definition's `Ready` state, and a large autoscaling cei ## Decision -[CI](../../../../.github/workflows/ci.yml) runs the required primary Node 24 jobs, plus the stable `all checks passed` aggregate, on repo-restricted enterprise 32-core pools. The aggregate performs no checkout or repository gate, but sharing the enterprise pool prevents the required verdict from introducing a separate standard-hosted billing dependency after its substantive jobs have already succeeded. The required Windows job runs Windows Node under Wine on standard `ubuntu-latest` for the blocking surfaces; an independent native `windows-2025` job starts automatically but does not participate in the aggregate ([dual Windows decision](2026-08-08-native-windows-pull-request-ci.md)). Standard `ubuntu-latest` jobs retain Node 22.19, Node 26, and Python SDK compatibility, and the serial references remain the complete unsharded cross-platform definitions. Those standard-hosted jobs keep the portable execution boundary observable without duplicating the primary inventory on every pull request. +[CI](../../../../.github/workflows/ci.yml) runs the required primary Node 24 jobs, plus the stable `all checks passed` aggregate, on repo-restricted enterprise 32-core pools. The aggregate performs no checkout or repository gate, but sharing the enterprise pool prevents the required verdict from introducing a separate standard-hosted billing dependency after its substantive jobs have already succeeded. The required Windows job runs native on the enterprise larger runner under normal operation, failing over to the self-hosted pool under `DSH_CI_FAILOVER=selfhosted` ([unified Windows CI note](2026-08-10-unified-windows-selfhosted-ci.md)). Standard `ubuntu-latest` jobs retain Node 22.19, Node 26, and Python SDK compatibility, and the serial cross-platform standbys continuously validate the failover targets. Those standard-hosted jobs keep the portable execution boundary observable without duplicating the primary inventory on every pull request. -The two Linux primary jobs, Node compatibility, Python SDK, and `windows node 24 / wine blocking` remain dependencies of `all checks passed`; `windows node 24 / native complete` is deliberately absent. Branch protection continues to require `e2e` and `all checks passed`. There is no automatic fallback when a remaining enterprise Linux label cannot allocate: the standard jobs continue to report their own contracts, but they cannot manufacture the missing required result. +The two Linux primary jobs, Node compatibility, Python SDK, and `windows node 24 / native complete` remain dependencies of `all checks passed`. The master-only `serial / windows (self-hosted standby)` is deliberately absent from the aggregate, mirroring the Linux standby pattern. The [larger-runner decision](2026-07-22-evidence-based-larger-hosted-runners.md) owns the current primary topology and its measurements. The [serial cross-platform reference](2026-07-21-serial-cross-platform-ci-reference.md) remains the independent standard-hosted completeness check, and the manual larger-runner suites retain size comparisons without expanding the ordinary required matrix. @@ -30,6 +30,6 @@ The [larger-runner decision](2026-07-22-evidence-based-larger-hosted-runners.md) ## Consequences -Ordinary pull requests spend enterprise capacity on the Linux critical path while the Wine job keeps the required Windows verdict on standard Linux allocation. The independent native job uses standard Windows allocation without delaying or changing the aggregate. A live exact-head run distinguishes the commands branch protection consumes from the separate diagnostic contract; queue delay is reported separately from each job's `startedAt` to `completedAt` execution interval. +Ordinary pull requests spend enterprise capacity on the Linux critical path while the native Windows job uses the enterprise larger runner on the normal path and the self-hosted pool under failover. A live exact-head run distinguishes the commands branch protection consumes from the separate diagnostic contract; queue delay is reported separately from each job's `startedAt` to `completedAt` execution interval. Standard compatibility, required Wine, and diagnostic native Windows jobs remain useful when enterprise allocation is degraded, but they do not make a blocked required Linux job or aggregate green. Recovering Linux availability may require restoring the complete standard-hosted topology; changing a pool definition's status alone is insufficient evidence that it can receive work. diff --git a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.zh.md b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.zh.md index ee88412d98..0ec92f11ef 100644 --- a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.zh.md +++ b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.zh.md @@ -12,9 +12,9 @@ Status: implemented ## 决策 -[CI](../../../../.github/workflows/ci.yml) 在仅限本仓库使用的企业级 32 核运行器池上运行必需的主 Node 24 作业,以及稳定的 `all checks passed` 聚合流程。该聚合流程不执行代码检出或仓库门禁;但让它与所依赖的实质性作业共用企业级运行器池,可以避免这些作业已经成功后,必需判定结果又引入一项单独的标准托管计费依赖。必需的 Windows 作业在标准 `ubuntu-latest` 上通过 Wine 运行 Windows Node,覆盖阻断性检查范围;一个独立的原生 `windows-2025` 作业会自动启动,但不参与聚合流程([双 Windows 决策](2026-08-08-native-windows-pull-request-ci.md))。标准 `ubuntu-latest` 作业保留 Node 22.19、Node 26 和 Python SDK 兼容性,串行参考流程仍是完整且未分片的跨平台定义。这些标准托管作业让可移植执行边界保持可观测,而不必在每个拉取请求中重复主清单。 +[CI](../../../../.github/workflows/ci.yml) 在仅限本仓库使用的企业级 32 核运行器池上运行必需的主 Node 24 作业,以及稳定的 `all checks passed` 聚合流程。该聚合流程不执行代码检出或仓库门禁;但让它与所依赖的实质性作业共用企业级运行器池,可以避免这些作业已经成功后,必需判定结果又引入一项单独的标准托管计费依赖。必需的 Windows 作业在正常运行下使用企业级大型运行器,在 `DSH_CI_FAILOVER=selfhosted` 时切换至自托管池([统一 Windows CI 笔记](2026-08-10-unified-windows-selfhosted-ci.md))。标准 `ubuntu-latest` 作业保留 Node 22.19、Node 26 和 Python SDK 兼容性,跨平台串行备援持续验证故障切换目标。这些标准托管作业让可移植执行边界保持可观测,而不必在每个拉取请求中重复主清单。 -两项 Linux 主作业、Node 兼容性、Python SDK 和 `windows node 24 / wine blocking` 继续作为 `all checks passed` 的依赖项;`windows node 24 / native complete` 被刻意排除。分支保护继续要求 `e2e` 和 `all checks passed`。剩余的企业级 Linux 运行器标签无法分配运行器时没有自动后备机制:标准作业会继续报告各自的约定,但无法产出缺失的必需结果。 +两项 Linux 主作业、Node 兼容性、Python SDK 和 `windows node 24 / native complete` 继续作为 `all checks passed` 的依赖项;仅 master 触发的 `serial / windows (self-hosted standby)` 被刻意排除,与 Linux 备援模式对称。 当前主拓扑及其测量结果由[大型运行器决策](2026-07-22-evidence-based-larger-hosted-runners.md)记录。[跨平台串行参考流程](2026-07-21-serial-cross-platform-ci-reference.md)继续作为独立的标准托管完整性检查,手动大型运行器套件则保留规格比较,同时不扩大普通必需矩阵。 @@ -30,6 +30,6 @@ Status: implemented ## 后果 -普通拉取请求会将企业级运行器容量用于 Linux 关键路径,而 Wine 作业让必需的 Windows 判定继续使用标准 Linux 运行器容量。独立原生作业使用标准 Windows 运行器容量,不会延迟或改变聚合流程。一次实际的分支头精确运行会区分分支保护采用的命令与单独的诊断约定;排队延迟与每个作业从 `startedAt` 到 `completedAt` 的执行区间分开报告。 +普通拉取请求会将企业级运行器容量用于 Linux 关键路径,而原生 Windows 作业在正常运行下使用企业级大型运行器、故障切换时使用自托管池。独立原生作业使用标准 Windows 运行器容量,不会延迟或改变聚合流程。一次实际的分支头精确运行会区分分支保护采用的命令与单独的诊断约定;排队延迟与每个作业从 `startedAt` 到 `completedAt` 的执行区间分开报告。 企业级运行器分配能力下降时,标准兼容性作业、必需的 Wine 作业与诊断性原生 Windows 作业仍能提供有用证据,但无法让受阻的必需 Linux 作业或聚合流程变绿。恢复 Linux 可用性时,可能需要恢复完整的标准托管拓扑;仅改变运行器池定义的状态,不足以证明它可以接收作业。 diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml index f4fe0c74d2..2403d869c6 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md -2026-07-26-ci-failover-runbook.md: 72261f95ea74b61e3915a1a6419b2c2e616efbd9 -2026-07-26-ci-failover-runbook.zh.md: 1ee679eabc296ab31d71b87945409788539425bd +2026-07-26-ci-failover-runbook.md: 25bfb8cc7ca6d53c5d28e7c941facdb86e5e147b +2026-07-26-ci-failover-runbook.zh.md: 22195500f5a28c9e92bc8aac8c58028628ab1d6b diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md index 72261f95ea..25bfb8cc7c 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md @@ -6,16 +6,20 @@ English | [中文](2026-07-26-ci-failover-runbook.zh.md) ## Problem -The three required Linux worker jobs in [CI](../../../../.github/workflows/ci.yml) (`node 24 / static`, `node 24 / coverage`, `node 24 / snapshots and artifacts`) run on the hosted enterprise 32-core pools; the required verdict job that aggregates them (`all checks passed`) runs on standard `ubuntu-latest`. When the enterprise pools degrade — jobs queue indefinitely or the enterprise labels vanish — every open pull request becomes unmergeable, and the ordinary recovery of merging a fix is itself deadlocked behind the very required checks that cannot run. **Scope: this switch recovers an enterprise Linux-pool outage.** The verdict's other required dependencies (`node-compat`, `python-sdk`, `windows`) stay on standard hosted runners by design (the portable boundary); in a broader GitHub-hosted capacity failure that also takes out the standard pools, those dependencies still block `all checks passed`, and only the Windows leg has no in-house substitute at all — during the 2026-07-27 outage the standard pools recovered first, which is the ordering this design bets on. An outage therefore needs a switch any responder with repository write access can throw without merging anything. +The three required Linux worker jobs in [CI](../../../../.github/workflows/ci.yml) (`node 24 / static`, `node 24 / coverage`, `node 24 / snapshots and artifacts`) run on the hosted enterprise 32-core pools; the required verdict job that aggregates them (`all checks passed`) runs on standard `ubuntu-latest`. The required Windows job (`windows node 24 / native complete`) runs on the hosted enterprise larger runner. When the enterprise pools degrade — jobs queue indefinitely or the enterprise labels vanish — every open pull request becomes unmergeable, and the ordinary recovery of merging a fix is itself deadlocked behind the very required checks that cannot run. **Scope: this switch recovers an enterprise Linux-pool outage AND a hosted Windows-pool outage.** The verdict's other required dependencies (`node-compat`, `python-sdk`) stay on standard hosted runners by design (the portable boundary); in a broader GitHub-hosted capacity failure that also takes out the standard pools, those dependencies still block `all checks passed`. An outage therefore needs a switch any responder with repository write access can throw without merging anything. ## Decision -Each of the three required Linux worker jobs — and the `all checks passed` verdict job, which would otherwise stay queued on the failed pool even after every worker passed — resolves its runner pool through the `DSH_CI_FAILOVER` repository variable. Unset (normal), they run on the hosted enterprise pools. Set to `selfhosted` by any repository writer, all four retarget onto the in-house self-hosted `vm-backup` pool, coverage and snapshot concurrency drop to shared-VM bounds, and the hosted-path pnpm cache restores are skipped. The switch is writer-manageable repository state, not a merge, so it works while every check is red. The in-house pool's readiness is continuously re-proven by the `serial / linux (self-hosted standby)` lane, which runs the complete unsharded aggregate on every master push. +Each of the three required Linux worker jobs, the required Windows job, and the `all checks passed` verdict job — which would otherwise stay queued on the failed pool even after every worker passed — resolves its runner pool through the `DSH_CI_FAILOVER` repository variable. Unset (normal), they run on the hosted enterprise pools. Set to `selfhosted` by any repository writer, all five retarget onto the in-house self-hosted pools: the Linux jobs and verdict onto the `vm-backup` pool (`[self-hosted, linux, x64, vm-backup]`), coverage and snapshot concurrency drop to shared-VM bounds, and the hosted-path pnpm cache restores are skipped; the Windows job onto the `dsh-win-ci` pool (`[self-hosted, dsh-win-ci, windows]`). The switch is writer-manageable repository state, not a merge, so it works while every check is red. The in-house pools' readiness is continuously re-proven by the `serial / linux (self-hosted standby)` and `serial / windows (self-hosted standby)` lanes, which run the complete unsharded aggregates on every master push. ### What the in-house pool is `vm-backup`: one 64-core VM, six always-on systemd-managed runner instances. Its image must preinstall Playwright Chromium's Linux system packages; CI downloads the lockfile-selected browser but never runs `apt` on this persistent shared host. Check the latest `serial / linux (self-hosted standby)` run before switching: its aggregate includes browser replay, so a green standby verifies both ordinary capacity and this browser prerequisite. +#### Windows pool + +`dsh-win-ci`: 16 always-on runner instances on the in-house Windows CI server. Labels: `[self-hosted, dsh-win-ci, windows]`. The image must preinstall Node 24, pnpm, Git, and enable Developer Mode for symlink support; Playwright Chromium is installed per-run by the `serial / windows (self-hosted standby)` lane. Check the latest `serial / windows (self-hosted standby)` run before switching: a green standby verifies the pool can execute `check:ci:windows-complete` end-to-end. + ### Switch (any repository writer, ~1 minute, no merge) 1. Repository **Settings → Secrets and variables → Actions → Variables → New repository variable**: name `DSH_CI_FAILOVER`, value `selfhosted`. diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md index 1ee679eabc..22195500f5 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md @@ -6,16 +6,20 @@ Status: implemented ## 问题 -[CI](../../../../.github/workflows/ci.yml) 中三个必需的 Linux 工作作业(`node 24 / static`、`node 24 / coverage`、`node 24 / snapshots and artifacts`)运行在托管的企业级 32 核池上;聚合它们的必需判定作业(`all checks passed`)运行在标准 `ubuntu-latest` 上。当企业池发生故障——作业无限排队或企业标签消失——所有开启的拉取请求都无法合并,而"合并一个修复"这一常规恢复手段本身正被那些无法运行的必需检查死锁。**适用范围:本切换恢复的是企业级 Linux 池故障。**判定作业的其余必需依赖(`node-compat`、`python-sdk`、`windows`)按设计留在标准托管运行器上(可移植边界);若更大范围的 GitHub 托管容量故障连标准池一并击倒,这些依赖仍会阻塞 `all checks passed`,且只有 Windows 这条腿完全没有自有替代——2026-07-27 的故障中标准池率先恢复,本设计押注的正是这一顺序。因此故障需要一个任何具备仓库写权限的响应者都能在不合并任何代码的情况下触发的开关。 +[CI](../../../../.github/workflows/ci.yml) 中三个必需的 Linux 工作作业(`node 24 / static`、`node 24 / coverage`、`node 24 / snapshots and artifacts`)运行在托管的企业级 32 核池上;聚合它们的必需判定作业(`all checks passed`)运行在标准 `ubuntu-latest` 上。必需的 Windows 作业(`windows node 24 / native complete`)运行在托管的企业级大型运行器上。当企业池发生故障——作业无限排队或企业标签消失——所有开启的拉取请求都无法合并,而"合并一个修复"这一常规恢复手段本身正被那些无法运行的必需检查死锁。**适用范围:本切换恢复的是企业级 Linux 池故障和托管 Windows 池故障。**判定作业的其余必需依赖(`node-compat`、`python-sdk`)按设计留在标准托管运行器上(可移植边界);若更大范围的 GitHub 托管容量故障连标准池一并击倒,这些依赖仍会阻塞 `all checks passed`。因此故障需要一个任何具备仓库写权限的响应者都能在不合并任何代码的情况下触发的开关。 ## 决策 -三个必需的 Linux 工作作业——以及 `all checks passed` 判定作业(若不随切换,即使全部工作作业通过,它仍会滞留在故障池的队列中)——各自通过仓库变量 `DSH_CI_FAILOVER` 解析运行器池。变量不存在(正常)时它们运行在托管企业池上;由任何具备写权限的协作者设为 `selfhosted` 时,四者全部切换到公司自有的自托管 `vm-backup` 池,覆盖率与快照的并发降到共享虚拟机上限,并跳过托管路径的 pnpm 缓存恢复。这个开关是写者可管理的仓库状态而非一次合并,因此在所有检查都是红色时仍然有效。自有池的就绪状态由 `serial / linux (self-hosted standby)` 通道持续验证——每次 master 推送都在其上运行完整的未分片聚合流程。 +三个必需的 Linux 工作作业、必需的 Windows 作业,以及 `all checks passed` 判定作业(若不随切换,即使全部工作作业通过,它仍会滞留在故障池的队列中)——各自通过仓库变量 `DSH_CI_FAILOVER` 解析运行器池。变量不存在(正常)时它们运行在托管企业池上;由任何具备写权限的协作者设为 `selfhosted` 时,五者全部切换到公司自有的自托管池:Linux 作业与判定位移入 `vm-backup` 池(`[self-hosted, linux, x64, vm-backup]`),覆盖率与快照的并发降到共享虚拟机上限,并跳过托管路径的 pnpm 缓存恢复;Windows 作业移入 `dsh-win-ci` 池(`[self-hosted, dsh-win-ci, windows]`)。这个开关是写者可管理的仓库状态而非一次合并,因此在所有检查都是红色时仍然有效。自有池的就绪状态由 `serial / linux (self-hosted standby)` 和 `serial / windows (self-hosted standby)` 通道持续验证——每次 master 推送都在其上运行完整的未分片聚合流程。 ### 自有池是什么 `vm-backup`:一台 64 核虚拟机,6 个常驻 systemd 管理的运行器实例。其镜像必须预装 Playwright Chromium 的 Linux 系统软件包;CI 会下载锁文件选定的浏览器,但绝不在这台持久化共享主机上运行 `apt`。切换前先看 `serial / linux (self-hosted standby)` 最近一次运行:其聚合流程包含浏览器回放,因此绿色热备同时验证常规容量和这项浏览器先决条件。 +#### Windows 池 + +`dsh-win-ci`:公司自有 Windows CI 服务器上的 16 个常驻运行器实例。标签:`[self-hosted, dsh-win-ci, windows]`。镜像必须预装 Node 24、pnpm、Git,并启用开发人员模式以支持符号链接;Playwright Chromium 由 `serial / windows (self-hosted standby)` 通道在每次运行时安装。切换前先看 `serial / windows (self-hosted standby)` 最近一次运行:绿色热备验证该池可完整执行 `check:ci:windows-complete`。 + ### 切换步骤(任何具备写权限的协作者,约 1 分钟,无需合并) 1. 仓库 **Settings → Secrets and variables → Actions → Variables → New repository variable**:名称 `DSH_CI_FAILOVER`,值 `selfhosted`。 diff --git a/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.i18n.yaml b/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.i18n.yaml index c0a7dcfc02..b619bdc1d8 100644 --- a/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.md -2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.md: 499141ca6a3703a12d10c195b732441d49419599 -2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.zh.md: c57b7d32cd7204a845ab1884e9ae0b9045749a1c +2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.md: 612c42d19e3891f3fa036fea74b36e36da2fb5cd +2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.zh.md: 7c82c787f11e93cbb62209d25df741d0abeba3dd diff --git a/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.md b/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.md index 499141ca6a..612c42d19e 100644 --- a/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.md +++ b/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.md @@ -13,8 +13,8 @@ Outside `landlock-run.yml`, each workflow that installed pnpm hand-provisioned i `pnpm/action-setup@v4` is the only pnpm provisioning mechanism in CI: no workflow runs `corepack enable`. The root dev dependency on `@yarnpkg/cli-dist` separately supplies the modern Yarn CLI exercised by the generated-project e2e; package-manager coverage therefore does not inherit the runner image's Yarn Classic. Caching remains per-job policy on top of pnpm provisioning, in three deliberate shapes: - **Symmetric cache** (restore and save): `actions/setup-node` with `cache: pnpm` — `e2e.yml`, `docs-pages.yml`, `pi-ai-provider-e2e.yml`, `build-exe-for-python-sdk.yml`, and the node-compat and two benchmark jobs of `ci.yml`. The larger-runner benchmark keeps its store cache Linux-only through a conditional `cache:` input; the consolidated benchmark caches on both platforms. -- **Restore-only / producer pairing** (hand-rolled `actions/cache` steps): the three enterprise-runner PR jobs and the Wine-based required Windows job restore without saving, keeping cache compression/upload off their latency-sensitive paths — an asymmetry `setup-node`'s cache cannot express. Each configures a store outside the action's replaceable install directory and resolves that path, matching the master-push serial-linux producer's path and exact key; the enterprise jobs skip restore during self-hosted failover because that VM's persistent store is already warm. -- **Cache-less or persistent** (no store-cache action): the independent native Windows job, native serial-windows and serial-macos, plus `sandbox.yml` install from a cold or runner-local store. Extracting the many-file pnpm store costs more than a clean Windows install; the self-hosted standby and failover jobs instead reuse their VM's persistent pnpm store without transferring a hosted cache archive. +- **Restore-only / producer pairing** (hand-rolled `actions/cache` steps): the three enterprise-runner PR jobs restore without saving, keeping cache compression/upload off their latency-sensitive paths — an asymmetry `setup-node`'s cache cannot express. Each configures a store outside the action's replaceable install directory and resolves that path, matching the master-push serial-linux producer's path and exact key; the enterprise jobs skip restore during self-hosted failover because that VM's persistent store is already warm. +- **Cache-less or persistent** (no store-cache action): the required native Windows job, serial-windows standby, serial-macos, plus `sandbox.yml` install from a cold or runner-local store. Extracting the many-file pnpm store costs more than a clean Windows install; the self-hosted standby and failover jobs instead reuse their VM's persistent pnpm store without transferring a hosted cache archive. ## Alternatives considered diff --git a/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.zh.md b/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.zh.md index c57b7d32cd..7c82c787f1 100644 --- a/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.zh.md +++ b/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.zh.md @@ -13,8 +13,8 @@ Status: implemented `pnpm/action-setup@v4` 是 CI 中提供 pnpm 的唯一机制:没有任何工作流运行 `corepack enable`。根目录的 `@yarnpkg/cli-dist` 开发依赖另行提供 generated-project e2e 所运行的现代 Yarn CLI(命令行界面);因此,用于包管理器覆盖率的 Yarn 不会沿用 runner 镜像里的 Yarn Classic。缓存仍是叠加在 pnpm 提供机制上的按作业政策,保持三种刻意的形态: - **对称缓存**(既恢复也保存):带 `cache: pnpm` 的 `actions/setup-node`——`e2e.yml`、`docs-pages.yml`、`pi-ai-provider-e2e.yml`、`build-exe-for-python-sdk.yml`,以及 `ci.yml` 的 node-compat 与两个 benchmark 作业。larger-runner benchmark 通过条件化的 `cache:` 输入让 store 缓存仅限 Linux;consolidated benchmark 在两个平台上都启用缓存。 -- **只恢复不上传/生产者配对**(手写的 `actions/cache` 步骤):企业 runner 上的三个 PR(Pull Request)作业和基于 Wine 的必需 Windows 作业只恢复不保存,把缓存压缩/上传挡在它们的延迟敏感路径之外——这种不对称是 `setup-node` 的缓存无法表达的。每个作业都在 action 可替换的安装目录之外配置 store,并解析该路径,从而与 master 推送触发的 serial-linux 生产者所用的路径和精确键匹配;企业作业在自托管故障切换期间跳过恢复,因为该 VM 的持久 store 已经预热。 -- **无缓存或持久化**(不使用 store 缓存 action):独立的原生 Windows 作业、原生 serial-windows 和 serial-macos,以及 `sandbox.yml` 均从冷 store 或 runner 本地 store 安装。解压含有大量文件的 pnpm store,成本高于在 Windows 上进行一次全新安装;自托管热备与故障切换作业则复用其 VM 的持久 pnpm store,不传输托管缓存归档。 +- **只恢复不上传/生产者配对**(手写的 `actions/cache` 步骤):企业 runner 上的三个 PR(Pull Request)作业只恢复不保存,把缓存压缩/上传挡在它们的延迟敏感路径之外——这种不对称是 `setup-node` 的缓存无法表达的。每个作业都在 action 可替换的安装目录之外配置 store,并解析该路径,从而与 master 推送触发的 serial-linux 生产者所用的路径和精确键匹配;企业作业在自托管故障切换期间跳过恢复,因为该 VM 的持久 store 已经预热。 +- **无缓存或持久化**(不使用 store 缓存 action):必需的 Windows 作业、serial-windows 热备、serial-macos,以及 `sandbox.yml` 均从冷 store 或 runner 本地 store 安装。解压含有大量文件的 pnpm store,成本高于在 Windows 上进行一次全新安装;自托管热备与故障切换作业则复用其 VM 的持久 pnpm store,不传输托管缓存归档。 ## 曾考虑的替代方案 diff --git a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml deleted file mode 100644 index 07eb13b5cd..0000000000 --- a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# side as of the last confirmed-consistent state. Both languages carry equal authority; -# after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md -2026-08-08-native-windows-pull-request-ci.md: 6a62fddb79670c3ab4cc0446796dbffd7130aed9 -2026-08-08-native-windows-pull-request-ci.zh.md: 990b8ed1434934337b8ff20c5f3be2c03cd9c61b diff --git a/.github/AGENTS.md b/.github/AGENTS.md index b2721fb84e..ad2aeac354 100644 --- a/.github/AGENTS.md +++ b/.github/AGENTS.md @@ -1,3 +1,3 @@ # AGENTS.md — GitHub Actions -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. +Run Windows CI jobs under native `pwsh`. The required pull-request `windows` job and the master `serial-windows` standby both run on real Windows: normally on the hosted enterprise runner, failing over to the self-hosted `[self-hosted, dsh-win-ci, windows]` pool under `DSH_CI_FAILOVER=selfhosted`. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a0482e103..f67a86c5fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -561,13 +561,6 @@ jobs: shell: pwsh run: pnpm install --frozen-lockfile - # 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: diff --git a/AGENTS.md b/AGENTS.md index a8a202147c..7b326fbdb5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,7 +69,6 @@ pnpm run typecheck pnpm run lint pnpm run duplication # cross-file TypeScript clone detection pnpm run build # tsc emits lib/types, tsdown bundles runtime -pnpm run check:windows-wine # ONLY when diagnosing a known Windows failure (needs wine); CI owns this signal pnpm run hygiene # knip + publint + workspace constraints + NodeNext consumer check pnpm run doc-sync # all documentation gates; leaf list in scripts/run-gates.ts pnpm run website:build # VitePress build (doubles as dead-link check) diff --git a/scripts/ci-workflow.spec.ts b/scripts/ci-workflow.spec.ts index 2269c7cbee..b6d7d858ff 100644 --- a/scripts/ci-workflow.spec.ts +++ b/scripts/ci-workflow.spec.ts @@ -27,46 +27,60 @@ describe('CI workflow', () => { } }) - it('keeps Wine blocking while native Windows reports independently', () => { + it('keeps a single required native Windows job with failover and a master-only standby', () => { const workflow = loadWorkflow('.github/workflows/ci.yml') if (!isRecord(workflow.jobs) || !isRecord(workflow.jobs.windows) - || !isRecord(workflow.jobs['windows-native']) + || !isRecord(workflow.jobs['serial-windows']) || !isRecord(workflow.jobs['all-checks-passed'])) { - throw new TypeError('CI workflow must define Wine, native Windows, and aggregate jobs') + throw new TypeError('CI workflow must define windows, serial-windows, and all-checks-passed jobs') } const windows = workflow.jobs.windows - const windowsNative = workflow.jobs['windows-native'] + const serialWindows = workflow.jobs['serial-windows'] const aggregate = workflow.jobs['all-checks-passed'] - if (!Array.isArray(windows.steps) || !Array.isArray(windowsNative.steps) || !Array.isArray(aggregate.needs)) { - throw new TypeError('Windows jobs must define steps and the aggregate must define needs') + if (!Array.isArray(windows.steps) || !Array.isArray(aggregate.needs)) { + throw new TypeError('Windows job must define steps and the aggregate must define needs') } - const nativeCommandSteps = windowsNative.steps.filter((step): step is Record & { run: string } => ( + const commandSteps = windows.steps.filter((step): step is Record & { run: string } => ( isRecord(step) && typeof step.run === 'string' )) - expect(windows['runs-on']).toBe('ubuntu-latest') - expect(windows.name).toBe('windows node 24 / wine blocking') + // Required PR job: native Windows, failover-able, runs windows-complete. + expect(typeof windows['runs-on']).toBe('string') + expect(windows['runs-on']).toContain('DSH_CI_FAILOVER') + expect(windows['runs-on']).toContain('self-hosted') + expect(windows['runs-on']).toContain('dsh-win-ci') + expect(windows['runs-on']).toContain('dsh-windows-2025-16core') + expect(windows.name).toBe('windows node 24 / native complete') expect(windows.if).toBe("github.event_name == 'pull_request'") - expect(JSON.stringify(windows)).toContain('bash scripts/wine-windows-gates.sh') - expect(workflow.jobs).toHaveProperty('wine-apt-cache') - expect(windowsNative['runs-on']).toBe('dsh-windows-2025-16core') - expect(windowsNative.name).toBe('windows node 24 / native complete') - expect(windowsNative['timeout-minutes']).toBe(60) - expect(windowsNative.if).toBe("github.event_name == 'pull_request'") - expect(windowsNative.env).toMatchObject({ + expect(windows.env).toMatchObject({ DSH_COVERAGE_MAX_WORKERS: '2', DSH_GATE_CONCURRENCY: '2', DSH_PUBLINT_CONCURRENCY: '8', }) - expect(windowsNative).not.toHaveProperty('continue-on-error') - expect(nativeCommandSteps).toHaveLength(3) - expect(nativeCommandSteps.every(step => step.shell === 'pwsh')).toBe(true) - expect(nativeCommandSteps.map(step => step.run)).toContain('pnpm run check:ci:windows-complete') - expect(JSON.stringify(windowsNative)).not.toMatch(/wine/i) + expect(commandSteps.every(step => step.shell === 'pwsh')).toBe(true) + expect(commandSteps.map(step => step.run)).toContain('pnpm run check:ci:windows-complete') + const developerMode = windows.steps.find((step): step is Record & { run: string } => ( + isRecord(step) && typeof step.run === 'string' && step.run.includes('AllowDevelopmentWithoutDevLicense') + )) + expect(developerMode).toBeDefined() + + // No Wine or dual-lane remnants. + const workflowJson = JSON.stringify(workflow) + expect(workflowJson).not.toMatch(/wine/i) + expect(workflow.jobs).not.toHaveProperty('windows-native') + expect(workflow.jobs).not.toHaveProperty('wine-apt-cache') + + // serial-windows: master-only standby, self-hosted, non-blocking. + expect(serialWindows.if).toBe("github.event_name == 'push' && github.ref == 'refs/heads/master'") + expect(serialWindows['runs-on']).toEqual(['self-hosted', 'dsh-win-ci', 'windows']) + expect(serialWindows.name).toBe('serial / windows (self-hosted standby)') + + // Aggregate: windows required, serial-windows excluded. expect(aggregate.needs).toContain('windows') expect(aggregate.needs).not.toContain('windows-native') + expect(aggregate.needs).not.toContain('serial-windows') }) it('keeps supported LSP source under native Windows coverage', () => { diff --git a/scripts/wine-windows-gates.sh b/scripts/wine-windows-gates.sh deleted file mode 100755 index 5e46b75f91..0000000000 --- a/scripts/wine-windows-gates.sh +++ /dev/null @@ -1,282 +0,0 @@ -#!/usr/bin/env bash -# Run the blocking Windows gates (workspace build, production site) with real -# win-x64 Node.js under Wine — the same script the pull-request `windows` job -# in ci.yml executes and the optional local gate `pnpm run check:windows-wine` -# wraps. Owning rationale and fidelity limits: -# .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md -# -# The working tree is never mutated: tracked plus untracked-unignored files -# are snapshotted into a scratch directory, the Wine-specific pnpm overrides -# (hoisted layout, win32-x64 platform packages) are appended to the SNAPSHOT's -# pnpm-workspace.yaml, and the install and gates run there against the shared -# pnpm store. The Wine prefix and the checksum-verified Windows Node zip -# persist in .cache/wine-windows/ so reruns skip provisioning. -# -# Environment: DSH_WINE_NODE_MAJOR (default $PRIMARY_NODE_VERSION, then 24) -# picks the Windows Node line; DSH_WINE_GATE_CACHE_DIR relocates the cache; -# DSH_WINE_GATE_KEEP=1 preserves the scratch tree for inspection. - -set -euo pipefail - -repo_root="$(git rev-parse --show-toplevel)" -node_major="${DSH_WINE_NODE_MAJOR:-${PRIMARY_NODE_VERSION:-24}}" -cache_dir="${DSH_WINE_GATE_CACHE_DIR:-$repo_root/.cache/wine-windows}" - -export WINEDEBUG='-all' -export WINEARCH=win64 -# Skip Wine Mono / Gecko installers: Node needs neither. -export WINEDLLOVERRIDES='mscoree,mshtml=' -export WINEPREFIX="$cache_dir/prefix" - -# ---- preflight: fail loud before any expensive work -------------------- -wine_bin='' -for candidate in "$(command -v wine || true)" "$(command -v wine64 || true)" /usr/lib/wine/wine64; do - if [ -n "$candidate" ] && [ -x "$candidate" ]; then wine_bin="$candidate"; break; fi -done -# GNU coreutils sha256sum on Linux; perl shasum ships with macOS. Both -# accept the same " " --check input. -checksum_tool='' -if command -v sha256sum > /dev/null; then - checksum_tool='sha256sum' -elif command -v shasum > /dev/null; then - checksum_tool='shasum' -fi -missing=() -[ -n "$wine_bin" ] || missing+=('wine (apt: wine | brew: wine-stable)') -command -v curl > /dev/null || missing+=('curl') -command -v unzip > /dev/null || missing+=('unzip') -[ -n "$checksum_tool" ] || missing+=('sha256sum or shasum (apt: coreutils | macOS ships shasum)') -if ! command -v pnpm > /dev/null; then corepack enable > /dev/null 2>&1 || true; fi -command -v pnpm > /dev/null || missing+=('pnpm (corepack enable)') -if (( ${#missing[@]} > 0 )); then - printf 'wine-windows-gates: missing required tool: %s\n' "${missing[@]}" >&2 - exit 1 -fi - -# Verify file $2 against SHA-256 hex $1 with whichever tool preflight found. -verify_sha256() { - case "$checksum_tool" in - sha256sum) printf '%s %s\n' "$1" "$2" | sha256sum --check - > /dev/null ;; - shasum) printf '%s %s\n' "$1" "$2" | shasum -a 256 --check - > /dev/null ;; - esac -} - -scratch="$(mktemp -d "${TMPDIR:-/tmp}/dsh-wine-gates.XXXXXX")" -cleanup() { - wineserver -k > /dev/null 2>&1 || true - if [ "${DSH_WINE_GATE_KEEP:-0}" = '1' ]; then - echo "wine-windows-gates: scratch tree kept at $scratch" - else - rm -rf "$scratch" - fi -} -trap cleanup EXIT -mkdir -p "$cache_dir" "$scratch/logs" - -# ---- provision Windows Node, boot Wine, snapshot + install concurrently ---- -curl_metadata_args=( - --fail --silent --show-error --location - --retry 3 --retry-all-errors --retry-delay 2 - --http1.1 --connect-timeout 10 --max-time 30 --retry-max-time 120 -) - -download_node_archive() { - local version="$1" output="$2" attempt status=0 - local archive="node-$version-win-x64.zip" - local primary_url="https://nodejs.org/dist/$version/$archive" - local mirror_url="https://npmmirror.com/mirrors/node/$version/$archive" - - if curl --fail --silent --show-error --location --http1.1 \ - --connect-timeout 10 --max-time 300 --speed-limit 1024 --speed-time 30 \ - -o "$output" "$primary_url"; then - return 0 - fi - echo 'wine-windows-gates: nodejs.org archive transfer stalled; resuming from the checksum-untrusted transport mirror' >&2 - for attempt in 1 2 3; do - if curl --fail --silent --show-error --location --http1.1 \ - --continue-at - --connect-timeout 10 --max-time 300 \ - --speed-limit 1024 --speed-time 30 \ - -o "$output" "$mirror_url"; then - return 0 - else - status=$? - fi - (( attempt < 3 )) || break - echo "wine-windows-gates: mirror transfer failed (exit $status) on attempt $attempt; resuming partial download" >&2 - done - return "$status" -} - -provision_node() { - # Latest release of the primary line, checksum-verified against the same - # dist directory. Bound and retry every transfer so a stalled nodejs.org - # response cannot consume the entire CI job. Offline runs fall back to the - # newest cached zip, loudly. - local version zip - version="$(curl "${curl_metadata_args[@]}" https://nodejs.org/dist/index.json 2> /dev/null \ - | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{const v=JSON.parse(d).find(r=>r.version.startsWith('v$node_major.'));if(v)console.log(v.version)})" \ - || true)" - if [ -n "$version" ]; then - zip="$cache_dir/node-$version-win-x64.zip" - if [ ! -f "$zip" ]; then - download_node_archive "$version" "$zip.tmp" - local expected - expected="$(curl "${curl_metadata_args[@]}" "https://nodejs.org/dist/$version/SHASUMS256.txt" \ - | awk -v a="node-$version-win-x64.zip" '$2 == a { print $1; exit }')" - [ -n "$expected" ] || { echo "wine-windows-gates: no SHASUMS256 entry for node-$version-win-x64.zip" >&2; exit 1; } - verify_sha256 "$expected" "$zip.tmp" - mv "$zip.tmp" "$zip" - fi - else - zip="$(ls -t "$cache_dir"/node-v"$node_major".*-win-x64.zip 2> /dev/null | head -1 || true)" - [ -n "$zip" ] || { echo "wine-windows-gates: nodejs.org unreachable and no cached Windows Node v$node_major zip in $cache_dir" >&2; exit 1; } - echo "wine-windows-gates: nodejs.org unreachable; using cached $(basename "$zip")" >&2 - fi - unzip -q -o "$zip" -d "$scratch/node-win" - echo "$scratch/node-win/$(basename "$zip" .zip)/node.exe" > "$scratch/node-win-path" -} - -boot_wine() { - "$wine_bin" wineboot --init > /dev/null 2>&1 || true - wineserver -w || true -} - -snapshot_and_install() { - # Tracked + untracked-unignored files, minus agent-session litter; the - # existence filter drops paths staged as deleted. Then the Wine-specific - # install-time overrides go on the SNAPSHOT only: hoisted because Windows - # Node under Wine does not realpath pnpm's isolated-layout symlinks, and - # win32-x64 so the Windows esbuild/rolldown/rollup binaries materialize. - # Neither is recorded in the lockfile, so --frozen-lockfile stays valid; - # --ignore-scripts skips host lifecycle scripts no gate loads. - git -C "$repo_root" ls-files -z --cached --others --exclude-standard -- . ':!:.claude' ':!:.codex' \ - | while IFS= read -r -d '' file; do [ -e "$repo_root/$file" ] && printf '%s\0' "$file"; done \ - | tar -C "$repo_root" --null --files-from=- -cf - \ - | tar -C "$scratch/tree" -xf - - cat >> "$scratch/tree/pnpm-workspace.yaml" << 'EOF' - -nodeLinker: hoisted -supportedArchitectures: - os: [current, win32] - cpu: [current, x64] -EOF - # The hoisted linker — used only by this lane — has an upstream rename - # race (pnpm/pnpm#12880): parallel linkers staging a nested package copy - # (observed on the tree's nested esbuild versions) rename their _tmp_* - # directory onto a path another racer already claimed, and the loser - # exits ERR_PNPM_ENOENT although an identical re-install succeeds. - # Exactly that signature earns up to two retries on a clean tree — the - # snapshot contains no node_modules, so wiping them restores the - # pre-install state; any other failure, or the race still standing after - # the final attempt, fails loud with the log tail. - local attempt - for attempt in 1 2 3; do - (cd "$scratch/tree" && pnpm install --frozen-lockfile --ignore-scripts > "$scratch/logs/install.log" 2>&1) \ - && return 0 - grep -q 'ERR_PNPM_ENOENT.*rename.*_tmp_' "$scratch/logs/install.log" || break - (( attempt < 3 )) || break - echo "wine-windows-gates: pnpm hoisted-linker rename race (pnpm/pnpm#12880) on install attempt $attempt; retrying on a clean tree" >&2 - find "$scratch/tree" -name node_modules -type d -prune -exec rm -rf {} + - done - tail -40 "$scratch/logs/install.log" >&2 - return 1 -} - -mkdir "$scratch/tree" -start=$SECONDS -provision_node & node_pid=$! -boot_wine & wine_pid=$! -snapshot_and_install & install_pid=$! -# Wait for EVERY child before judging any: a bare `wait` under set -e would -# exit on the first failure and let the EXIT trap delete $scratch while the -# other children still run inside it. Named statuses also make the report -# point at the root cause instead of a downstream symptom. -node_status=0; wait "$node_pid" || node_status=$? -wine_status=0; wait "$wine_pid" || wine_status=$? -install_status=0; wait "$install_pid" || install_status=$? -provision_failed=0 -report_provision() { - if (( $2 != 0 )); then - echo "wine-windows-gates: FAILED $1 (exit $2)" >&2 - provision_failed=$2 - fi -} -report_provision 'Windows Node provisioning' "$node_status" -report_provision 'wineboot' "$wine_status" -report_provision 'workspace snapshot + pnpm install' "$install_status" -if (( provision_failed != 0 )); then exit "$provision_failed"; fi -node_win="$(cat "$scratch/node-win-path")" -echo "wine-windows-gates: provisioned in $((SECONDS - start))s (wine $("$wine_bin" --version 2> /dev/null), node $(basename "$(dirname "$node_win")"))" - -# ---- resolve entrypoints, lay the vue link, smoke ------------------------ -# Node under Wine cannot attach stdio to pipes the caller owns (Socket open -# EBADF at bootstrap), so every invocation routes stdio through a file. -wine_node() { - local log="$1" - shift - local status=0 - "$wine_bin" "$node_win" "$@" < /dev/null > "$log" 2>&1 || status=$? - return "$status" -} - -cd "$scratch/tree" -tsc_js='node_modules/typescript/bin/tsc' -tsdown_js='node_modules/tsdown/dist/run.mjs' -vitepress_js='node_modules/vitepress/bin/vitepress.js' -[ -f "$vitepress_js" ] || vitepress_js='website/node_modules/vitepress/bin/vitepress.js' -for entry in "$tsc_js" "$tsdown_js" "$vitepress_js"; do - [ -f "$entry" ] || { echo "wine-windows-gates: expected entrypoint missing after hoisted install: $entry" >&2; exit 1; } -done -# VitePress links vue into the site's node_modules at build time; Wine cannot -# CREATE Windows symlinks (ENOTSUP) but follows pre-existing Unix ones. -if [ -d node_modules/vue ] && [ ! -e website/node_modules/vue ]; then - mkdir -p website/node_modules - ln -s ../../node_modules/vue website/node_modules/vue -fi - -wine_node "$scratch/logs/smoke.log" -p "'smoke: ' + process.platform + ' ' + process.arch + ' ' + process.version" -cat "$scratch/logs/smoke.log" -grep -q '^smoke: win32 x64' "$scratch/logs/smoke.log" || { echo 'wine-windows-gates: Windows Node smoke did not report win32 x64' >&2; exit 1; } - -# ---- the two blocking surfaces, concurrently ------------------------------ -# The build preserves the face order from package.json: compile and bundle the -# Host face before compiling and bundling the Client face. -# Both statuses are captured so one failure cannot hide the other's result. -build_gate() { - wine_node "$scratch/logs/host-tsc.log" "$tsc_js" -b tsconfig.host.json --pretty false || return $? - wine_node "$scratch/logs/host-tsdown.log" "$tsdown_js" --env.DSH_BUILD_FACE host || return $? - wine_node "$scratch/logs/client-tsc.log" "$tsc_js" -b tsconfig.client.json --pretty false || return $? - wine_node "$scratch/logs/client-tsdown.log" "$tsdown_js" --env.DSH_BUILD_FACE client -} -site_gate() { - cd website - wine_node "$scratch/logs/site.log" "../$vitepress_js" build . -} - -start=$SECONDS -build_gate & build_pid=$! -site_gate & site_pid=$! -build_status=0 -wait "$build_pid" || build_status=$? -site_status=0 -wait "$site_pid" || site_status=$? -elapsed=$((SECONDS - start)) - -report() { - local label="$1" status="$2" - shift 2 - if (( status == 0 )); then - echo "wine-windows-gates: PASS $label (${elapsed}s window)" - else - echo "== FAILED $label (exit $status) ==" >&2 - for log in "$@"; do tail -n 200 "$log" >&2 || true; done - fi -} -report 'build (Host tsc/tsdown, Client tsc/tsdown)' "$build_status" \ - "$scratch/logs/host-tsc.log" \ - "$scratch/logs/host-tsdown.log" \ - "$scratch/logs/client-tsc.log" \ - "$scratch/logs/client-tsdown.log" -report 'production site (vitepress build)' "$site_status" "$scratch/logs/site.log" -if (( build_status != 0 )); then exit "$build_status"; fi -exit "$site_status" From 060617c725f41bb88b0b71bd4971457d02afaec9 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Mon, 10 Aug 2026 23:35:58 +0800 Subject: [PATCH 07/10] fix: repair broken cross-links and archived-note sealing after master merge - Fix relative links to archived native-windows-pull-request-ci note (../../archived/ not ../archived/ from implemented/process/) - Replace dangling reference to nonexistent 2026-08-10-unified-windows-selfhosted-ci.md with the archived note - Seal archived note triplet in manifest.json - Update i18n consistency sidecars --- .agents/notes/archived/manifest.json | 3 +++ .../2026-08-08-native-windows-pull-request-ci.i18n.yaml | 4 ++-- .../2026-07-21-serial-cross-platform-ci-reference.i18n.yaml | 4 ++-- .../process/2026-07-21-serial-cross-platform-ci-reference.md | 2 +- .../2026-07-21-serial-cross-platform-ci-reference.zh.md | 2 +- .../2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml | 4 ++-- .../2026-07-22-evidence-based-larger-hosted-runners.md | 2 +- .../2026-07-22-evidence-based-larger-hosted-runners.zh.md | 2 +- .../2026-07-23-portable-required-pull-request-ci.i18n.yaml | 4 ++-- .../process/2026-07-23-portable-required-pull-request-ci.md | 2 +- .../2026-07-23-portable-required-pull-request-ci.zh.md | 2 +- 11 files changed, 17 insertions(+), 14 deletions(-) diff --git a/.agents/notes/archived/manifest.json b/.agents/notes/archived/manifest.json index 638d87373d..b6bb8bb53f 100644 --- a/.agents/notes/archived/manifest.json +++ b/.agents/notes/archived/manifest.json @@ -301,6 +301,9 @@ "process/2026-07-27-wine-windows-gates-experiment.i18n.yaml": "sha256:6f4cbc12ee9cddbb297bf7e138ccabcd204f66898a0f7411b1633f03d5a9eab5", "process/2026-07-27-wine-windows-gates-experiment.md": "sha256:8d37dcdab058098c7de3da1de00ce61bef92bbc8d6ee71add959474c6fb3e936", "process/2026-07-27-wine-windows-gates-experiment.zh.md": "sha256:77fbf04df36af09e55007a93bd6b22d08ff99869efe8de3e97dac5b4701e0a9e", + "process/2026-08-08-native-windows-pull-request-ci.i18n.yaml": "sha256:7533de3f9df64b08f57f2010829d6aec5c83a003c22d66c8f31835ee51694767", + "process/2026-08-08-native-windows-pull-request-ci.md": "sha256:9a62d601c9188c9058a19cbe6b03583eed51a2388a2297ad991e5c1727f756c7", + "process/2026-08-08-native-windows-pull-request-ci.zh.md": "sha256:f4ef98c6d57b46b41a251a6957a2f03f317717bee55c3e6984608d94202b5321", "simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.i18n.yaml": "sha256:ad3d1263cb0051b885173bf064de62065e2c646ccaae2d7250723da3b4eab90c", "simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md": "sha256:8fb061d51c8c23b47d2367814bab3623c6d5b972f38d207a273caa9030b579bd", "simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.zh.md": "sha256:2ffeaca91f82844a5616d6dcce6b4af514bb8a7c46f78e47f668b204ac6edc04", diff --git a/.agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml b/.agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml index 0a565b17d1..e5c58e9e42 100644 --- a/.agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml +++ b/.agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.md -2026-08-08-native-windows-pull-request-ci.md: 3f69f335f6d873124fbbc1fd96d06ff2b3c8dc43 -2026-08-08-native-windows-pull-request-ci.zh.md: 136a7e8791da141cc12ba77d148f47b28db4790b +2026-08-08-native-windows-pull-request-ci.md: a8c84f768b04b4c93ef16f6e9531d674ff59b8ff +2026-08-08-native-windows-pull-request-ci.zh.md: 178940aaca5680f17c188c9d4e19f2b4f48ed9cb diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml index a65b22ba3f..cd17490bb0 100644 --- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md -2026-07-21-serial-cross-platform-ci-reference.md: b7b90f84095fe317f92c051f66da0bf8ee080b19 -2026-07-21-serial-cross-platform-ci-reference.zh.md: d62d7d754eb0343d8b1dd86adae7f501c04b177b +2026-07-21-serial-cross-platform-ci-reference.md: 32e787276628f48c67a76f05c22d7e018590755c +2026-07-21-serial-cross-platform-ci-reference.zh.md: d31c4343a631723ce2ce4311fe44221347078e7c diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md index b7b90f8409..32e7872766 100644 --- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md +++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md @@ -28,7 +28,7 @@ The standalone [Sandbox](../../../../.github/workflows/sandbox.yml) workflow bel Master reference jobs are diagnostic and do not participate in the pull request's required `all checks passed` result. The CI and Sandbox workflows keep their cross-platform references on master pushes. Performance is evaluated from completed hosted-job timestamps and reported as a measurement; it is not encoded as a `timeout-minutes` value. -The portable reference uses GitHub's standard `ubuntu-latest`, `macos-latest`, and `windows-2025` labels. The required pull-request Windows job runs native on the enterprise larger runner under normal operation, failing over to the self-hosted pool under `DSH_CI_FAILOVER=selfhosted`; the archived [dual Windows decision](../archived/process/2026-08-08-native-windows-pull-request-ci.md) records the prior Wine/native topology. Required pull-request jobs use portable standard capacity under the [required-CI decision](2026-07-23-portable-required-pull-request-ci.md). Higher-core hosted runners remain manual benchmarks because a correctness path must remain runnable without repository-external runner configuration. +The portable reference uses GitHub's standard `ubuntu-latest`, `macos-latest`, and `windows-2025` labels. The required pull-request Windows job runs native on the enterprise larger runner under normal operation, failing over to the self-hosted pool under `DSH_CI_FAILOVER=selfhosted`; the archived [dual Windows decision](../../archived/process/2026-08-08-native-windows-pull-request-ci.md) records the prior Wine/native topology. Required pull-request jobs use portable standard capacity under the [required-CI decision](2026-07-23-portable-required-pull-request-ci.md). Higher-core hosted runners remain manual benchmarks because a correctness path must remain runnable without repository-external runner configuration. ## Alternatives considered diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md index d62d7d754e..d31c4343a6 100644 --- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md +++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md @@ -28,7 +28,7 @@ macOS 参考流程使用 fork 进程运行常规 Vitest 项目。macOS arm64 上 master 分支的参考作业仅用于诊断,不参与拉取请求所要求的 `all checks passed` 结果。CI 与 Sandbox 工作流把跨平台参考流程保留在 master 推送上。系统根据已完成托管作业的时间戳评估性能,并将其报告为测量结果,而不是写成 `timeout-minutes` 值。 -可移植的参考流程使用 GitHub 标准的 `ubuntu-latest`、`macos-latest` 和 `windows-2025` 标签。拉取请求必需的 Windows 作业在正常运行下使用企业级大型运行器,在 `DSH_CI_FAILOVER=selfhosted` 时切换至自托管池;已归档的[双 Windows 决策](../archived/process/2026-08-08-native-windows-pull-request-ci.md)记录此前的 Wine/原生拓扑。依据[必需 CI 决策](2026-07-23-portable-required-pull-request-ci.md),拉取请求必需作业使用可移植的标准容量。更高核心数的托管运行器仍仅用于手动基准测试,因为正确性路径必须无需仓库外部的运行器配置即可运行。 +可移植的参考流程使用 GitHub 标准的 `ubuntu-latest`、`macos-latest` 和 `windows-2025` 标签。拉取请求必需的 Windows 作业在正常运行下使用企业级大型运行器,在 `DSH_CI_FAILOVER=selfhosted` 时切换至自托管池;已归档的[双 Windows 决策](../../archived/process/2026-08-08-native-windows-pull-request-ci.md)记录此前的 Wine/原生拓扑。依据[必需 CI 决策](2026-07-23-portable-required-pull-request-ci.md),拉取请求必需作业使用可移植的标准容量。更高核心数的托管运行器仍仅用于手动基准测试,因为正确性路径必须无需仓库外部的运行器配置即可运行。 ## 曾考虑的替代方案 diff --git a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml index 2c61edfac9..cc54e6ee1d 100644 --- a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md -2026-07-22-evidence-based-larger-hosted-runners.md: 84c951809891b4936549a2f429dc7efc99833c1b -2026-07-22-evidence-based-larger-hosted-runners.zh.md: e097f8b18c7a03a4760e9cb4f45b6385c44b051c +2026-07-22-evidence-based-larger-hosted-runners.md: cf7de074585ea98529ce5f9f1cbadb142944f2ef +2026-07-22-evidence-based-larger-hosted-runners.zh.md: 7d59ac56740be7740dfc30669853ccdc80b7a9ee diff --git a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md index 84c9518098..cf7de07458 100644 --- a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md +++ b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md @@ -24,7 +24,7 @@ The gate dependencies remain explicit. Coverage consumes source and does not wai The artifact boundary remains explicit. `scripts/publint-all.ts` calls publint's supported API against an in-memory publication view formed from each manifest's declared files plus npm's mandatory metadata, avoiding one package-manager pack process per package. `scripts/verify-built-package-invariants.mjs` stages the declared `lib/` files below the real package and imports its compiled self-reference through plain Node and Cordis Loader normalization; a runtime chunk omitted from the publication contract still fails. -Within this enterprise required topology, Windows shares one 32-core setup across the blocking build and production site plus observational built-artifact contracts, while Linux owns the duplicate lint, coverage, and snapshot inventories. The later [dual Windows pull-request topology](2026-08-08-native-windows-pull-request-ci.md) adds a separate non-blocking standard-hosted native job that independently enforces supported-source coverage without extending this paid required path. +Within this enterprise required topology, Windows shares one 32-core setup across the blocking build and production site plus observational built-artifact contracts, while Linux owns the duplicate lint, coverage, and snapshot inventories. The later [dual Windows pull-request topology](../../archived/process/2026-08-08-native-windows-pull-request-ci.md) adds a separate non-blocking standard-hosted native job that independently enforces supported-source coverage without extending this paid required path. An exact-head all-size benchmark ran the complete unsharded primary Node aggregate on every Linux pool before the eager-build correction: diff --git a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md index e097f8b18c..7d59ac5674 100644 --- a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md +++ b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md @@ -24,7 +24,7 @@ Linux 主流程使用 3 个相互独立的 32 核作业。覆盖率单独运行 产物边界保持显式。`scripts/publint-all.ts` 对内存中的发布视图调用 publint 支持的 API;该视图由每个 manifest(元数据清单)声明的文件和 npm 强制要求的元数据组成,从而避免为每个包启动一次包管理器 pack 进程。`scripts/verify-built-package-invariants.mjs` 将已声明的 `lib/` 文件暂存到真实包下,并通过普通 Node 和 Cordis Loader 规范化导入其已编译的自身引用;发布约定只要遗漏一个运行时分片,检查仍会失败。 -在这项企业级必需拓扑中,Windows 通过一次 32 核环境设置同时承载阻塞性构建、生产网站与观测性构建产物约定,重复的 lint、覆盖率和快照清单则由 Linux 负责。后续的[拉取请求双 Windows 拓扑](2026-08-08-native-windows-pull-request-ci.md)新增一个独立且不阻断的标准托管原生作业;该作业会独立强制执行受支持源码覆盖率,同时不延长这条付费必需路径。 +在这项企业级必需拓扑中,Windows 通过一次 32 核环境设置同时承载阻塞性构建、生产网站与观测性构建产物约定,重复的 lint、覆盖率和快照清单则由 Linux 负责。后续的[拉取请求双 Windows 拓扑](../../archived/process/2026-08-08-native-windows-pull-request-ci.md)新增一个独立且不阻断的标准托管原生作业;该作业会独立强制执行受支持源码覆盖率,同时不延长这条付费必需路径。 一次分支头精确的全规格基准测试在修正构建尽早启动逻辑前,对每种 Linux 池都运行了完整且未分片的主 Node 聚合流程: diff --git a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.i18n.yaml b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.i18n.yaml index 4738f87fda..3fc548edab 100644 --- a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md -2026-07-23-portable-required-pull-request-ci.md: 6f7a0e0e6354e9bea632d1b00881a44acd921f9e -2026-07-23-portable-required-pull-request-ci.zh.md: 0ec92f11efa55ed155ada1ba858cd1878398f1ec +2026-07-23-portable-required-pull-request-ci.md: 175a849f2cdc8f86e244c034c3aaf68a8fa44307 +2026-07-23-portable-required-pull-request-ci.zh.md: 1dec93aca6f12b9c366ceedc78ce47f76b57d509 diff --git a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md index 6f7a0e0e63..175a849f2c 100644 --- a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md +++ b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md @@ -12,7 +12,7 @@ Billing health, a runner definition's `Ready` state, and a large autoscaling cei ## Decision -[CI](../../../../.github/workflows/ci.yml) runs the required primary Node 24 jobs, plus the stable `all checks passed` aggregate, on repo-restricted enterprise 32-core pools. The aggregate performs no checkout or repository gate, but sharing the enterprise pool prevents the required verdict from introducing a separate standard-hosted billing dependency after its substantive jobs have already succeeded. The required Windows job runs native on the enterprise larger runner under normal operation, failing over to the self-hosted pool under `DSH_CI_FAILOVER=selfhosted` ([unified Windows CI note](2026-08-10-unified-windows-selfhosted-ci.md)). Standard `ubuntu-latest` jobs retain Node 22.19, Node 26, and Python SDK compatibility, and the serial cross-platform standbys continuously validate the failover targets. Those standard-hosted jobs keep the portable execution boundary observable without duplicating the primary inventory on every pull request. +[CI](../../../../.github/workflows/ci.yml) runs the required primary Node 24 jobs, plus the stable `all checks passed` aggregate, on repo-restricted enterprise 32-core pools. The aggregate performs no checkout or repository gate, but sharing the enterprise pool prevents the required verdict from introducing a separate standard-hosted billing dependency after its substantive jobs have already succeeded. The required Windows job runs native on the enterprise larger runner under normal operation, failing over to the self-hosted pool under `DSH_CI_FAILOVER=selfhosted` ([unified Windows CI note](../../archived/process/2026-08-08-native-windows-pull-request-ci.md)). Standard `ubuntu-latest` jobs retain Node 22.19, Node 26, and Python SDK compatibility, and the serial cross-platform standbys continuously validate the failover targets. Those standard-hosted jobs keep the portable execution boundary observable without duplicating the primary inventory on every pull request. The two Linux primary jobs, Node compatibility, Python SDK, and `windows node 24 / native complete` remain dependencies of `all checks passed`. The master-only `serial / windows (self-hosted standby)` is deliberately absent from the aggregate, mirroring the Linux standby pattern. diff --git a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.zh.md b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.zh.md index 0ec92f11ef..1dec93aca6 100644 --- a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.zh.md +++ b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.zh.md @@ -12,7 +12,7 @@ Status: implemented ## 决策 -[CI](../../../../.github/workflows/ci.yml) 在仅限本仓库使用的企业级 32 核运行器池上运行必需的主 Node 24 作业,以及稳定的 `all checks passed` 聚合流程。该聚合流程不执行代码检出或仓库门禁;但让它与所依赖的实质性作业共用企业级运行器池,可以避免这些作业已经成功后,必需判定结果又引入一项单独的标准托管计费依赖。必需的 Windows 作业在正常运行下使用企业级大型运行器,在 `DSH_CI_FAILOVER=selfhosted` 时切换至自托管池([统一 Windows CI 笔记](2026-08-10-unified-windows-selfhosted-ci.md))。标准 `ubuntu-latest` 作业保留 Node 22.19、Node 26 和 Python SDK 兼容性,跨平台串行备援持续验证故障切换目标。这些标准托管作业让可移植执行边界保持可观测,而不必在每个拉取请求中重复主清单。 +[CI](../../../../.github/workflows/ci.yml) 在仅限本仓库使用的企业级 32 核运行器池上运行必需的主 Node 24 作业,以及稳定的 `all checks passed` 聚合流程。该聚合流程不执行代码检出或仓库门禁;但让它与所依赖的实质性作业共用企业级运行器池,可以避免这些作业已经成功后,必需判定结果又引入一项单独的标准托管计费依赖。必需的 Windows 作业在正常运行下使用企业级大型运行器,在 `DSH_CI_FAILOVER=selfhosted` 时切换至自托管池([统一 Windows CI 笔记](../../archived/process/2026-08-08-native-windows-pull-request-ci.md))。标准 `ubuntu-latest` 作业保留 Node 22.19、Node 26 和 Python SDK 兼容性,跨平台串行备援持续验证故障切换目标。这些标准托管作业让可移植执行边界保持可观测,而不必在每个拉取请求中重复主清单。 两项 Linux 主作业、Node 兼容性、Python SDK 和 `windows node 24 / native complete` 继续作为 `all checks passed` 的依赖项;仅 master 触发的 `serial / windows (self-hosted standby)` 被刻意排除,与 Linux 备援模式对称。 From 4b37c4827c21ee6ba21e3728e823d8212de070fa Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Tue, 11 Aug 2026 01:48:37 +0800 Subject: [PATCH 08/10] ci: keep Wine required, add failover to native Windows, enable serial-windows standby MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restore the Wine-emulated windows job as the required pull-request Windows signal. Add DSH_CI_FAILOVER switch to windows-native so it retargets onto the self-hosted [self-hosted, dsh-win-ci, windows] pool under failover. Enable serial-windows as a master-only self-hosted standby mirroring serial-linux-selfhosted. The windows-native job remains absent from all-checks-passed.needs — Wine owns the required verdict, native Windows reports independently. Restore wine-apt-cache, wine-windows-gates.sh, check:windows-wine script, and the original Agent Note topology. --- ...-08-08-dsh-run-headless-command.i18n.yaml} | 6 +- .../2026-08-08-dsh-run-headless-command.md | 44 +++ .../2026-08-08-dsh-run-headless-command.zh.md | 44 +++ .agents/notes/archived/manifest.json | 6 +- ...rial-cross-platform-ci-reference.i18n.yaml | 4 +- ...7-21-serial-cross-platform-ci-reference.md | 4 +- ...1-serial-cross-platform-ci-reference.zh.md | 4 +- ...ence-based-larger-hosted-runners.i18n.yaml | 4 +- ...22-evidence-based-larger-hosted-runners.md | 2 +- ...evidence-based-larger-hosted-runners.zh.md | 2 +- ...ortable-required-pull-request-ci.i18n.yaml | 4 +- ...07-23-portable-required-pull-request-ci.md | 6 +- ...23-portable-required-pull-request-ci.zh.md | 6 +- .../2026-07-26-ci-failover-runbook.i18n.yaml | 4 +- .../process/2026-07-26-ci-failover-runbook.md | 8 +- .../2026-07-26-ci-failover-runbook.zh.md | 8 +- ...n-setup-for-symmetric-ci-caching.i18n.yaml | 4 +- ...m-action-setup-for-symmetric-ci-caching.md | 4 +- ...ction-setup-for-symmetric-ci-caching.zh.md | 4 +- ...8-native-windows-pull-request-ci.i18n.yaml | 6 + ...26-08-08-native-windows-pull-request-ci.md | 1 - ...08-08-native-windows-pull-request-ci.zh.md | 1 - .github/AGENTS.md | 2 +- .github/workflows/ci.yml | 125 +++++++- AGENTS.md | 1 + package.json | 1 + scripts/ci-workflow.spec.ts | 53 ++-- scripts/wine-windows-gates.sh | 282 ++++++++++++++++++ 28 files changed, 561 insertions(+), 79 deletions(-) rename .agents/notes/archived/{process/2026-08-08-native-windows-pull-request-ci.i18n.yaml => feature/2026-08-08-dsh-run-headless-command.i18n.yaml} (56%) create mode 100644 .agents/notes/archived/feature/2026-08-08-dsh-run-headless-command.md create mode 100644 .agents/notes/archived/feature/2026-08-08-dsh-run-headless-command.zh.md create mode 100644 .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml rename .agents/notes/{archived => implemented}/process/2026-08-08-native-windows-pull-request-ci.md (99%) rename .agents/notes/{archived => implemented}/process/2026-08-08-native-windows-pull-request-ci.zh.md (99%) create mode 100755 scripts/wine-windows-gates.sh diff --git a/.agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml b/.agents/notes/archived/feature/2026-08-08-dsh-run-headless-command.i18n.yaml similarity index 56% rename from .agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml rename to .agents/notes/archived/feature/2026-08-08-dsh-run-headless-command.i18n.yaml index e5c58e9e42..d07841d540 100644 --- a/.agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml +++ b/.agents/notes/archived/feature/2026-08-08-dsh-run-headless-command.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write .agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.md -2026-08-08-native-windows-pull-request-ci.md: a8c84f768b04b4c93ef16f6e9531d674ff59b8ff -2026-08-08-native-windows-pull-request-ci.zh.md: 178940aaca5680f17c188c9d4e19f2b4f48ed9cb +# pnpm run verify-translation-pairing --write .agents/notes/archived/feature/2026-08-08-dsh-run-headless-command.md +2026-08-08-dsh-run-headless-command.md: ce9cff965192357022c49655983fe6ff8d554b9f +2026-08-08-dsh-run-headless-command.zh.md: 0484c069ed6365235e4616542a4fb3d5ceb2d880 diff --git a/.agents/notes/archived/feature/2026-08-08-dsh-run-headless-command.md b/.agents/notes/archived/feature/2026-08-08-dsh-run-headless-command.md new file mode 100644 index 0000000000..ce9cff9651 --- /dev/null +++ b/.agents/notes/archived/feature/2026-08-08-dsh-run-headless-command.md @@ -0,0 +1,44 @@ +# Agent Note: `dsh run` owns one-shot headless execution + +Status: implemented +Archived: 2026-08-10 + +English | [中文](2026-08-08-dsh-run-headless-command.zh.md) + +> **Superseded command grammar.** [Apps now own their command lines](../architecture/2026-08-06-app-owned-command-line.md): the headless startup row parses the task from `dsh --profile headless `, and the launcher no longer has a `run` invocation or patches task text into rows. This note remains the rejected launcher-owned design context; the direct execution and completion contract it selected remains current in [headless is a direct core entry point](../architecture/2026-08-09-headless-direct-core-entry-point.md). + +## Problem + +Generic profile boot and one-shot task execution have different lifecycle contracts. A root grammar that accepts optional task text makes one argv shape mean either a long-lived process or a terminating task according to a plugin row discovered only after composition. It also exposes a profile implementation detail as the primary user command and gives custom profiles no explicit one-shot entry. + +The `run` verb must have one top-level meaning. Sharing it with application-file execution or inferring its meaning from positional shape creates the same ambiguity. + +## Decision + +One-shot execution owns this grammar: + +```text +dsh run [--profile ] [--patch ...] +``` + +`--profile` defaults to `headless` and supports custom one-shot compositions. `--patch` is repeatable and occupies the normal overlay layer. Commander joins the variadic task arguments with spaces and rejects a missing or blank task before boot. + +`RunInvocation` is a distinct `DshInvocation` member. The generic profile invocation carries no task state and accepts no positional arguments. Both dispatch paths use `runProfile`: profile boot omits `task`, while `run` supplies it. A one-shot profile without `headless-runner` fails through the composed-row check, and profile boot containing that row without a task points to `dsh run --profile ""`. + +The [profile plugin bundle decision](../architecture/2026-08-05-profile-plugin-bundles.md) owns composition. [Headless is a direct core entry point](../architecture/2026-08-09-headless-direct-core-entry-point.md) owns the execution contract: one fresh persisted Session, final assistant text on stdout, completed/non-completed exit mapping, empty stderr on success, no listening port, and bounded signal shutdown after Agent quiescence and Session flush. + +The `run` verb belongs only to one-shot task execution. Application-file launch requires a distinct command name. + +## Alternatives considered + +| Alternative | Contract mismatch | +|---|---| +| Put task text on root profile boot | Lifecycle meaning depends on a plugin row discovered after parsing. | +| Accept root aliases such as `dsh -p` | The pre-release grammar acquires compatibility branches with no current command ownership. | +| Require `--profile headless` | The shipped one-shot surface loses its shortest canonical spelling. | +| Use `dsh run` for application files | One top-level verb has two meanings and the primary task command becomes indirect. | +| Add a shallow `apps/cli/src/run.ts` forwarder | Command ownership splits without hiding any complexity. | + +## Consequences + +Help, documentation, parser tests, built-bin acceptance, PTY shutdown coverage, and the assembled keyless snapshot use `dsh run`. Custom one-shot profiles use `--profile`; long-lived profile boot and config dumps keep the root profile grammar. Application-file execution is a separate command concern. diff --git a/.agents/notes/archived/feature/2026-08-08-dsh-run-headless-command.zh.md b/.agents/notes/archived/feature/2026-08-08-dsh-run-headless-command.zh.md new file mode 100644 index 0000000000..0484c069ed --- /dev/null +++ b/.agents/notes/archived/feature/2026-08-08-dsh-run-headless-command.zh.md @@ -0,0 +1,44 @@ +# Agent Note: `dsh run` 负责一次性 headless 执行 + +Status: implemented +Archived: 2026-08-10 + +[English](2026-08-08-dsh-run-headless-command.md) | 中文 + +> **命令语法已被取代。** [应用现在持有自己的命令行](../architecture/2026-08-06-app-owned-command-line.md):headless 启动行从 `dsh --profile headless ` 解析任务,启动器不再包含 `run` 调用,也不再把任务文本 patch 进配置行。本笔记保留被否决的启动器持有设计背景;它选定的直接执行与完成约定仍由 [headless 是直接 core 入口](../architecture/2026-08-09-headless-direct-core-entry-point.md)持有。 + +## 问题 + +通用 profile 启动与一次性任务执行具有不同的生命周期约定。若根语法接受可选任务文本,同一种 argv 形态会表示常驻进程或终止式任务,具体含义取决于组合完成后才发现的插件配置行。它还会把 profile 实现细节暴露成主要用户命令,并使自定义 profile 缺少明确的一次性入口。 + +`run` 动词必须只有一种顶层含义。与应用文件执行共用该动词,或根据位置参数形态推断含义,都会产生相同的歧义。 + +## 决策 + +一次性执行采用以下语法: + +```text +dsh run [--profile ] [--patch ...] +``` + +`--profile` 默认为 `headless`,并支持自定义一次性组合。`--patch` 可重复使用,并占据正常的 overlay 层。Commander 用空格拼接可变数量的任务参数,并在启动前拒绝缺失或空白任务。 + +`RunInvocation` 是单独的 `DshInvocation` 成员。通用 profile 调用不携带任务状态,也不接受位置参数。两条分派路径都使用 `runProfile`:profile 启动省略 `task`,而 `run` 提供该字段。缺少 `headless-runner` 的一次性 profile 会触发组合行检查;如果启动的 profile 包含该行却未提供任务,错误会指向 `dsh run --profile ""`。 + +[profile 插件组合包决策](../architecture/2026-08-05-profile-plugin-bundles.md)负责组合。[Headless 是直接 core 入口](../architecture/2026-08-09-headless-direct-core-entry-point.md)负责执行约定:一个新的持久化会话、stdout 上的最终 assistant 文本、completed/非 completed 的退出状态映射、成功时为空的 stderr、无监听端口,以及 Agent 完全停稳且会话 flush 后的有界信号关闭。 + +`run` 动词只负责一次性任务执行。应用文件启动需要不同的命令名。 + +## 考虑过的替代方案 + +| 替代方案 | 约定不匹配之处 | +|---|---| +| 把任务文本放在根 profile 启动命令上 | 生命周期含义依赖解析后才发现的插件配置行。 | +| 接受 `dsh -p` 等根命令别名 | 预发布语法获得不属于任何当前命令的兼容分支。 | +| 要求指定 `--profile headless` | 随附的一次性 surface 失去最短的规范写法。 | +| 将 `dsh run` 用于应用文件 | 一个顶层动词具有两种含义,主要任务命令也变得间接。 | +| 添加仅转发的 `apps/cli/src/run.ts` | 命令归属被拆分,却没有隐藏任何复杂度。 | + +## 后果 + +帮助信息、文档、解析器测试、构建后二进制验收、PTY 关闭覆盖和组装应用的无密钥快照均使用 `dsh run`。自定义一次性 profile 使用 `--profile`;常驻 profile 启动与配置 dump 使用根 profile 语法。应用文件执行是独立的命令关注点。 diff --git a/.agents/notes/archived/manifest.json b/.agents/notes/archived/manifest.json index b6bb8bb53f..d3d1795489 100644 --- a/.agents/notes/archived/manifest.json +++ b/.agents/notes/archived/manifest.json @@ -253,6 +253,9 @@ "feature/2026-07-31-web-cards-toolrow.i18n.yaml": "sha256:f9a6ab72a77934cdcc02167c7313f08d7e9925362017b34bed7ad56c8c70fbaa", "feature/2026-07-31-web-cards-toolrow.md": "sha256:5058f7cec4497d1cb0a5c8e77b88fddacac6eead034f3edec88e8514919b8a3e", "feature/2026-07-31-web-cards-toolrow.zh.md": "sha256:ba84ef2e1be61211ab5ba6950b78ede3d3a979f252bc068d3e04e2c025f7bc03", + "feature/2026-08-08-dsh-run-headless-command.i18n.yaml": "sha256:1c2b4c5b61b9263b6267275d6fc69faeaad3cc887f0728a7ed4172d817af812b", + "feature/2026-08-08-dsh-run-headless-command.md": "sha256:7695fe7fd322377d5986f14e35f13337f4cd376405c758218a81230f6d182d1c", + "feature/2026-08-08-dsh-run-headless-command.zh.md": "sha256:113c14a36c64d2facc8ae46f37c7aa76359d8cacb9c18fcba26a723f15d036fb", "process/2026-06-11-doc-sync-enforcement.i18n.yaml": "sha256:33b6d5874427bd7a2bd82e7e2f4f482b12448b2464aef15a9c57975edb48554d", "process/2026-06-11-doc-sync-enforcement.md": "sha256:aa2fe83d519fc30d48dff19e596e83c8922aacc9e063e14fe2cc35b769b9100e", "process/2026-06-11-doc-sync-enforcement.zh.md": "sha256:698017bd35f030fdea3eac51df9e43138c48140f504739d687b7251d13fced2b", @@ -301,9 +304,6 @@ "process/2026-07-27-wine-windows-gates-experiment.i18n.yaml": "sha256:6f4cbc12ee9cddbb297bf7e138ccabcd204f66898a0f7411b1633f03d5a9eab5", "process/2026-07-27-wine-windows-gates-experiment.md": "sha256:8d37dcdab058098c7de3da1de00ce61bef92bbc8d6ee71add959474c6fb3e936", "process/2026-07-27-wine-windows-gates-experiment.zh.md": "sha256:77fbf04df36af09e55007a93bd6b22d08ff99869efe8de3e97dac5b4701e0a9e", - "process/2026-08-08-native-windows-pull-request-ci.i18n.yaml": "sha256:7533de3f9df64b08f57f2010829d6aec5c83a003c22d66c8f31835ee51694767", - "process/2026-08-08-native-windows-pull-request-ci.md": "sha256:9a62d601c9188c9058a19cbe6b03583eed51a2388a2297ad991e5c1727f756c7", - "process/2026-08-08-native-windows-pull-request-ci.zh.md": "sha256:f4ef98c6d57b46b41a251a6957a2f03f317717bee55c3e6984608d94202b5321", "simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.i18n.yaml": "sha256:ad3d1263cb0051b885173bf064de62065e2c646ccaae2d7250723da3b4eab90c", "simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md": "sha256:8fb061d51c8c23b47d2367814bab3623c6d5b972f38d207a273caa9030b579bd", "simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.zh.md": "sha256:2ffeaca91f82844a5616d6dcce6b4af514bb8a7c46f78e47f668b204ac6edc04", diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml index cd17490bb0..aa3cabead0 100644 --- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md -2026-07-21-serial-cross-platform-ci-reference.md: 32e787276628f48c67a76f05c22d7e018590755c -2026-07-21-serial-cross-platform-ci-reference.zh.md: d31c4343a631723ce2ce4311fe44221347078e7c +2026-07-21-serial-cross-platform-ci-reference.md: c2ed11d40f7f5487117b5c72f11bc1709042f68a +2026-07-21-serial-cross-platform-ci-reference.zh.md: bef8c3b640cf43942e380e921d5f62d91723eff8 diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md index 32e7872766..c2ed11d40f 100644 --- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md +++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md @@ -16,7 +16,7 @@ Real-kernel sandbox proofs require specific hosted operating systems and archite ## Decision -[CI](../../../../.github/workflows/ci.yml) gives pull-request and master-push events complementary responsibilities. Pull requests run consolidated Linux and native Windows jobs plus the Node compatibility and Python contracts. On a push to `master`, the active references are `serial / linux (self-hosted standby)` and `serial / windows (self-hosted standby)` on the in-house self-hosted pools — the hot-standby drills that continuously re-prove the failover targets described in the [failover runbook](2026-07-26-ci-failover-runbook.md). The standard-hosted `serial / linux`, `serial / macos`, and `serial / windows` definitions remain disabled under `TODO(hosted-serial-ci)` until their portable capacity can be restored. The separate job definitions intentionally keep their short checkout, runtime setup, and immutable install sequences visible instead of hiding operating systems behind a matrix or reusable workflow. `workflow_dispatch` is reserved for runner benchmarks. +[CI](../../../../.github/workflows/ci.yml) gives pull-request and master-push events complementary responsibilities. Pull requests run consolidated Linux and Wine-hosted Windows jobs plus the Node compatibility and Python contracts on standard GitHub-hosted capacity; an independent native Windows job reports the complete Windows inventory without participating in the required aggregate. On a push to `master`, the active references are `serial / linux (self-hosted standby)` on the in-house `vm-backup` pool and `serial / windows (self-hosted standby)` on the in-house `dsh-win-ci` pool — the hot-standby drills that continuously re-prove the failover targets described in the [failover runbook](2026-07-26-ci-failover-runbook.md). The standard-hosted `serial / linux`, `serial / macos`, and `serial / windows` definitions remain disabled under `TODO(hosted-serial-ci)` until their portable capacity can be restored. The separate job definitions intentionally keep their short checkout, runtime setup, and immutable install sequences visible instead of hiding operating systems behind a matrix or reusable workflow. `workflow_dispatch` is reserved for runner benchmarks. Each reference job runs `pnpm run check:ci` without any shard selector. `DSH_GATE_CONCURRENCY=1` makes the top-level aggregate execute one ready gate at a time; coverage, snapshot replay, built-bin smoke, and publication validation also receive worker counts of one. The reference jobs may run beside one another, but each host's repository gates are serial and complete. Linux installs bubblewrap before replaying snapshots, and Windows enables Developer Mode before installing the symlinked workspace. @@ -28,7 +28,7 @@ The standalone [Sandbox](../../../../.github/workflows/sandbox.yml) workflow bel Master reference jobs are diagnostic and do not participate in the pull request's required `all checks passed` result. The CI and Sandbox workflows keep their cross-platform references on master pushes. Performance is evaluated from completed hosted-job timestamps and reported as a measurement; it is not encoded as a `timeout-minutes` value. -The portable reference uses GitHub's standard `ubuntu-latest`, `macos-latest`, and `windows-2025` labels. The required pull-request Windows job runs native on the enterprise larger runner under normal operation, failing over to the self-hosted pool under `DSH_CI_FAILOVER=selfhosted`; the archived [dual Windows decision](../../archived/process/2026-08-08-native-windows-pull-request-ci.md) records the prior Wine/native topology. Required pull-request jobs use portable standard capacity under the [required-CI decision](2026-07-23-portable-required-pull-request-ci.md). Higher-core hosted runners remain manual benchmarks because a correctness path must remain runnable without repository-external runner configuration. +The portable reference uses GitHub's standard `ubuntu-latest`, `macos-latest`, and `windows-2025` labels. The required pull-request Windows job runs under Wine on `ubuntu-latest`, while the independent pull-request native job uses the hosted `dsh-windows-2025-16core` runner under normal operation and the self-hosted `[self-hosted, dsh-win-ci, windows]` pool under failover (see the [failover runbook](2026-07-26-ci-failover-runbook.md)), and is absent from the required aggregate under the [dual Windows decision](2026-08-08-native-windows-pull-request-ci.md); when enabled, `serial / windows` remains a second complete, unsharded native-kernel oracle. Required pull-request jobs use portable standard capacity under the [required-CI decision](2026-07-23-portable-required-pull-request-ci.md). Higher-core hosted runners remain manual benchmarks because a correctness path must remain runnable without repository-external runner configuration. ## Alternatives considered diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md index d31c4343a6..bef8c3b640 100644 --- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md +++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md @@ -16,7 +16,7 @@ Status: implemented ## 决策 -[CI](../../../../.github/workflows/ci.yml) 为拉取请求事件与 master 推送事件赋予互补的职责。拉取请求运行合并后的 Linux 与原生 Windows 作业,以及 Node 兼容性与 Python 约定。向 `master` 推送时,当前启用的参考作业是公司自有自托管池上的 `serial / linux (self-hosted standby)` 与 `serial / windows (self-hosted standby)`——这些热备演练持续验证[故障切换手册](2026-07-26-ci-failover-runbook.md)所描述的切换目标。标准托管的 `serial / linux`、`serial / macos` 和 `serial / windows` 定义仍处于禁用状态,并由 `TODO(hosted-serial-ci)` 标记,直到其可移植容量恢复。各自独立的作业定义有意显式保留简短的代码检出、运行时设置和依赖锁定的安装步骤,而不是用矩阵或可复用工作流隐藏操作系统差异。`workflow_dispatch` 仅用于运行器基准测试。 +[CI](../../../../.github/workflows/ci.yml) 为拉取请求事件与 master 推送事件赋予互补的职责。拉取请求在 GitHub 标准托管容量上运行合并后的 Linux 和由 Wine 承载的 Windows 作业,以及 Node 兼容性与 Python 约定;一个独立的原生 Windows 作业会报告完整的 Windows 清单,但不参与必需聚合流程。向 `master` 推送时,当前启用的参考作业是公司自有 `vm-backup` 池上的 `serial / linux (self-hosted standby)` 和 `dsh-win-ci` 池上的 `serial / windows (self-hosted standby)`——这些热备演练持续验证[故障切换手册](2026-07-26-ci-failover-runbook.md)所描述的切换目标。标准托管的 `serial / linux`、`serial / macos` 和 `serial / windows` 定义仍处于禁用状态,并由 `TODO(hosted-serial-ci)` 标记,直到其可移植容量恢复。各自独立的作业定义有意显式保留简短的代码检出、运行时设置和依赖锁定的安装步骤,而不是用矩阵或可复用工作流隐藏操作系统差异。`workflow_dispatch` 仅用于运行器基准测试。 每个参考作业均在不设置任何分片选择器的情况下运行 `pnpm run check:ci`。`DSH_GATE_CONCURRENCY=1` 使顶层聚合每次只执行一个已经就绪的门禁;覆盖率、快照回放、built-bin 冒烟测试和发布验证的 worker 数量也设为 1。各参考作业可以彼此并行,但每台主机上的仓库门禁都串行运行且完整执行。Linux 在回放快照前安装 bubblewrap,Windows 则在安装采用符号链接的工作区前启用开发人员模式。 @@ -28,7 +28,7 @@ macOS 参考流程使用 fork 进程运行常规 Vitest 项目。macOS arm64 上 master 分支的参考作业仅用于诊断,不参与拉取请求所要求的 `all checks passed` 结果。CI 与 Sandbox 工作流把跨平台参考流程保留在 master 推送上。系统根据已完成托管作业的时间戳评估性能,并将其报告为测量结果,而不是写成 `timeout-minutes` 值。 -可移植的参考流程使用 GitHub 标准的 `ubuntu-latest`、`macos-latest` 和 `windows-2025` 标签。拉取请求必需的 Windows 作业在正常运行下使用企业级大型运行器,在 `DSH_CI_FAILOVER=selfhosted` 时切换至自托管池;已归档的[双 Windows 决策](../../archived/process/2026-08-08-native-windows-pull-request-ci.md)记录此前的 Wine/原生拓扑。依据[必需 CI 决策](2026-07-23-portable-required-pull-request-ci.md),拉取请求必需作业使用可移植的标准容量。更高核心数的托管运行器仍仅用于手动基准测试,因为正确性路径必须无需仓库外部的运行器配置即可运行。 +可移植的参考流程使用 GitHub 标准的 `ubuntu-latest`、`macos-latest` 和 `windows-2025` 标签。拉取请求必需的 Windows 作业在 `ubuntu-latest` 上通过 Wine 运行,而独立的拉取请求原生作业在正常运行下使用托管的 `dsh-windows-2025-16core` 运行器,故障切换时使用自托管 `[self-hosted, dsh-win-ci, windows]` 池(参见[故障切换手册](2026-07-26-ci-failover-runbook.md)),依据[双 Windows 决策](2026-08-08-native-windows-pull-request-ci.md)不参与必需聚合流程;`serial / windows` 启用时,仍作为第二个完整且未分片的原生内核标尺。依据[必需 CI 决策](2026-07-23-portable-required-pull-request-ci.md),拉取请求必需作业使用可移植的标准容量。更高核心数的托管运行器仍仅用于手动基准测试,因为正确性路径必须无需仓库外部的运行器配置即可运行。 ## 曾考虑的替代方案 diff --git a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml index cc54e6ee1d..2c61edfac9 100644 --- a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md -2026-07-22-evidence-based-larger-hosted-runners.md: cf7de074585ea98529ce5f9f1cbadb142944f2ef -2026-07-22-evidence-based-larger-hosted-runners.zh.md: 7d59ac56740be7740dfc30669853ccdc80b7a9ee +2026-07-22-evidence-based-larger-hosted-runners.md: 84c951809891b4936549a2f429dc7efc99833c1b +2026-07-22-evidence-based-larger-hosted-runners.zh.md: e097f8b18c7a03a4760e9cb4f45b6385c44b051c diff --git a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md index cf7de07458..84c9518098 100644 --- a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md +++ b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md @@ -24,7 +24,7 @@ The gate dependencies remain explicit. Coverage consumes source and does not wai The artifact boundary remains explicit. `scripts/publint-all.ts` calls publint's supported API against an in-memory publication view formed from each manifest's declared files plus npm's mandatory metadata, avoiding one package-manager pack process per package. `scripts/verify-built-package-invariants.mjs` stages the declared `lib/` files below the real package and imports its compiled self-reference through plain Node and Cordis Loader normalization; a runtime chunk omitted from the publication contract still fails. -Within this enterprise required topology, Windows shares one 32-core setup across the blocking build and production site plus observational built-artifact contracts, while Linux owns the duplicate lint, coverage, and snapshot inventories. The later [dual Windows pull-request topology](../../archived/process/2026-08-08-native-windows-pull-request-ci.md) adds a separate non-blocking standard-hosted native job that independently enforces supported-source coverage without extending this paid required path. +Within this enterprise required topology, Windows shares one 32-core setup across the blocking build and production site plus observational built-artifact contracts, while Linux owns the duplicate lint, coverage, and snapshot inventories. The later [dual Windows pull-request topology](2026-08-08-native-windows-pull-request-ci.md) adds a separate non-blocking standard-hosted native job that independently enforces supported-source coverage without extending this paid required path. An exact-head all-size benchmark ran the complete unsharded primary Node aggregate on every Linux pool before the eager-build correction: diff --git a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md index 7d59ac5674..e097f8b18c 100644 --- a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md +++ b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md @@ -24,7 +24,7 @@ Linux 主流程使用 3 个相互独立的 32 核作业。覆盖率单独运行 产物边界保持显式。`scripts/publint-all.ts` 对内存中的发布视图调用 publint 支持的 API;该视图由每个 manifest(元数据清单)声明的文件和 npm 强制要求的元数据组成,从而避免为每个包启动一次包管理器 pack 进程。`scripts/verify-built-package-invariants.mjs` 将已声明的 `lib/` 文件暂存到真实包下,并通过普通 Node 和 Cordis Loader 规范化导入其已编译的自身引用;发布约定只要遗漏一个运行时分片,检查仍会失败。 -在这项企业级必需拓扑中,Windows 通过一次 32 核环境设置同时承载阻塞性构建、生产网站与观测性构建产物约定,重复的 lint、覆盖率和快照清单则由 Linux 负责。后续的[拉取请求双 Windows 拓扑](../../archived/process/2026-08-08-native-windows-pull-request-ci.md)新增一个独立且不阻断的标准托管原生作业;该作业会独立强制执行受支持源码覆盖率,同时不延长这条付费必需路径。 +在这项企业级必需拓扑中,Windows 通过一次 32 核环境设置同时承载阻塞性构建、生产网站与观测性构建产物约定,重复的 lint、覆盖率和快照清单则由 Linux 负责。后续的[拉取请求双 Windows 拓扑](2026-08-08-native-windows-pull-request-ci.md)新增一个独立且不阻断的标准托管原生作业;该作业会独立强制执行受支持源码覆盖率,同时不延长这条付费必需路径。 一次分支头精确的全规格基准测试在修正构建尽早启动逻辑前,对每种 Linux 池都运行了完整且未分片的主 Node 聚合流程: diff --git a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.i18n.yaml b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.i18n.yaml index 3fc548edab..e67565f381 100644 --- a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md -2026-07-23-portable-required-pull-request-ci.md: 175a849f2cdc8f86e244c034c3aaf68a8fa44307 -2026-07-23-portable-required-pull-request-ci.zh.md: 1dec93aca6f12b9c366ceedc78ce47f76b57d509 +2026-07-23-portable-required-pull-request-ci.md: b7c637408afa32611c4e70073ddec9aaabfba7f5 +2026-07-23-portable-required-pull-request-ci.zh.md: ee88412d982da5de613d8dfdbdcdd9977c899d2d diff --git a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md index 175a849f2c..b7c637408a 100644 --- a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md +++ b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md @@ -12,9 +12,9 @@ Billing health, a runner definition's `Ready` state, and a large autoscaling cei ## Decision -[CI](../../../../.github/workflows/ci.yml) runs the required primary Node 24 jobs, plus the stable `all checks passed` aggregate, on repo-restricted enterprise 32-core pools. The aggregate performs no checkout or repository gate, but sharing the enterprise pool prevents the required verdict from introducing a separate standard-hosted billing dependency after its substantive jobs have already succeeded. The required Windows job runs native on the enterprise larger runner under normal operation, failing over to the self-hosted pool under `DSH_CI_FAILOVER=selfhosted` ([unified Windows CI note](../../archived/process/2026-08-08-native-windows-pull-request-ci.md)). Standard `ubuntu-latest` jobs retain Node 22.19, Node 26, and Python SDK compatibility, and the serial cross-platform standbys continuously validate the failover targets. Those standard-hosted jobs keep the portable execution boundary observable without duplicating the primary inventory on every pull request. +[CI](../../../../.github/workflows/ci.yml) runs the required primary Node 24 jobs, plus the stable `all checks passed` aggregate, on repo-restricted enterprise 32-core pools. The aggregate performs no checkout or repository gate, but sharing the enterprise pool prevents the required verdict from introducing a separate standard-hosted billing dependency after its substantive jobs have already succeeded. The required Windows job runs Windows Node under Wine on standard `ubuntu-latest` for the blocking surfaces; an independent native `windows-2025` job starts automatically but does not participate in the aggregate ([dual Windows decision](2026-08-08-native-windows-pull-request-ci.md)). Standard `ubuntu-latest` jobs retain Node 22.19, Node 26, and Python SDK compatibility, and the serial references remain the complete unsharded cross-platform definitions. Those standard-hosted jobs keep the portable execution boundary observable without duplicating the primary inventory on every pull request. -The two Linux primary jobs, Node compatibility, Python SDK, and `windows node 24 / native complete` remain dependencies of `all checks passed`. The master-only `serial / windows (self-hosted standby)` is deliberately absent from the aggregate, mirroring the Linux standby pattern. +The two Linux primary jobs, Node compatibility, Python SDK, and `windows node 24 / wine blocking` remain dependencies of `all checks passed`; `windows node 24 / native complete` is deliberately absent. Branch protection continues to require `e2e` and `all checks passed`. There is no automatic fallback when a remaining enterprise Linux label cannot allocate: the standard jobs continue to report their own contracts, but they cannot manufacture the missing required result. The [larger-runner decision](2026-07-22-evidence-based-larger-hosted-runners.md) owns the current primary topology and its measurements. The [serial cross-platform reference](2026-07-21-serial-cross-platform-ci-reference.md) remains the independent standard-hosted completeness check, and the manual larger-runner suites retain size comparisons without expanding the ordinary required matrix. @@ -30,6 +30,6 @@ The [larger-runner decision](2026-07-22-evidence-based-larger-hosted-runners.md) ## Consequences -Ordinary pull requests spend enterprise capacity on the Linux critical path while the native Windows job uses the enterprise larger runner on the normal path and the self-hosted pool under failover. A live exact-head run distinguishes the commands branch protection consumes from the separate diagnostic contract; queue delay is reported separately from each job's `startedAt` to `completedAt` execution interval. +Ordinary pull requests spend enterprise capacity on the Linux critical path while the Wine job keeps the required Windows verdict on standard Linux allocation. The independent native job uses standard Windows allocation without delaying or changing the aggregate. A live exact-head run distinguishes the commands branch protection consumes from the separate diagnostic contract; queue delay is reported separately from each job's `startedAt` to `completedAt` execution interval. Standard compatibility, required Wine, and diagnostic native Windows jobs remain useful when enterprise allocation is degraded, but they do not make a blocked required Linux job or aggregate green. Recovering Linux availability may require restoring the complete standard-hosted topology; changing a pool definition's status alone is insufficient evidence that it can receive work. diff --git a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.zh.md b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.zh.md index 1dec93aca6..ee88412d98 100644 --- a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.zh.md +++ b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.zh.md @@ -12,9 +12,9 @@ Status: implemented ## 决策 -[CI](../../../../.github/workflows/ci.yml) 在仅限本仓库使用的企业级 32 核运行器池上运行必需的主 Node 24 作业,以及稳定的 `all checks passed` 聚合流程。该聚合流程不执行代码检出或仓库门禁;但让它与所依赖的实质性作业共用企业级运行器池,可以避免这些作业已经成功后,必需判定结果又引入一项单独的标准托管计费依赖。必需的 Windows 作业在正常运行下使用企业级大型运行器,在 `DSH_CI_FAILOVER=selfhosted` 时切换至自托管池([统一 Windows CI 笔记](../../archived/process/2026-08-08-native-windows-pull-request-ci.md))。标准 `ubuntu-latest` 作业保留 Node 22.19、Node 26 和 Python SDK 兼容性,跨平台串行备援持续验证故障切换目标。这些标准托管作业让可移植执行边界保持可观测,而不必在每个拉取请求中重复主清单。 +[CI](../../../../.github/workflows/ci.yml) 在仅限本仓库使用的企业级 32 核运行器池上运行必需的主 Node 24 作业,以及稳定的 `all checks passed` 聚合流程。该聚合流程不执行代码检出或仓库门禁;但让它与所依赖的实质性作业共用企业级运行器池,可以避免这些作业已经成功后,必需判定结果又引入一项单独的标准托管计费依赖。必需的 Windows 作业在标准 `ubuntu-latest` 上通过 Wine 运行 Windows Node,覆盖阻断性检查范围;一个独立的原生 `windows-2025` 作业会自动启动,但不参与聚合流程([双 Windows 决策](2026-08-08-native-windows-pull-request-ci.md))。标准 `ubuntu-latest` 作业保留 Node 22.19、Node 26 和 Python SDK 兼容性,串行参考流程仍是完整且未分片的跨平台定义。这些标准托管作业让可移植执行边界保持可观测,而不必在每个拉取请求中重复主清单。 -两项 Linux 主作业、Node 兼容性、Python SDK 和 `windows node 24 / native complete` 继续作为 `all checks passed` 的依赖项;仅 master 触发的 `serial / windows (self-hosted standby)` 被刻意排除,与 Linux 备援模式对称。 +两项 Linux 主作业、Node 兼容性、Python SDK 和 `windows node 24 / wine blocking` 继续作为 `all checks passed` 的依赖项;`windows node 24 / native complete` 被刻意排除。分支保护继续要求 `e2e` 和 `all checks passed`。剩余的企业级 Linux 运行器标签无法分配运行器时没有自动后备机制:标准作业会继续报告各自的约定,但无法产出缺失的必需结果。 当前主拓扑及其测量结果由[大型运行器决策](2026-07-22-evidence-based-larger-hosted-runners.md)记录。[跨平台串行参考流程](2026-07-21-serial-cross-platform-ci-reference.md)继续作为独立的标准托管完整性检查,手动大型运行器套件则保留规格比较,同时不扩大普通必需矩阵。 @@ -30,6 +30,6 @@ Status: implemented ## 后果 -普通拉取请求会将企业级运行器容量用于 Linux 关键路径,而原生 Windows 作业在正常运行下使用企业级大型运行器、故障切换时使用自托管池。独立原生作业使用标准 Windows 运行器容量,不会延迟或改变聚合流程。一次实际的分支头精确运行会区分分支保护采用的命令与单独的诊断约定;排队延迟与每个作业从 `startedAt` 到 `completedAt` 的执行区间分开报告。 +普通拉取请求会将企业级运行器容量用于 Linux 关键路径,而 Wine 作业让必需的 Windows 判定继续使用标准 Linux 运行器容量。独立原生作业使用标准 Windows 运行器容量,不会延迟或改变聚合流程。一次实际的分支头精确运行会区分分支保护采用的命令与单独的诊断约定;排队延迟与每个作业从 `startedAt` 到 `completedAt` 的执行区间分开报告。 企业级运行器分配能力下降时,标准兼容性作业、必需的 Wine 作业与诊断性原生 Windows 作业仍能提供有用证据,但无法让受阻的必需 Linux 作业或聚合流程变绿。恢复 Linux 可用性时,可能需要恢复完整的标准托管拓扑;仅改变运行器池定义的状态,不足以证明它可以接收作业。 diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml index 2403d869c6..f4fe0c74d2 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md -2026-07-26-ci-failover-runbook.md: 25bfb8cc7ca6d53c5d28e7c941facdb86e5e147b -2026-07-26-ci-failover-runbook.zh.md: 22195500f5a28c9e92bc8aac8c58028628ab1d6b +2026-07-26-ci-failover-runbook.md: 72261f95ea74b61e3915a1a6419b2c2e616efbd9 +2026-07-26-ci-failover-runbook.zh.md: 1ee679eabc296ab31d71b87945409788539425bd diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md index 25bfb8cc7c..72261f95ea 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md @@ -6,20 +6,16 @@ English | [中文](2026-07-26-ci-failover-runbook.zh.md) ## Problem -The three required Linux worker jobs in [CI](../../../../.github/workflows/ci.yml) (`node 24 / static`, `node 24 / coverage`, `node 24 / snapshots and artifacts`) run on the hosted enterprise 32-core pools; the required verdict job that aggregates them (`all checks passed`) runs on standard `ubuntu-latest`. The required Windows job (`windows node 24 / native complete`) runs on the hosted enterprise larger runner. When the enterprise pools degrade — jobs queue indefinitely or the enterprise labels vanish — every open pull request becomes unmergeable, and the ordinary recovery of merging a fix is itself deadlocked behind the very required checks that cannot run. **Scope: this switch recovers an enterprise Linux-pool outage AND a hosted Windows-pool outage.** The verdict's other required dependencies (`node-compat`, `python-sdk`) stay on standard hosted runners by design (the portable boundary); in a broader GitHub-hosted capacity failure that also takes out the standard pools, those dependencies still block `all checks passed`. An outage therefore needs a switch any responder with repository write access can throw without merging anything. +The three required Linux worker jobs in [CI](../../../../.github/workflows/ci.yml) (`node 24 / static`, `node 24 / coverage`, `node 24 / snapshots and artifacts`) run on the hosted enterprise 32-core pools; the required verdict job that aggregates them (`all checks passed`) runs on standard `ubuntu-latest`. When the enterprise pools degrade — jobs queue indefinitely or the enterprise labels vanish — every open pull request becomes unmergeable, and the ordinary recovery of merging a fix is itself deadlocked behind the very required checks that cannot run. **Scope: this switch recovers an enterprise Linux-pool outage.** The verdict's other required dependencies (`node-compat`, `python-sdk`, `windows`) stay on standard hosted runners by design (the portable boundary); in a broader GitHub-hosted capacity failure that also takes out the standard pools, those dependencies still block `all checks passed`, and only the Windows leg has no in-house substitute at all — during the 2026-07-27 outage the standard pools recovered first, which is the ordering this design bets on. An outage therefore needs a switch any responder with repository write access can throw without merging anything. ## Decision -Each of the three required Linux worker jobs, the required Windows job, and the `all checks passed` verdict job — which would otherwise stay queued on the failed pool even after every worker passed — resolves its runner pool through the `DSH_CI_FAILOVER` repository variable. Unset (normal), they run on the hosted enterprise pools. Set to `selfhosted` by any repository writer, all five retarget onto the in-house self-hosted pools: the Linux jobs and verdict onto the `vm-backup` pool (`[self-hosted, linux, x64, vm-backup]`), coverage and snapshot concurrency drop to shared-VM bounds, and the hosted-path pnpm cache restores are skipped; the Windows job onto the `dsh-win-ci` pool (`[self-hosted, dsh-win-ci, windows]`). The switch is writer-manageable repository state, not a merge, so it works while every check is red. The in-house pools' readiness is continuously re-proven by the `serial / linux (self-hosted standby)` and `serial / windows (self-hosted standby)` lanes, which run the complete unsharded aggregates on every master push. +Each of the three required Linux worker jobs — and the `all checks passed` verdict job, which would otherwise stay queued on the failed pool even after every worker passed — resolves its runner pool through the `DSH_CI_FAILOVER` repository variable. Unset (normal), they run on the hosted enterprise pools. Set to `selfhosted` by any repository writer, all four retarget onto the in-house self-hosted `vm-backup` pool, coverage and snapshot concurrency drop to shared-VM bounds, and the hosted-path pnpm cache restores are skipped. The switch is writer-manageable repository state, not a merge, so it works while every check is red. The in-house pool's readiness is continuously re-proven by the `serial / linux (self-hosted standby)` lane, which runs the complete unsharded aggregate on every master push. ### What the in-house pool is `vm-backup`: one 64-core VM, six always-on systemd-managed runner instances. Its image must preinstall Playwright Chromium's Linux system packages; CI downloads the lockfile-selected browser but never runs `apt` on this persistent shared host. Check the latest `serial / linux (self-hosted standby)` run before switching: its aggregate includes browser replay, so a green standby verifies both ordinary capacity and this browser prerequisite. -#### Windows pool - -`dsh-win-ci`: 16 always-on runner instances on the in-house Windows CI server. Labels: `[self-hosted, dsh-win-ci, windows]`. The image must preinstall Node 24, pnpm, Git, and enable Developer Mode for symlink support; Playwright Chromium is installed per-run by the `serial / windows (self-hosted standby)` lane. Check the latest `serial / windows (self-hosted standby)` run before switching: a green standby verifies the pool can execute `check:ci:windows-complete` end-to-end. - ### Switch (any repository writer, ~1 minute, no merge) 1. Repository **Settings → Secrets and variables → Actions → Variables → New repository variable**: name `DSH_CI_FAILOVER`, value `selfhosted`. diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md index 22195500f5..1ee679eabc 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md @@ -6,20 +6,16 @@ Status: implemented ## 问题 -[CI](../../../../.github/workflows/ci.yml) 中三个必需的 Linux 工作作业(`node 24 / static`、`node 24 / coverage`、`node 24 / snapshots and artifacts`)运行在托管的企业级 32 核池上;聚合它们的必需判定作业(`all checks passed`)运行在标准 `ubuntu-latest` 上。必需的 Windows 作业(`windows node 24 / native complete`)运行在托管的企业级大型运行器上。当企业池发生故障——作业无限排队或企业标签消失——所有开启的拉取请求都无法合并,而"合并一个修复"这一常规恢复手段本身正被那些无法运行的必需检查死锁。**适用范围:本切换恢复的是企业级 Linux 池故障和托管 Windows 池故障。**判定作业的其余必需依赖(`node-compat`、`python-sdk`)按设计留在标准托管运行器上(可移植边界);若更大范围的 GitHub 托管容量故障连标准池一并击倒,这些依赖仍会阻塞 `all checks passed`。因此故障需要一个任何具备仓库写权限的响应者都能在不合并任何代码的情况下触发的开关。 +[CI](../../../../.github/workflows/ci.yml) 中三个必需的 Linux 工作作业(`node 24 / static`、`node 24 / coverage`、`node 24 / snapshots and artifacts`)运行在托管的企业级 32 核池上;聚合它们的必需判定作业(`all checks passed`)运行在标准 `ubuntu-latest` 上。当企业池发生故障——作业无限排队或企业标签消失——所有开启的拉取请求都无法合并,而"合并一个修复"这一常规恢复手段本身正被那些无法运行的必需检查死锁。**适用范围:本切换恢复的是企业级 Linux 池故障。**判定作业的其余必需依赖(`node-compat`、`python-sdk`、`windows`)按设计留在标准托管运行器上(可移植边界);若更大范围的 GitHub 托管容量故障连标准池一并击倒,这些依赖仍会阻塞 `all checks passed`,且只有 Windows 这条腿完全没有自有替代——2026-07-27 的故障中标准池率先恢复,本设计押注的正是这一顺序。因此故障需要一个任何具备仓库写权限的响应者都能在不合并任何代码的情况下触发的开关。 ## 决策 -三个必需的 Linux 工作作业、必需的 Windows 作业,以及 `all checks passed` 判定作业(若不随切换,即使全部工作作业通过,它仍会滞留在故障池的队列中)——各自通过仓库变量 `DSH_CI_FAILOVER` 解析运行器池。变量不存在(正常)时它们运行在托管企业池上;由任何具备写权限的协作者设为 `selfhosted` 时,五者全部切换到公司自有的自托管池:Linux 作业与判定位移入 `vm-backup` 池(`[self-hosted, linux, x64, vm-backup]`),覆盖率与快照的并发降到共享虚拟机上限,并跳过托管路径的 pnpm 缓存恢复;Windows 作业移入 `dsh-win-ci` 池(`[self-hosted, dsh-win-ci, windows]`)。这个开关是写者可管理的仓库状态而非一次合并,因此在所有检查都是红色时仍然有效。自有池的就绪状态由 `serial / linux (self-hosted standby)` 和 `serial / windows (self-hosted standby)` 通道持续验证——每次 master 推送都在其上运行完整的未分片聚合流程。 +三个必需的 Linux 工作作业——以及 `all checks passed` 判定作业(若不随切换,即使全部工作作业通过,它仍会滞留在故障池的队列中)——各自通过仓库变量 `DSH_CI_FAILOVER` 解析运行器池。变量不存在(正常)时它们运行在托管企业池上;由任何具备写权限的协作者设为 `selfhosted` 时,四者全部切换到公司自有的自托管 `vm-backup` 池,覆盖率与快照的并发降到共享虚拟机上限,并跳过托管路径的 pnpm 缓存恢复。这个开关是写者可管理的仓库状态而非一次合并,因此在所有检查都是红色时仍然有效。自有池的就绪状态由 `serial / linux (self-hosted standby)` 通道持续验证——每次 master 推送都在其上运行完整的未分片聚合流程。 ### 自有池是什么 `vm-backup`:一台 64 核虚拟机,6 个常驻 systemd 管理的运行器实例。其镜像必须预装 Playwright Chromium 的 Linux 系统软件包;CI 会下载锁文件选定的浏览器,但绝不在这台持久化共享主机上运行 `apt`。切换前先看 `serial / linux (self-hosted standby)` 最近一次运行:其聚合流程包含浏览器回放,因此绿色热备同时验证常规容量和这项浏览器先决条件。 -#### Windows 池 - -`dsh-win-ci`:公司自有 Windows CI 服务器上的 16 个常驻运行器实例。标签:`[self-hosted, dsh-win-ci, windows]`。镜像必须预装 Node 24、pnpm、Git,并启用开发人员模式以支持符号链接;Playwright Chromium 由 `serial / windows (self-hosted standby)` 通道在每次运行时安装。切换前先看 `serial / windows (self-hosted standby)` 最近一次运行:绿色热备验证该池可完整执行 `check:ci:windows-complete`。 - ### 切换步骤(任何具备写权限的协作者,约 1 分钟,无需合并) 1. 仓库 **Settings → Secrets and variables → Actions → Variables → New repository variable**:名称 `DSH_CI_FAILOVER`,值 `selfhosted`。 diff --git a/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.i18n.yaml b/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.i18n.yaml index b619bdc1d8..c0a7dcfc02 100644 --- a/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.md -2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.md: 612c42d19e3891f3fa036fea74b36e36da2fb5cd -2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.zh.md: 7c82c787f11e93cbb62209d25df741d0abeba3dd +2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.md: 499141ca6a3703a12d10c195b732441d49419599 +2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.zh.md: c57b7d32cd7204a845ab1884e9ae0b9045749a1c diff --git a/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.md b/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.md index 612c42d19e..499141ca6a 100644 --- a/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.md +++ b/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.md @@ -13,8 +13,8 @@ Outside `landlock-run.yml`, each workflow that installed pnpm hand-provisioned i `pnpm/action-setup@v4` is the only pnpm provisioning mechanism in CI: no workflow runs `corepack enable`. The root dev dependency on `@yarnpkg/cli-dist` separately supplies the modern Yarn CLI exercised by the generated-project e2e; package-manager coverage therefore does not inherit the runner image's Yarn Classic. Caching remains per-job policy on top of pnpm provisioning, in three deliberate shapes: - **Symmetric cache** (restore and save): `actions/setup-node` with `cache: pnpm` — `e2e.yml`, `docs-pages.yml`, `pi-ai-provider-e2e.yml`, `build-exe-for-python-sdk.yml`, and the node-compat and two benchmark jobs of `ci.yml`. The larger-runner benchmark keeps its store cache Linux-only through a conditional `cache:` input; the consolidated benchmark caches on both platforms. -- **Restore-only / producer pairing** (hand-rolled `actions/cache` steps): the three enterprise-runner PR jobs restore without saving, keeping cache compression/upload off their latency-sensitive paths — an asymmetry `setup-node`'s cache cannot express. Each configures a store outside the action's replaceable install directory and resolves that path, matching the master-push serial-linux producer's path and exact key; the enterprise jobs skip restore during self-hosted failover because that VM's persistent store is already warm. -- **Cache-less or persistent** (no store-cache action): the required native Windows job, serial-windows standby, serial-macos, plus `sandbox.yml` install from a cold or runner-local store. Extracting the many-file pnpm store costs more than a clean Windows install; the self-hosted standby and failover jobs instead reuse their VM's persistent pnpm store without transferring a hosted cache archive. +- **Restore-only / producer pairing** (hand-rolled `actions/cache` steps): the three enterprise-runner PR jobs and the Wine-based required Windows job restore without saving, keeping cache compression/upload off their latency-sensitive paths — an asymmetry `setup-node`'s cache cannot express. Each configures a store outside the action's replaceable install directory and resolves that path, matching the master-push serial-linux producer's path and exact key; the enterprise jobs skip restore during self-hosted failover because that VM's persistent store is already warm. +- **Cache-less or persistent** (no store-cache action): the independent native Windows job, native serial-windows and serial-macos, plus `sandbox.yml` install from a cold or runner-local store. Extracting the many-file pnpm store costs more than a clean Windows install; the self-hosted standby and failover jobs instead reuse their VM's persistent pnpm store without transferring a hosted cache archive. ## Alternatives considered diff --git a/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.zh.md b/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.zh.md index 7c82c787f1..c57b7d32cd 100644 --- a/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.zh.md +++ b/.agents/notes/implemented/process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.zh.md @@ -13,8 +13,8 @@ Status: implemented `pnpm/action-setup@v4` 是 CI 中提供 pnpm 的唯一机制:没有任何工作流运行 `corepack enable`。根目录的 `@yarnpkg/cli-dist` 开发依赖另行提供 generated-project e2e 所运行的现代 Yarn CLI(命令行界面);因此,用于包管理器覆盖率的 Yarn 不会沿用 runner 镜像里的 Yarn Classic。缓存仍是叠加在 pnpm 提供机制上的按作业政策,保持三种刻意的形态: - **对称缓存**(既恢复也保存):带 `cache: pnpm` 的 `actions/setup-node`——`e2e.yml`、`docs-pages.yml`、`pi-ai-provider-e2e.yml`、`build-exe-for-python-sdk.yml`,以及 `ci.yml` 的 node-compat 与两个 benchmark 作业。larger-runner benchmark 通过条件化的 `cache:` 输入让 store 缓存仅限 Linux;consolidated benchmark 在两个平台上都启用缓存。 -- **只恢复不上传/生产者配对**(手写的 `actions/cache` 步骤):企业 runner 上的三个 PR(Pull Request)作业只恢复不保存,把缓存压缩/上传挡在它们的延迟敏感路径之外——这种不对称是 `setup-node` 的缓存无法表达的。每个作业都在 action 可替换的安装目录之外配置 store,并解析该路径,从而与 master 推送触发的 serial-linux 生产者所用的路径和精确键匹配;企业作业在自托管故障切换期间跳过恢复,因为该 VM 的持久 store 已经预热。 -- **无缓存或持久化**(不使用 store 缓存 action):必需的 Windows 作业、serial-windows 热备、serial-macos,以及 `sandbox.yml` 均从冷 store 或 runner 本地 store 安装。解压含有大量文件的 pnpm store,成本高于在 Windows 上进行一次全新安装;自托管热备与故障切换作业则复用其 VM 的持久 pnpm store,不传输托管缓存归档。 +- **只恢复不上传/生产者配对**(手写的 `actions/cache` 步骤):企业 runner 上的三个 PR(Pull Request)作业和基于 Wine 的必需 Windows 作业只恢复不保存,把缓存压缩/上传挡在它们的延迟敏感路径之外——这种不对称是 `setup-node` 的缓存无法表达的。每个作业都在 action 可替换的安装目录之外配置 store,并解析该路径,从而与 master 推送触发的 serial-linux 生产者所用的路径和精确键匹配;企业作业在自托管故障切换期间跳过恢复,因为该 VM 的持久 store 已经预热。 +- **无缓存或持久化**(不使用 store 缓存 action):独立的原生 Windows 作业、原生 serial-windows 和 serial-macos,以及 `sandbox.yml` 均从冷 store 或 runner 本地 store 安装。解压含有大量文件的 pnpm store,成本高于在 Windows 上进行一次全新安装;自托管热备与故障切换作业则复用其 VM 的持久 pnpm store,不传输托管缓存归档。 ## 曾考虑的替代方案 diff --git a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml new file mode 100644 index 0000000000..dcdbff1208 --- /dev/null +++ b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md +2026-08-08-native-windows-pull-request-ci.md: 33fbf1ae378112b4fd82633a77afa52056d93d98 +2026-08-08-native-windows-pull-request-ci.zh.md: 552e5cd3129011198fe442ba747cf2fdb7d97365 diff --git a/.agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.md b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md similarity index 99% rename from .agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.md rename to .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md index a8c84f768b..33fbf1ae37 100644 --- a/.agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.md +++ b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md @@ -1,7 +1,6 @@ # Agent Note: Dual Wine and native Windows pull-request CI Status: implemented -Archived: 2026-08-10 English | [中文](2026-08-08-native-windows-pull-request-ci.zh.md) diff --git a/.agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.zh.md b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md similarity index 99% rename from .agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.zh.md rename to .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md index 178940aaca..552e5cd312 100644 --- a/.agents/notes/archived/process/2026-08-08-native-windows-pull-request-ci.zh.md +++ b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md @@ -1,7 +1,6 @@ # Agent Note: Wine 与原生 Windows 双通道拉取请求 CI Status: implemented -Archived: 2026-08-10 [English](2026-08-08-native-windows-pull-request-ci.md) | 中文 diff --git a/.github/AGENTS.md b/.github/AGENTS.md index ad2aeac354..3e879db8ae 100644 --- a/.github/AGENTS.md +++ b/.github/AGENTS.md @@ -1,3 +1,3 @@ # AGENTS.md — GitHub Actions -Run Windows CI jobs under native `pwsh`. The required pull-request `windows` job and the master `serial-windows` standby both run on real Windows: normally on the hosted enterprise runner, failing over to the self-hosted `[self-hosted, dsh-win-ci, windows]` pool under `DSH_CI_FAILOVER=selfhosted`. +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` (or the self-hosted `[self-hosted, dsh-win-ci, windows]` pool under `DSH_CI_FAILOVER=selfhosted`) but reports independently. The master `serial-windows` standby continuously validates the self-hosted failover target — see the [failover runbook](../.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md). diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81a8ffc53f..1249439fde 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -300,12 +300,123 @@ jobs: - name: Run complete keyless Python suite run: uv run --python 3.10 --group test --project python/sdk pytest - # 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. + # 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. 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 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. Under normal operation it 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-native: if: github.event_name == 'pull_request' runs-on: >- ${{ vars.DSH_CI_FAILOVER == 'selfhosted' @@ -765,7 +876,9 @@ 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`. (`needs` cannot reach across workflow files; e2e.yml stays its own + # `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 # check.) # `if: always()` is load-bearing: without it a failed dependency # would SKIP this job, and GitHub counts a skipped required check as passing diff --git a/AGENTS.md b/AGENTS.md index 48d915344e..f6c75a7372 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,6 +70,7 @@ pnpm run lint pnpm run duplication # cross-file TypeScript clone detection pnpm run build # tsc emits lib/types, tsdown bundles runtime pnpm run hygiene # knip + publint + workspace constraints + NodeNext consumer check +pnpm run check:windows-wine # ONLY when diagnosing a known Windows failure (needs wine); CI owns this signal pnpm run doc-sync # all documentation gates; leaf list in scripts/run-gates.ts pnpm run website:build # VitePress build (doubles as dead-link check) pnpm run demo:headless "task" # one-shot agent (needs DEEPSEEK_API_KEY) diff --git a/package.json b/package.json index b1ef084671..3fd0255f98 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "check:ci:snapshot": "tsx scripts/run-gates.ts ci-snapshot", "check:ci:artifacts": "tsx scripts/run-gates.ts ci-artifacts", "check:ci:consumers": "tsx scripts/run-gates.ts ci-consumers", + "check:windows-wine": "bash scripts/wine-windows-gates.sh", "check:ci:windows-blocking": "tsx scripts/run-gates.ts ci-windows-blocking", "check:ci:windows-complete": "tsx scripts/run-gates.ts ci-windows-complete", "check:ci:windows-observational": "tsx scripts/run-gates.ts ci-windows-observational", diff --git a/scripts/ci-workflow.spec.ts b/scripts/ci-workflow.spec.ts index b6d7d858ff..3731017029 100644 --- a/scripts/ci-workflow.spec.ts +++ b/scripts/ci-workflow.spec.ts @@ -27,16 +27,20 @@ describe('CI workflow', () => { } }) - it('keeps a single required native Windows job with failover and a master-only standby', () => { + it('keeps a required Wine Windows job, a non-blocking native Windows job with failover, and a master-only standby', () => { const workflow = loadWorkflow('.github/workflows/ci.yml') if (!isRecord(workflow.jobs) || !isRecord(workflow.jobs.windows) + || !isRecord(workflow.jobs['windows-native']) + || !isRecord(workflow.jobs['wine-apt-cache']) || !isRecord(workflow.jobs['serial-windows']) || !isRecord(workflow.jobs['all-checks-passed'])) { - throw new TypeError('CI workflow must define windows, serial-windows, and all-checks-passed jobs') + throw new TypeError('CI workflow must define windows, windows-native, wine-apt-cache, serial-windows, and all-checks-passed jobs') } const windows = workflow.jobs.windows + const windowsNative = workflow.jobs['windows-native'] + const wineAptCache = workflow.jobs['wine-apt-cache'] const serialWindows = workflow.jobs['serial-windows'] const aggregate = workflow.jobs['all-checks-passed'] if (!Array.isArray(windows.steps) || !Array.isArray(aggregate.needs)) { @@ -46,38 +50,35 @@ describe('CI workflow', () => { isRecord(step) && typeof step.run === 'string' )) - // Required PR job: native Windows, failover-able, runs windows-complete. - expect(typeof windows['runs-on']).toBe('string') - expect(windows['runs-on']).toContain('DSH_CI_FAILOVER') - expect(windows['runs-on']).toContain('self-hosted') - expect(windows['runs-on']).toContain('dsh-win-ci') - expect(windows['runs-on']).toContain('dsh-windows-2025-16core') - expect(windows.name).toBe('windows node 24 / native complete') + // Required PR job: Wine on ubuntu-latest, runs wine-windows-gates.sh. + expect(windows['runs-on']).toBe('ubuntu-latest') + expect(windows.name).toBe('windows node 24 / wine blocking') expect(windows.if).toBe("github.event_name == 'pull_request'") - expect(windows.env).toMatchObject({ - DSH_COVERAGE_MAX_WORKERS: '2', - DSH_GATE_CONCURRENCY: '2', - DSH_PUBLINT_CONCURRENCY: '8', - }) - expect(commandSteps.every(step => step.shell === 'pwsh')).toBe(true) - expect(commandSteps.map(step => step.run)).toContain('pnpm run check:ci:windows-complete') - const developerMode = windows.steps.find((step): step is Record & { run: string } => ( - isRecord(step) && typeof step.run === 'string' && step.run.includes('AllowDevelopmentWithoutDevLicense') - )) - expect(developerMode).toBeDefined() + expect(commandSteps.some(step => step.run.includes('wine-windows-gates.sh'))).toBe(true) - // No Wine or dual-lane remnants. - const workflowJson = JSON.stringify(workflow) - expect(workflowJson).not.toMatch(/wine/i) - expect(workflow.jobs).not.toHaveProperty('windows-native') - expect(workflow.jobs).not.toHaveProperty('wine-apt-cache') + // windows-native: non-blocking native job with failover, runs windows-complete. + expect(typeof windowsNative['runs-on']).toBe('string') + expect(windowsNative['runs-on']).toContain('DSH_CI_FAILOVER') + 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'") + const nativeCommandSteps = (windowsNative.steps as unknown[]).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') + + // wine-apt-cache: master-only, seeds the Wine apt cache. + expect(wineAptCache.if).toBe("github.event_name == 'push' && github.ref == 'refs/heads/master'") + expect(wineAptCache['runs-on']).toBe('ubuntu-latest') // serial-windows: master-only standby, self-hosted, non-blocking. expect(serialWindows.if).toBe("github.event_name == 'push' && github.ref == 'refs/heads/master'") expect(serialWindows['runs-on']).toEqual(['self-hosted', 'dsh-win-ci', 'windows']) expect(serialWindows.name).toBe('serial / windows (self-hosted standby)') - // Aggregate: windows required, serial-windows excluded. + // Aggregate: Wine `windows` required, native `windows-native` excluded. expect(aggregate.needs).toContain('windows') expect(aggregate.needs).not.toContain('windows-native') expect(aggregate.needs).not.toContain('serial-windows') diff --git a/scripts/wine-windows-gates.sh b/scripts/wine-windows-gates.sh new file mode 100755 index 0000000000..5e46b75f91 --- /dev/null +++ b/scripts/wine-windows-gates.sh @@ -0,0 +1,282 @@ +#!/usr/bin/env bash +# Run the blocking Windows gates (workspace build, production site) with real +# win-x64 Node.js under Wine — the same script the pull-request `windows` job +# in ci.yml executes and the optional local gate `pnpm run check:windows-wine` +# wraps. Owning rationale and fidelity limits: +# .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md +# +# The working tree is never mutated: tracked plus untracked-unignored files +# are snapshotted into a scratch directory, the Wine-specific pnpm overrides +# (hoisted layout, win32-x64 platform packages) are appended to the SNAPSHOT's +# pnpm-workspace.yaml, and the install and gates run there against the shared +# pnpm store. The Wine prefix and the checksum-verified Windows Node zip +# persist in .cache/wine-windows/ so reruns skip provisioning. +# +# Environment: DSH_WINE_NODE_MAJOR (default $PRIMARY_NODE_VERSION, then 24) +# picks the Windows Node line; DSH_WINE_GATE_CACHE_DIR relocates the cache; +# DSH_WINE_GATE_KEEP=1 preserves the scratch tree for inspection. + +set -euo pipefail + +repo_root="$(git rev-parse --show-toplevel)" +node_major="${DSH_WINE_NODE_MAJOR:-${PRIMARY_NODE_VERSION:-24}}" +cache_dir="${DSH_WINE_GATE_CACHE_DIR:-$repo_root/.cache/wine-windows}" + +export WINEDEBUG='-all' +export WINEARCH=win64 +# Skip Wine Mono / Gecko installers: Node needs neither. +export WINEDLLOVERRIDES='mscoree,mshtml=' +export WINEPREFIX="$cache_dir/prefix" + +# ---- preflight: fail loud before any expensive work -------------------- +wine_bin='' +for candidate in "$(command -v wine || true)" "$(command -v wine64 || true)" /usr/lib/wine/wine64; do + if [ -n "$candidate" ] && [ -x "$candidate" ]; then wine_bin="$candidate"; break; fi +done +# GNU coreutils sha256sum on Linux; perl shasum ships with macOS. Both +# accept the same " " --check input. +checksum_tool='' +if command -v sha256sum > /dev/null; then + checksum_tool='sha256sum' +elif command -v shasum > /dev/null; then + checksum_tool='shasum' +fi +missing=() +[ -n "$wine_bin" ] || missing+=('wine (apt: wine | brew: wine-stable)') +command -v curl > /dev/null || missing+=('curl') +command -v unzip > /dev/null || missing+=('unzip') +[ -n "$checksum_tool" ] || missing+=('sha256sum or shasum (apt: coreutils | macOS ships shasum)') +if ! command -v pnpm > /dev/null; then corepack enable > /dev/null 2>&1 || true; fi +command -v pnpm > /dev/null || missing+=('pnpm (corepack enable)') +if (( ${#missing[@]} > 0 )); then + printf 'wine-windows-gates: missing required tool: %s\n' "${missing[@]}" >&2 + exit 1 +fi + +# Verify file $2 against SHA-256 hex $1 with whichever tool preflight found. +verify_sha256() { + case "$checksum_tool" in + sha256sum) printf '%s %s\n' "$1" "$2" | sha256sum --check - > /dev/null ;; + shasum) printf '%s %s\n' "$1" "$2" | shasum -a 256 --check - > /dev/null ;; + esac +} + +scratch="$(mktemp -d "${TMPDIR:-/tmp}/dsh-wine-gates.XXXXXX")" +cleanup() { + wineserver -k > /dev/null 2>&1 || true + if [ "${DSH_WINE_GATE_KEEP:-0}" = '1' ]; then + echo "wine-windows-gates: scratch tree kept at $scratch" + else + rm -rf "$scratch" + fi +} +trap cleanup EXIT +mkdir -p "$cache_dir" "$scratch/logs" + +# ---- provision Windows Node, boot Wine, snapshot + install concurrently ---- +curl_metadata_args=( + --fail --silent --show-error --location + --retry 3 --retry-all-errors --retry-delay 2 + --http1.1 --connect-timeout 10 --max-time 30 --retry-max-time 120 +) + +download_node_archive() { + local version="$1" output="$2" attempt status=0 + local archive="node-$version-win-x64.zip" + local primary_url="https://nodejs.org/dist/$version/$archive" + local mirror_url="https://npmmirror.com/mirrors/node/$version/$archive" + + if curl --fail --silent --show-error --location --http1.1 \ + --connect-timeout 10 --max-time 300 --speed-limit 1024 --speed-time 30 \ + -o "$output" "$primary_url"; then + return 0 + fi + echo 'wine-windows-gates: nodejs.org archive transfer stalled; resuming from the checksum-untrusted transport mirror' >&2 + for attempt in 1 2 3; do + if curl --fail --silent --show-error --location --http1.1 \ + --continue-at - --connect-timeout 10 --max-time 300 \ + --speed-limit 1024 --speed-time 30 \ + -o "$output" "$mirror_url"; then + return 0 + else + status=$? + fi + (( attempt < 3 )) || break + echo "wine-windows-gates: mirror transfer failed (exit $status) on attempt $attempt; resuming partial download" >&2 + done + return "$status" +} + +provision_node() { + # Latest release of the primary line, checksum-verified against the same + # dist directory. Bound and retry every transfer so a stalled nodejs.org + # response cannot consume the entire CI job. Offline runs fall back to the + # newest cached zip, loudly. + local version zip + version="$(curl "${curl_metadata_args[@]}" https://nodejs.org/dist/index.json 2> /dev/null \ + | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{const v=JSON.parse(d).find(r=>r.version.startsWith('v$node_major.'));if(v)console.log(v.version)})" \ + || true)" + if [ -n "$version" ]; then + zip="$cache_dir/node-$version-win-x64.zip" + if [ ! -f "$zip" ]; then + download_node_archive "$version" "$zip.tmp" + local expected + expected="$(curl "${curl_metadata_args[@]}" "https://nodejs.org/dist/$version/SHASUMS256.txt" \ + | awk -v a="node-$version-win-x64.zip" '$2 == a { print $1; exit }')" + [ -n "$expected" ] || { echo "wine-windows-gates: no SHASUMS256 entry for node-$version-win-x64.zip" >&2; exit 1; } + verify_sha256 "$expected" "$zip.tmp" + mv "$zip.tmp" "$zip" + fi + else + zip="$(ls -t "$cache_dir"/node-v"$node_major".*-win-x64.zip 2> /dev/null | head -1 || true)" + [ -n "$zip" ] || { echo "wine-windows-gates: nodejs.org unreachable and no cached Windows Node v$node_major zip in $cache_dir" >&2; exit 1; } + echo "wine-windows-gates: nodejs.org unreachable; using cached $(basename "$zip")" >&2 + fi + unzip -q -o "$zip" -d "$scratch/node-win" + echo "$scratch/node-win/$(basename "$zip" .zip)/node.exe" > "$scratch/node-win-path" +} + +boot_wine() { + "$wine_bin" wineboot --init > /dev/null 2>&1 || true + wineserver -w || true +} + +snapshot_and_install() { + # Tracked + untracked-unignored files, minus agent-session litter; the + # existence filter drops paths staged as deleted. Then the Wine-specific + # install-time overrides go on the SNAPSHOT only: hoisted because Windows + # Node under Wine does not realpath pnpm's isolated-layout symlinks, and + # win32-x64 so the Windows esbuild/rolldown/rollup binaries materialize. + # Neither is recorded in the lockfile, so --frozen-lockfile stays valid; + # --ignore-scripts skips host lifecycle scripts no gate loads. + git -C "$repo_root" ls-files -z --cached --others --exclude-standard -- . ':!:.claude' ':!:.codex' \ + | while IFS= read -r -d '' file; do [ -e "$repo_root/$file" ] && printf '%s\0' "$file"; done \ + | tar -C "$repo_root" --null --files-from=- -cf - \ + | tar -C "$scratch/tree" -xf - + cat >> "$scratch/tree/pnpm-workspace.yaml" << 'EOF' + +nodeLinker: hoisted +supportedArchitectures: + os: [current, win32] + cpu: [current, x64] +EOF + # The hoisted linker — used only by this lane — has an upstream rename + # race (pnpm/pnpm#12880): parallel linkers staging a nested package copy + # (observed on the tree's nested esbuild versions) rename their _tmp_* + # directory onto a path another racer already claimed, and the loser + # exits ERR_PNPM_ENOENT although an identical re-install succeeds. + # Exactly that signature earns up to two retries on a clean tree — the + # snapshot contains no node_modules, so wiping them restores the + # pre-install state; any other failure, or the race still standing after + # the final attempt, fails loud with the log tail. + local attempt + for attempt in 1 2 3; do + (cd "$scratch/tree" && pnpm install --frozen-lockfile --ignore-scripts > "$scratch/logs/install.log" 2>&1) \ + && return 0 + grep -q 'ERR_PNPM_ENOENT.*rename.*_tmp_' "$scratch/logs/install.log" || break + (( attempt < 3 )) || break + echo "wine-windows-gates: pnpm hoisted-linker rename race (pnpm/pnpm#12880) on install attempt $attempt; retrying on a clean tree" >&2 + find "$scratch/tree" -name node_modules -type d -prune -exec rm -rf {} + + done + tail -40 "$scratch/logs/install.log" >&2 + return 1 +} + +mkdir "$scratch/tree" +start=$SECONDS +provision_node & node_pid=$! +boot_wine & wine_pid=$! +snapshot_and_install & install_pid=$! +# Wait for EVERY child before judging any: a bare `wait` under set -e would +# exit on the first failure and let the EXIT trap delete $scratch while the +# other children still run inside it. Named statuses also make the report +# point at the root cause instead of a downstream symptom. +node_status=0; wait "$node_pid" || node_status=$? +wine_status=0; wait "$wine_pid" || wine_status=$? +install_status=0; wait "$install_pid" || install_status=$? +provision_failed=0 +report_provision() { + if (( $2 != 0 )); then + echo "wine-windows-gates: FAILED $1 (exit $2)" >&2 + provision_failed=$2 + fi +} +report_provision 'Windows Node provisioning' "$node_status" +report_provision 'wineboot' "$wine_status" +report_provision 'workspace snapshot + pnpm install' "$install_status" +if (( provision_failed != 0 )); then exit "$provision_failed"; fi +node_win="$(cat "$scratch/node-win-path")" +echo "wine-windows-gates: provisioned in $((SECONDS - start))s (wine $("$wine_bin" --version 2> /dev/null), node $(basename "$(dirname "$node_win")"))" + +# ---- resolve entrypoints, lay the vue link, smoke ------------------------ +# Node under Wine cannot attach stdio to pipes the caller owns (Socket open +# EBADF at bootstrap), so every invocation routes stdio through a file. +wine_node() { + local log="$1" + shift + local status=0 + "$wine_bin" "$node_win" "$@" < /dev/null > "$log" 2>&1 || status=$? + return "$status" +} + +cd "$scratch/tree" +tsc_js='node_modules/typescript/bin/tsc' +tsdown_js='node_modules/tsdown/dist/run.mjs' +vitepress_js='node_modules/vitepress/bin/vitepress.js' +[ -f "$vitepress_js" ] || vitepress_js='website/node_modules/vitepress/bin/vitepress.js' +for entry in "$tsc_js" "$tsdown_js" "$vitepress_js"; do + [ -f "$entry" ] || { echo "wine-windows-gates: expected entrypoint missing after hoisted install: $entry" >&2; exit 1; } +done +# VitePress links vue into the site's node_modules at build time; Wine cannot +# CREATE Windows symlinks (ENOTSUP) but follows pre-existing Unix ones. +if [ -d node_modules/vue ] && [ ! -e website/node_modules/vue ]; then + mkdir -p website/node_modules + ln -s ../../node_modules/vue website/node_modules/vue +fi + +wine_node "$scratch/logs/smoke.log" -p "'smoke: ' + process.platform + ' ' + process.arch + ' ' + process.version" +cat "$scratch/logs/smoke.log" +grep -q '^smoke: win32 x64' "$scratch/logs/smoke.log" || { echo 'wine-windows-gates: Windows Node smoke did not report win32 x64' >&2; exit 1; } + +# ---- the two blocking surfaces, concurrently ------------------------------ +# The build preserves the face order from package.json: compile and bundle the +# Host face before compiling and bundling the Client face. +# Both statuses are captured so one failure cannot hide the other's result. +build_gate() { + wine_node "$scratch/logs/host-tsc.log" "$tsc_js" -b tsconfig.host.json --pretty false || return $? + wine_node "$scratch/logs/host-tsdown.log" "$tsdown_js" --env.DSH_BUILD_FACE host || return $? + wine_node "$scratch/logs/client-tsc.log" "$tsc_js" -b tsconfig.client.json --pretty false || return $? + wine_node "$scratch/logs/client-tsdown.log" "$tsdown_js" --env.DSH_BUILD_FACE client +} +site_gate() { + cd website + wine_node "$scratch/logs/site.log" "../$vitepress_js" build . +} + +start=$SECONDS +build_gate & build_pid=$! +site_gate & site_pid=$! +build_status=0 +wait "$build_pid" || build_status=$? +site_status=0 +wait "$site_pid" || site_status=$? +elapsed=$((SECONDS - start)) + +report() { + local label="$1" status="$2" + shift 2 + if (( status == 0 )); then + echo "wine-windows-gates: PASS $label (${elapsed}s window)" + else + echo "== FAILED $label (exit $status) ==" >&2 + for log in "$@"; do tail -n 200 "$log" >&2 || true; done + fi +} +report 'build (Host tsc/tsdown, Client tsc/tsdown)' "$build_status" \ + "$scratch/logs/host-tsc.log" \ + "$scratch/logs/host-tsdown.log" \ + "$scratch/logs/client-tsc.log" \ + "$scratch/logs/client-tsdown.log" +report 'production site (vitepress build)' "$site_status" "$scratch/logs/site.log" +if (( build_status != 0 )); then exit "$build_status"; fi +exit "$site_status" From 8424a73cd8470f36d5b012a10c5b7e5ab2592816 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Tue, 11 Aug 2026 11:59:15 +0800 Subject: [PATCH 09/10] docs: extend failover runbook with Windows pool Cover the hosted Windows-pool outage in scope, add the dsh-win-ci pool description (32 scheduled-task runner instances, 96-core machine, Git Bash on PATH prerequisite), and update the switch to retarget all five jobs (three Linux workers, native Windows, all-checks-passed). --- .../process/2026-07-26-ci-failover-runbook.i18n.yaml | 4 ++-- .../process/2026-07-26-ci-failover-runbook.md | 12 ++++++++---- .../process/2026-07-26-ci-failover-runbook.zh.md | 10 +++++++--- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml index f4fe0c74d2..8615d248fc 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md -2026-07-26-ci-failover-runbook.md: 72261f95ea74b61e3915a1a6419b2c2e616efbd9 -2026-07-26-ci-failover-runbook.zh.md: 1ee679eabc296ab31d71b87945409788539425bd +2026-07-26-ci-failover-runbook.md: 47901844f4ec581dff8500cc429c54a076b7642b +2026-07-26-ci-failover-runbook.zh.md: 88a793e144a4d06718ffe323a94f8e81f8e62b82 diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md index 72261f95ea..47901844f4 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md @@ -6,25 +6,29 @@ English | [中文](2026-07-26-ci-failover-runbook.zh.md) ## Problem -The three required Linux worker jobs in [CI](../../../../.github/workflows/ci.yml) (`node 24 / static`, `node 24 / coverage`, `node 24 / snapshots and artifacts`) run on the hosted enterprise 32-core pools; the required verdict job that aggregates them (`all checks passed`) runs on standard `ubuntu-latest`. When the enterprise pools degrade — jobs queue indefinitely or the enterprise labels vanish — every open pull request becomes unmergeable, and the ordinary recovery of merging a fix is itself deadlocked behind the very required checks that cannot run. **Scope: this switch recovers an enterprise Linux-pool outage.** The verdict's other required dependencies (`node-compat`, `python-sdk`, `windows`) stay on standard hosted runners by design (the portable boundary); in a broader GitHub-hosted capacity failure that also takes out the standard pools, those dependencies still block `all checks passed`, and only the Windows leg has no in-house substitute at all — during the 2026-07-27 outage the standard pools recovered first, which is the ordering this design bets on. An outage therefore needs a switch any responder with repository write access can throw without merging anything. +The three required Linux worker jobs in [CI](../../../../.github/workflows/ci.yml) (`node 24 / static`, `node 24 / coverage`, `node 24 / snapshots and artifacts`) run on the hosted enterprise 32-core pools; the required verdict job that aggregates them (`all checks passed`) runs on standard `ubuntu-latest`; the independent native Windows job (`windows node 24 / native complete`) runs on the hosted `dsh-windows-2025-16core` larger runner. When the enterprise pools degrade — jobs queue indefinitely or the enterprise labels vanish — every open pull request becomes unmergeable, and the ordinary recovery of merging a fix is itself deadlocked behind the very required checks that cannot run. **Scope: this switch recovers an enterprise Linux-pool outage AND a hosted Windows-pool outage.** The verdict's other required dependencies (`node-compat`, `python-sdk`, `windows`) stay on standard hosted runners by design (the portable boundary); in a broader GitHub-hosted capacity failure that also takes out the standard pools, those dependencies still block `all checks passed`. An outage therefore needs a switch any responder with repository write access can throw without merging anything. ## Decision -Each of the three required Linux worker jobs — and the `all checks passed` verdict job, which would otherwise stay queued on the failed pool even after every worker passed — resolves its runner pool through the `DSH_CI_FAILOVER` repository variable. Unset (normal), they run on the hosted enterprise pools. Set to `selfhosted` by any repository writer, all four retarget onto the in-house self-hosted `vm-backup` pool, coverage and snapshot concurrency drop to shared-VM bounds, and the hosted-path pnpm cache restores are skipped. The switch is writer-manageable repository state, not a merge, so it works while every check is red. The in-house pool's readiness is continuously re-proven by the `serial / linux (self-hosted standby)` lane, which runs the complete unsharded aggregate on every master push. +Each of the three required Linux worker jobs, the independent native Windows job, and the `all checks passed` verdict job — which would otherwise stay queued on the failed pool even after every worker passed — resolves its runner pool through the `DSH_CI_FAILOVER` repository variable. Unset (normal), they run on the hosted enterprise pools. Set to `selfhosted` by any repository writer, all five retarget onto the in-house self-hosted pools: the Linux jobs and verdict onto the `vm-backup` pool, coverage and snapshot concurrency drop to shared-VM bounds, and the hosted-path pnpm cache restores are skipped; the native Windows job onto the `dsh-win-ci` pool. The switch is writer-manageable repository state, not a merge, so it works while every check is red. The in-house pools' readiness is continuously re-proven by the `serial / linux (self-hosted standby)` and `serial / windows (self-hosted standby)` lanes, which run the complete unsharded aggregates on every master push. ### What the in-house pool is `vm-backup`: one 64-core VM, six always-on systemd-managed runner instances. Its image must preinstall Playwright Chromium's Linux system packages; CI downloads the lockfile-selected browser but never runs `apt` on this persistent shared host. Check the latest `serial / linux (self-hosted standby)` run before switching: its aggregate includes browser replay, so a green standby verifies both ordinary capacity and this browser prerequisite. +#### Windows pool + +`dsh-win-ci`: 32 always-on runner instances (scheduled tasks `GH-Runner-01`…`GH-Runner-32`) on the in-house Windows CI server (one 96-core / 580 GB machine). Labels: `[self-hosted, dsh-win-ci, windows]`. The image must preinstall Node 24, pnpm, Git (with Git Bash on `PATH`, i.e. `C:\Program Files\Git\bin` — the `bash` tool spawns `bash` by name), PowerShell 7, and enable Developer Mode for symlink support. Check the latest `serial / windows (self-hosted standby)` run before switching: a green standby verifies the pool can execute `check:ci:windows-complete` end-to-end. + ### Switch (any repository writer, ~1 minute, no merge) 1. Repository **Settings → Secrets and variables → Actions → Variables → New repository variable**: name `DSH_CI_FAILOVER`, value `selfhosted`. 2. Retrigger the required jobs so they re-resolve their pool. Jobs already **queued** for the hosted labels do not retarget and cannot be re-run in place, so for the documented indefinite-queue outage, cancel the stuck run and re-run all jobs, or push a new commit; "Re-run failed jobs" only helps once a job has actually failed rather than queued. 3. That is the entire switch. Under failover the workflow also, automatically: drops `DSH_COVERAGE_MAX_WORKERS` to 8 and `DSH_SNAPSHOT_MAX_CONCURRENCY` to 12 (sized for six always-on instances: worst case 6 × 8 = 48 coverage workers on the 64-core VM) (shared-VM contention bounds), and skips the hosted-path pnpm cache restores (the VM's persistent store serves warm installs). -#**Dependabot exception.** All four selectors deliberately exclude `dependabot[bot]`: under failover, Dependabot PRs stay queued for the hosted pool rather than executing dependency-supplied code on the persistent VM. A Dependabot PR that remains queued during an outage is expected behavior, not a failed switch; it completes when the hosted pool recovers. +#**Dependabot exception.** All five selectors deliberately exclude `dependabot[bot]`: under failover, Dependabot PRs stay queued for the hosted pool rather than executing dependency-supplied code on the persistent VMs. A Dependabot PR that remains queued during an outage is expected behavior, not a failed switch; it completes when the hosted pool recovers. -**Who can flip the variable.** GitHub's API lets any collaborator with write access manage repository variables, so the switch is writer-level, not strictly admin-only. In this repository's trust model that is not an escalation: the runner group admits all workflows of this private, fork-disabled repository (a deliberate trade to make PR-ref failover possible at all), so any writer could already reach the VM by pushing a branch workflow. The boundary against untrusted code is repository membership; the variable only routes work for members. +**Who can flip the variable.** GitHub's API lets any collaborator with write access manage repository variables, so the switch is writer-level, not strictly admin-only. In this repository's trust model that is not an escalation: the runner groups admit all workflows of this private, fork-disabled repository (a deliberate trade to make PR-ref failover possible at all), so any writer could already reach the VMs by pushing a branch workflow. The boundary against untrusted code is repository membership; the variable only routes work for members. ## Capacity during failover diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md index 1ee679eabc..88a793e144 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md @@ -6,23 +6,27 @@ Status: implemented ## 问题 -[CI](../../../../.github/workflows/ci.yml) 中三个必需的 Linux 工作作业(`node 24 / static`、`node 24 / coverage`、`node 24 / snapshots and artifacts`)运行在托管的企业级 32 核池上;聚合它们的必需判定作业(`all checks passed`)运行在标准 `ubuntu-latest` 上。当企业池发生故障——作业无限排队或企业标签消失——所有开启的拉取请求都无法合并,而"合并一个修复"这一常规恢复手段本身正被那些无法运行的必需检查死锁。**适用范围:本切换恢复的是企业级 Linux 池故障。**判定作业的其余必需依赖(`node-compat`、`python-sdk`、`windows`)按设计留在标准托管运行器上(可移植边界);若更大范围的 GitHub 托管容量故障连标准池一并击倒,这些依赖仍会阻塞 `all checks passed`,且只有 Windows 这条腿完全没有自有替代——2026-07-27 的故障中标准池率先恢复,本设计押注的正是这一顺序。因此故障需要一个任何具备仓库写权限的响应者都能在不合并任何代码的情况下触发的开关。 +[CI](../../../../.github/workflows/ci.yml) 中三个必需的 Linux 工作作业(`node 24 / static`、`node 24 / coverage`、`node 24 / snapshots and artifacts`)运行在托管的企业级 32 核池上;聚合它们的必需判定作业(`all checks passed`)运行在标准 `ubuntu-latest` 上;独立的原生 Windows 作业(`windows node 24 / native complete`)运行在托管的 `dsh-windows-2025-16core` 大型运行器上。当企业池发生故障——作业无限排队或企业标签消失——所有开启的拉取请求都无法合并,而"合并一个修复"这一常规恢复手段本身正被那些无法运行的必需检查死锁。**适用范围:本切换恢复的是企业级 Linux 池故障与托管 Windows 池故障。**判定作业的其余必需依赖(`node-compat`、`python-sdk`、`windows`)按设计留在标准托管运行器上(可移植边界);若更大范围的 GitHub 托管容量故障连标准池一并击倒,这些依赖仍会阻塞 `all checks passed`。因此故障需要一个任何具备仓库写权限的响应者都能在不合并任何代码的情况下触发的开关。 ## 决策 -三个必需的 Linux 工作作业——以及 `all checks passed` 判定作业(若不随切换,即使全部工作作业通过,它仍会滞留在故障池的队列中)——各自通过仓库变量 `DSH_CI_FAILOVER` 解析运行器池。变量不存在(正常)时它们运行在托管企业池上;由任何具备写权限的协作者设为 `selfhosted` 时,四者全部切换到公司自有的自托管 `vm-backup` 池,覆盖率与快照的并发降到共享虚拟机上限,并跳过托管路径的 pnpm 缓存恢复。这个开关是写者可管理的仓库状态而非一次合并,因此在所有检查都是红色时仍然有效。自有池的就绪状态由 `serial / linux (self-hosted standby)` 通道持续验证——每次 master 推送都在其上运行完整的未分片聚合流程。 +三个必需的 Linux 工作作业、独立的原生 Windows 作业,以及 `all checks passed` 判定作业(若不随切换,即使全部工作作业通过,它仍会滞留在故障池的队列中)——各自通过仓库变量 `DSH_CI_FAILOVER` 解析运行器池。变量不存在(正常)时它们运行在托管企业池上;由任何具备写权限的协作者设为 `selfhosted` 时,五个作业全部切换到公司自有的自托管池:Linux 作业与判定作业切到 `vm-backup` 池,覆盖率与快照的并发降到共享虚拟机上限,并跳过托管路径的 pnpm 缓存恢复;原生 Windows 作业切到 `dsh-win-ci` 池。这个开关是写者可管理的仓库状态而非一次合并,因此在所有检查都是红色时仍然有效。自有池的就绪状态由 `serial / linux (self-hosted standby)` 与 `serial / windows (self-hosted standby)` 通道持续验证——每次 master 推送都在其上运行完整的未分片聚合流程。 ### 自有池是什么 `vm-backup`:一台 64 核虚拟机,6 个常驻 systemd 管理的运行器实例。其镜像必须预装 Playwright Chromium 的 Linux 系统软件包;CI 会下载锁文件选定的浏览器,但绝不在这台持久化共享主机上运行 `apt`。切换前先看 `serial / linux (self-hosted standby)` 最近一次运行:其聚合流程包含浏览器回放,因此绿色热备同时验证常规容量和这项浏览器先决条件。 +#### Windows 池 + +`dsh-win-ci`:公司内部 Windows CI 服务器(一台 96 核 / 580 GB 机器)上 32 个常驻运行器实例(计划任务 `GH-Runner-01`…`GH-Runner-32`)。标签:`[self-hosted, dsh-win-ci, windows]`。镜像必须预装 Node 24、pnpm、Git(Git Bash 在 `PATH` 上,即 `C:\Program Files\Git\bin`——`bash` 工具按名称 spawn `bash`)、PowerShell 7,并为符号链接支持启用开发人员模式。切换前先看 `serial / windows (self-hosted standby)` 最近一次运行:绿色热备验证该池能端到端执行 `check:ci:windows-complete`。 + ### 切换步骤(任何具备写权限的协作者,约 1 分钟,无需合并) 1. 仓库 **Settings → Secrets and variables → Actions → Variables → New repository variable**:名称 `DSH_CI_FAILOVER`,值 `selfhosted`。 2. 重新触发必需作业,使其重新解析运行器池。已经为托管标签**排队**的作业不会重定向,也无法原地 re-run,因此对于本手册所述的无限排队故障,应取消卡住的运行并 re-run all jobs,或推送一个新提交;“Re-run failed jobs”只有在作业真正失败(而非仍在排队)时才有用。 3. 切换到此完成。故障切换状态下工作流还会自动:把 `DSH_COVERAGE_MAX_WORKERS` 降为 8、`DSH_SNAPSHOT_MAX_CONCURRENCY` 降为 12(按 6 个常驻实例定容:最坏情况下,6 × 8 = 48 个覆盖率工作进程运行在 64 核虚拟机上)(共享虚拟机的争抢上限),并跳过托管路径的 pnpm 缓存恢复(虚拟机的持久 store 直接提供热安装)。 -#**Dependabot 例外。**四个选择器都刻意排除了 `dependabot[bot]`:故障切换期间,Dependabot 拉取请求继续在托管池排队,而不是把依赖项提供的代码放到持久化虚拟机上执行。故障期间 Dependabot PR 持续排队是预期行为而非切换失败;托管池恢复后它会自行完成。 +#**Dependabot 例外。**五个选择器都刻意排除了 `dependabot[bot]`:故障切换期间,Dependabot 拉取请求继续在托管池排队,而不是把依赖项提供的代码放到持久化虚拟机上执行。故障期间 Dependabot PR 持续排队是预期行为而非切换失败;托管池恢复后它会自行完成。 **谁能扳动这个变量。**GitHub 的 API 允许任何具有写权限的协作者管理仓库变量,因此该开关实际是写者级而非严格的管理员级。在本仓库的信任模型下这并不构成升权:runner group 接纳本私有、禁 fork 仓库的全部工作流(这是让 PR 引用的故障切换得以成立的刻意取舍),因此任何写者本就可以通过推送分支工作流触达这台虚拟机。抵御不可信代码的边界是仓库成员资格;变量只是为成员路由工作。 From f7745a35e1cd6489c66b61f3efbc25a82920854c Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Tue, 11 Aug 2026 12:25:38 +0800 Subject: [PATCH 10/10] test(subagent-codex): use async rm for real-product temp cleanup Switch the afterEach cleanup from rmSync to the async rm with 10 retries, matching subagent-claude-code. On native Windows the synchronous rmSync retry window (5 x 100ms) is too short for the real Codex subprocess to release its directory handles after exit, so cleanup repeatedly fails with EPERM; the async variant with a wider retry window drains the handles before removing the tree. --- packages/subagent/subagent-codex/tests/real-product.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/subagent/subagent-codex/tests/real-product.spec.ts b/packages/subagent/subagent-codex/tests/real-product.spec.ts index ef618aaef0..2aaed86bee 100644 --- a/packages/subagent/subagent-codex/tests/real-product.spec.ts +++ b/packages/subagent/subagent-codex/tests/real-product.spec.ts @@ -4,9 +4,9 @@ import { mkdirSync, mkdtempSync, readFileSync, - rmSync, writeFileSync, } from 'node:fs' +import { rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { delimiter, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' @@ -41,7 +41,7 @@ afterEach(async () => { await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose())) await Promise.all(fixtures.splice(0).map(fixture => fixture.close())) for (const root of roots.splice(0)) { - rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }) + await rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }) } })