ci: split pull-request CI into Linux, Windows, and Python workflows (#6898)

* ci: run native Windows PR jobs in a separate workflow

GitHub re-runs failed jobs only after a whole run finishes, so slow Windows jobs blocked re-running failed Linux jobs. CI Windows now owns the native Windows jobs and the Windows x64 Python runtime target, with its own required 'windows checks passed' verdict.

Refs #6893

* ci: run Python PR jobs in a separate workflow

Python rarely fails while the Windows lanes run long, so the Python SDK suite and the Linux/Windows x64 runtime matrix move into CI Python. The three pull-request workflows can now be re-run without waiting for each other, and branch protection gains "python checks passed" beside the existing verdicts.

Refs #6893

* docs: record the three PR verdicts in the failover runbook

* test: pin the shared environment across the PR workflows

* test: read the python SDK lane from CI Python

* test(sdk): parse session JSONL frames in the keyless smoke

Node 24.19 returned only the first frame of a concatenated zstd log, so the single JSON.parse saw just the header; 24.22 returns every frame and the JSONL body broke the parse. Read frames explicitly and select the main session by its header so both versions agree.
This commit is contained in:
imccyu authored and GitHub committed 2026-10-22 04:09:35 +00:00
1 parent 1453c0ff86
commit 1276a8e13a
33 files changed
+561 -362

No files matched your search

@@ -9,8 +9,8 @@
en: 4fb1c27a631b32d3
zh: 9dbe9a4b9aadfca9
/agent-note-serial-cross-platform-ci-reference/decision:
en: c88f606c0749bd67
zh: b2038b0d92fd8890
en: b78b36281749773b
zh: c4ed34782577e5e7
/agent-note-serial-cross-platform-ci-reference/alternatives-considered:
en: 3610152c5c4e93af
zh: f4c09d327716f750
@@ -16,7 +16,7 @@ Real-kernel sandbox proofs require specific hosted operating systems and archite
## Decision
[CI](../../../../.github/workflows/ci.yml) (pull-request-only) and [CI master](../../../../.github/workflows/ci-master.yml) (master-push + workflow_dispatch) give pull-request and master-push events complementary responsibilities. Pull requests run Linux, native Windows, [Node compatibility with self-hosted routing under the Linux variable and trust conditions](../../../../.github/workflows/ci.yml), and Python checks; [platform scheduling](../../../../.github/workflows/ci-master.yml) assigns Wine and three Python runtime carriers to master pushes. 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). There is no standard-hosted `serial / linux` definition; the standard-hosted `serial / macos` remains disabled under `TODO(hosted-serial-ci)` until its portable capacity can be restored. The current `serial / windows` definition is the in-house `dsh-win-ci` standby. 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), [CI Windows](../../../../.github/workflows/ci-windows.yml), and [CI Python](../../../../.github/workflows/ci-python.yml) (pull-request-only) and [CI master](../../../../.github/workflows/ci-master.yml) (master-push + workflow_dispatch) give pull-request and master-push events complementary responsibilities. Pull requests run Linux, native Windows, [Node compatibility with self-hosted routing under the Linux variable and trust conditions](../../../../.github/workflows/ci.yml), and Python checks; [platform scheduling](../../../../.github/workflows/ci-master.yml) assigns Wine and three Python runtime carriers to master pushes. 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). There is no standard-hosted `serial / linux` definition; the standard-hosted `serial / macos` remains disabled under `TODO(hosted-serial-ci)` until its portable capacity can be restored. The current `serial / windows` definition is the in-house `dsh-win-ci` standby. 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.
@@ -16,7 +16,7 @@ Status: implemented
## 决策
[CI](../../../../.github/workflows/ci.yml)(仅 pull request)与 [CI master](../../../../.github/workflows/ci-master.yml)(master 推送 + `workflow_dispatch`)为拉取请求事件与 master 推送事件赋予互补的职责。拉取请求运行 Linux、原生 Windows、[仅在 Linux 变量与信任条件满足时使用自托管路由的 Node 兼容性](../../../../.github/workflows/ci.yml)与 Python 检查;[平台调度](../../../../.github/workflows/ci-master.yml)将 Wine 与三个 Python 运行时载体分配给 master 推送。向 `master` 推送时,当前启用的参考作业是公司自有 `vm-backup` 池上的 `serial / linux (self-hosted standby)` 和 `dsh-win-ci` 池上的 `serial / windows (self-hosted standby)`——这些热备演练持续验证[故障切换手册](2026-07-26-ci-failover-runbook.zh.md)所描述的切换目标。不存在标准托管的 `serial / linux` 定义;标准托管的 `serial / macos` 仍处于禁用状态,并由 `TODO(hosted-serial-ci)` 标记,直到其可移植容量恢复。当前 `serial / windows` 定义是公司自有 `dsh-win-ci` 池的 standby。各自独立的作业定义有意显式保留简短的代码检出、运行时设置和依赖锁定的安装步骤,而不是用矩阵或可复用工作流隐藏操作系统差异。`workflow_dispatch` 仅用于运行器基准测试。
[CI](../../../../.github/workflows/ci.yml)、[CI Windows](../../../../.github/workflows/ci-windows.yml) 与 [CI Python](../../../../.github/workflows/ci-python.yml)(仅 pull request)与 [CI master](../../../../.github/workflows/ci-master.yml)(master 推送 + `workflow_dispatch`)为拉取请求事件与 master 推送事件赋予互补的职责。拉取请求运行 Linux、原生 Windows、[仅在 Linux 变量与信任条件满足时使用自托管路由的 Node 兼容性](../../../../.github/workflows/ci.yml)与 Python 检查;[平台调度](../../../../.github/workflows/ci-master.yml)将 Wine 与三个 Python 运行时载体分配给 master 推送。向 `master` 推送时,当前启用的参考作业是公司自有 `vm-backup` 池上的 `serial / linux (self-hosted standby)` 和 `dsh-win-ci` 池上的 `serial / windows (self-hosted standby)`——这些热备演练持续验证[故障切换手册](2026-07-26-ci-failover-runbook.zh.md)所描述的切换目标。不存在标准托管的 `serial / linux` 定义;标准托管的 `serial / macos` 仍处于禁用状态,并由 `TODO(hosted-serial-ci)` 标记,直到其可移植容量恢复。当前 `serial / windows` 定义是公司自有 `dsh-win-ci` 池的 standby。各自独立的作业定义有意显式保留简短的代码检出、运行时设置和依赖锁定的安装步骤,而不是用矩阵或可复用工作流隐藏操作系统差异。`workflow_dispatch` 仅用于运行器基准测试。
每个参考作业均在不设置任何分片选择器的情况下运行 `pnpm run check:ci`。`DSH_GATE_CONCURRENCY=1` 使顶层聚合每次只执行一个已经就绪的门禁;覆盖率、快照回放、built-bin 冒烟测试和发布验证的 worker 数量也设为 1。各参考作业可以彼此并行,但每台主机上的仓库门禁都串行运行且完整执行。Linux 在回放快照前安装 bubblewrap,Windows 则在安装采用符号链接的工作区前启用开发人员模式。
@@ -6,11 +6,11 @@
en: 677b522817e84e2f
zh: f8518a7033b0fd54
/agent-note-ci-failover-runbook-hosted-pools-in-house-pool/problem:
en: 3ecedc74cceced3b
zh: f4708f1887d42c61
en: bcdd24f3c39d593c
zh: 3e576d09c8c3bfd4
/agent-note-ci-failover-runbook-hosted-pools-in-house-pool/decision:
en: 4a76615718dbc8f0
zh: f46b885a11d5dd1d
en: fed13ca92e488b69
zh: 9e5c9cdc118ab84e
/agent-note-ci-failover-runbook-hosted-pools-in-house-pool/decision/release-rehearsals-share-the-linux-switch:
en: 01e74068e0a96375
zh: aa811861ce30cb47
@@ -6,11 +6,11 @@ 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 [native Windows jobs](2026-08-08-native-windows-pull-request-ci.md) run 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: two independent switches, one per platform.** `DSH_CI_FAILOVER_LINUX` recovers an enterprise Linux-pool outage (the three required Linux workers plus the `all checks passed` verdict); `DSH_CI_FAILOVER_WINDOWS` recovers a hosted Windows-pool outage (the native Windows jobs). A Linux-pool outage need not retarget Windows jobs and vice versa. The [Node compatibility jobs](../../../../.github/workflows/ci.yml) also follow the Linux switch with isolated setup; the verdict's `node-24-bench`, `python-sdk`, and `python-runtime` dependencies stay on standard hosted runners; 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`; the [native Windows jobs](2026-08-08-native-windows-pull-request-ci.md) in [CI Windows](../../../../.github/workflows/ci-windows.yml) run 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: two independent switches, one per platform.** `DSH_CI_FAILOVER_LINUX` recovers an enterprise Linux-pool outage (the three required Linux workers plus the `all checks passed`, `windows checks passed`, and `python checks passed` verdicts); `DSH_CI_FAILOVER_WINDOWS` recovers a hosted Windows-pool outage (the native Windows jobs). A Linux-pool outage need not retarget Windows jobs and vice versa. The [Node compatibility jobs](../../../../.github/workflows/ci.yml) also follow the Linux switch with isolated setup; the verdict's `node-24-bench`, `python-sdk`, and `python-runtime` dependencies stay on standard hosted runners, as does CI Python's `python-runtime`; in a broader GitHub-hosted capacity failure that also takes out the standard pools, those dependencies still block the verdicts that require them. An outage therefore needs a switch any responder with repository write access can throw without merging anything.
## Decision
The three primary Linux jobs (`node-24`, `node-24-coverage`, `node-24-consumers`), the two `node-compat` matrix entries, and `all-checks-passed` resolve through `DSH_CI_FAILOVER_LINUX`; the native Windows jobs resolve through `DSH_CI_FAILOVER_WINDOWS`. A platform switch does not redirect the other platform. Set to `selfhosted` by a repository writer, the applicable trusted jobs select `vm-backup` or `dsh-win-ci`; the `blacksmith` value routes the participating jobs per the [blacksmith failover leg note](2026-09-09-blacksmith-failover-leg.md); unset or any other value retains the workflow-defined hosted fallbacks. Node compatibility jobs require a same-repository, non-fork head and a non-Dependabot author, use isolated runtime setup, and retain the `ubuntu-latest` fallback under unset and non-special values; the blacksmith branch carries none of those predicates. Under the `selfhosted` value, Linux failover bounds snapshot concurrency and skips hosted package-cache restores. The verdict follows its workers so it does not remain queued on an unavailable hosted pool. Each switch is writer-manageable repository state, not a merge, so it works while checks are red. The `serial / linux (self-hosted standby)` and `serial / windows (self-hosted standby)` lanes re-prove the complete unsharded aggregates on master pushes.
The three primary Linux jobs (`node-24`, `node-24-coverage`, `node-24-consumers`), the two `node-compat` matrix entries, and the `all-checks-passed`, `windows-checks-passed`, and `python-checks-passed` verdicts resolve through `DSH_CI_FAILOVER_LINUX`; the native Windows jobs resolve through `DSH_CI_FAILOVER_WINDOWS`. A platform switch does not redirect the other platform. Set to `selfhosted` by a repository writer, the applicable trusted jobs select `vm-backup` or `dsh-win-ci`; the `blacksmith` value routes the participating jobs per the [blacksmith failover leg note](2026-09-09-blacksmith-failover-leg.md); unset or any other value retains the workflow-defined hosted fallbacks. Node compatibility jobs require a same-repository, non-fork head and a non-Dependabot author, use isolated runtime setup, and retain the `ubuntu-latest` fallback under unset and non-special values; the blacksmith branch carries none of those predicates. Under the `selfhosted` value, Linux failover bounds snapshot concurrency and skips hosted package-cache restores. The three verdict jobs follow the Linux switch so they do not remain queued on an unavailable hosted pool. Each switch is writer-manageable repository state, not a merge, so it works while checks are red. The `serial / linux (self-hosted standby)` and `serial / windows (self-hosted standby)` lanes re-prove the complete unsharded aggregates on master pushes.
The [superseded-CI cancellation policy](../../../../.github/workflows/ci.yml) governs master pushes and manual runs in the same workflow/ref group, including standby drills. Rapid master updates can starve a drill before it reaches a verdict. Use the latest completed standby verdict and check its age and commit before treating it as readiness evidence; a cancelled or merely scheduled run is not proof of readiness.
@@ -6,11 +6,11 @@ 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 作业](2026-08-08-native-windows-pull-request-ci.zh.md)运行在托管的 `dsh-windows-2025-16core` 大型运行器上。当企业池发生故障——作业无限排队或企业标签消失——所有开启的拉取请求都无法合并,而"合并一个修复"这一常规恢复手段本身正被那些无法运行的必需检查死锁。**适用范围:两个独立开关,每个平台一个。**`DSH_CI_FAILOVER_LINUX` 恢复企业级 Linux 池故障(三个必需的 Linux 工作作业加 `all checks passed` 判定作业);`DSH_CI_FAILOVER_WINDOWS` 恢复托管 Windows 池故障(原生 Windows 作业)。Linux 池故障无需重定向 Windows 作业,反之亦然。[Node 兼容性作业](../../../../.github/workflows/ci.yml)也通过隔离设置跟随 Linux 开关;判定作业的 `node-24-bench`、`python-sdk` 和 `python-runtime` 依赖仍留在标准托管运行器上;若更大范围的 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` 上;[原生 Windows 作业](2026-08-08-native-windows-pull-request-ci.zh.md)(位于 [CI Windows](../../../../.github/workflows/ci-windows.yml))运行在托管的 `dsh-windows-2025-16core` 大型运行器上。当企业池发生故障——作业无限排队或企业标签消失——所有开启的拉取请求都无法合并,而"合并一个修复"这一常规恢复手段本身正被那些无法运行的必需检查死锁。**适用范围:两个独立开关,每个平台一个。**`DSH_CI_FAILOVER_LINUX` 恢复企业级 Linux 池故障(三个必需的 Linux 工作作业加 `all checks passed`、`windows checks passed` 与 `python checks passed` 判定作业);`DSH_CI_FAILOVER_WINDOWS` 恢复托管 Windows 池故障(原生 Windows 作业)。Linux 池故障无需重定向 Windows 作业,反之亦然。[Node 兼容性作业](../../../../.github/workflows/ci.yml)也通过隔离设置跟随 Linux 开关;判定作业的 `node-24-bench`、`python-sdk` 和 `python-runtime` 依赖仍留在标准托管运行器上,CI Python 的 `python-runtime` 也是如此;若更大范围的 GitHub 托管容量故障连标准池一并击倒,这些依赖仍会阻塞要求它们的判定。因此故障需要一个任何具备仓库写权限的响应者都能在不合并任何代码的情况下触发的开关。
## 决策
三个主要 Linux 作业(`node-24`、`node-24-coverage`、`node-24-consumers`)、两个 `node-compat` 矩阵条目和 `all-checks-passed` 通过 `DSH_CI_FAILOVER_LINUX` 解析;原生 Windows 作业通过 `DSH_CI_FAILOVER_WINDOWS` 解析。一个平台的开关不会重定向另一个平台。仓库写者将变量设为 `selfhosted` 时,适用的可信作业选择 `vm-backup` 或 `dsh-win-ci`;`blacksmith` 取值按 [blacksmith 故障切换支路笔记](2026-09-09-blacksmith-failover-leg.zh.md) 路由参与切换的作业;未设置或任何其它值保留工作流定义的托管回退。Node 兼容性作业要求同仓库且非 fork 的头部以及非 Dependabot 作者,使用隔离运行时设置,并在未设置与非特殊值下保留 `ubuntu-latest` 回退;blacksmith 分支不带上述任何条件。在 `selfhosted` 取值下,Linux 故障切换会限制快照并发,并跳过托管软件包缓存恢复。判定作业跟随工作作业,避免继续在不可用的托管池排队。每个开关都是写者可管理的仓库状态而非一次合并,因此在检查失败时仍然有效。`serial / linux (self-hosted standby)` 与 `serial / windows (self-hosted standby)` 通道在 master 推送上重新验证完整的未分片聚合流程。
三个主要 Linux 作业(`node-24`、`node-24-coverage`、`node-24-consumers`)、两个 `node-compat` 矩阵条目,以及 `all-checks-passed`、`windows-checks-passed`、`python-checks-passed` 三个判定作业通过 `DSH_CI_FAILOVER_LINUX` 解析;原生 Windows 作业通过 `DSH_CI_FAILOVER_WINDOWS` 解析。一个平台的开关不会重定向另一个平台。仓库写者将变量设为 `selfhosted` 时,适用的可信作业选择 `vm-backup` 或 `dsh-win-ci`;`blacksmith` 取值按 [blacksmith 故障切换支路笔记](2026-09-09-blacksmith-failover-leg.zh.md) 路由参与切换的作业;未设置或任何其它值保留工作流定义的托管回退。Node 兼容性作业要求同仓库且非 fork 的头部以及非 Dependabot 作者,使用隔离运行时设置,并在未设置与非特殊值下保留 `ubuntu-latest` 回退;blacksmith 分支不带上述任何条件。在 `selfhosted` 取值下,Linux 故障切换会限制快照并发,并跳过托管软件包缓存恢复。三个判定作业跟随 Linux 开关,避免继续在不可用的托管池排队。每个开关都是写者可管理的仓库状态而非一次合并,因此在检查失败时仍然有效。`serial / linux (self-hosted standby)` 与 `serial / windows (self-hosted standby)` 通道在 master 推送上重新验证完整的未分片聚合流程。
[被取代 CI 的取消策略](../../../../.github/workflows/ci.yml) 管理同一工作流/引用组内的 master 推送和手动运行,包括热备演练。master 快速更新可能让演练因反复被取消而始终无法得出结论。判断就绪状态时,使用最近一次已完成的热备结论,并核对其时间和提交;已取消或仅被调度的运行不构成就绪证据。
@@ -9,11 +9,11 @@
en: 3e229ecf646af866
zh: d358507b1e1d381f
/agent-note-wine-and-native-windows-ci/decision:
en: 5a74ef2d98f6b4cf
zh: 4a539303a3cd4ed8
en: 1125b44a170f70fb
zh: 37838b9da2003651
/agent-note-wine-and-native-windows-ci/alternatives-considered:
en: f6c5e77fe3f05739
zh: 8de640d12d3a38c4
en: 0e9a322cfc38d313
zh: f7bdeb86ba4fea1a
/agent-note-wine-and-native-windows-ci/consequences:
en: 58069228d4e904c6
zh: 2601a48b82400903
en: 3dc1ae32e0a74331
zh: ab0e9d00aad92d62
@@ -14,9 +14,9 @@ A coverage audit found that stale branch state had restored temporary exclusions
The master-only `windows` job in [ci-master.yml](../../../../.github/workflows/ci-master.yml) runs `windows node 24 / wine` on `ubuntu-latest`. It retains the checksum-verified Windows Node, Wine apt and pnpm caches, a hoisted install confined to a workspace snapshot, and the [shared Wine gate script](../../../../scripts/wine-windows-gates.sh) that runs the workspace build and production site. Node distribution transfers use bounded retries; when nodejs.org stalls on the large archive, a range-capable transport mirror resumes the same bytes, but nodejs.org remains the version and SHA-256 authority and the archive is never promoted before that checksum passes. Wine is outside the PR aggregate under the [master-only platform policy](../../../../.github/workflows/ci-master.yml). The [archived Wine experiment](../../archived/process/2026-07-27-wine-windows-gates-experiment.md) preserves its measured trade-offs, while this note owns the current dual topology.
Every pull request starts three native jobs: `windows-build`, `windows-coverage`, and `windows-native-tests`. Each job enables Developer Mode for workspace symlinks, provisions the repository-pinned pnpm through `pnpm/action-setup`, performs an immutable install without a transferred store archive, and runs its inventory under native PowerShell. The Windows failover variable (`DSH_CI_FAILOVER_WINDOWS`) selects the organization-owned `dsh-windows-2025-16core` runner by default, the in-house pool under `selfhosted`, or Blacksmith runners under `blacksmith` (see the [blacksmith failover leg note](2026-09-09-blacksmith-failover-leg.md)). Blacksmith build and coverage jobs request 16 vCPU; file-serial native tests request 2 vCPU. Per-job deadlines range from 60 to 120 minutes.
Every pull request starts three native jobs in the separate [CI Windows workflow](../../../../.github/workflows/ci-windows.yml): `windows-build`, `windows-coverage`, and `windows-native-tests`. GitHub re-runs failed jobs only after every job of a run finishes, so a separate workflow lets maintainers re-run a failed Linux job while Windows jobs are still running, and the reverse. Each job enables Developer Mode for workspace symlinks, provisions the repository-pinned pnpm through `pnpm/action-setup`, performs an immutable install without a transferred store archive, and runs its inventory under native PowerShell. The Windows failover variable (`DSH_CI_FAILOVER_WINDOWS`) selects the organization-owned `dsh-windows-2025-16core` runner by default, the in-house pool under `selfhosted`, or Blacksmith runners under `blacksmith` (see the [blacksmith failover leg note](2026-09-09-blacksmith-failover-leg.md)). Blacksmith build and coverage jobs request 16 vCPU; file-serial native tests request 2 vCPU. Per-job deadlines range from 60 to 120 minutes.
`windows-build` and `windows-native-tests` are dependencies of `all checks passed`; their workspace-build and targeted native-process results are blocking. After its blocking build and production-site checks succeed, `windows-build` runs `check:ci:windows-observational-ready` in the same workspace. This step uses `continue-on-error`, disables fail-fast, and has a 15-minute timeout; failures produce a workflow warning because Linux owns the blocking static, documentation, package, and built-artifact verdicts. The required verdict waits for the step to finish but does not require its diagnostics to pass. `windows-coverage` remains outside aggregate `needs`, so its 100%-per-file result stays red and visible without delaying the verdict.
`windows-build` and `windows-native-tests` are dependencies of the required `windows checks passed` verdict; their workspace-build and targeted native-process results are blocking. After its blocking build and production-site checks succeed, `windows-build` runs `check:ci:windows-observational-ready` in the same workspace. This step uses `continue-on-error`, disables fail-fast, and has a 15-minute timeout; failures produce a workflow warning because Linux owns the blocking static, documentation, package, and built-artifact verdicts. The required verdict waits for the step to finish but does not require its diagnostics to pass. `windows-coverage` remains outside aggregate `needs`, so its 100%-per-file result stays red and visible without delaying the verdict.
`windows-coverage` runs [in-job partitioned coverage](../../../../scripts/coverage-partitions.ts) without a preceding build, matching the Linux coverage lane: four single-worker instrumented shards beside a two-worker exempt-heavy gate; workspace imports resolve to `src` through the tsconfig paths map, and the lib-consuming suites self-skip on unbuilt checkouts. Both coverage gates raise Vitest's per-test, expect.poll, and hook budgets through `DSH_COVERAGE_TEST_TIMEOUT_MS=90000`. The observational step reuses the successful build, retains the distinct MPA documentation build, starts independent static gates together, and uses CI worker defaults for `publint`, with an eight-worker override on shared self-hosted runners. Its built-bin smoke starts only after every other observational gate settles, including failed gates. The ready aggregate requires a successful prior workspace build; it removes only that build and its internal dependency edges from the shared internal inventory. This keeps bounded real-application startup measurements from competing with tool-catalog, NodeNext, package, and documentation processes. The script-only translation-pairing merge suite runs in the exempt-heavy gate because it imports only `scripts/` sources and child processes; V8 instrumentation contributes no threshold coverage there but magnifies Git-process latency. Lefthook concurrency fixtures retain their outcomes with 30-second case budgets and a 10-second process-ready probe, while the installer allows five seconds for a preempted lock owner to publish its record after exclusive creation. Directory-picker composition gives its debounced config write an explicit 15-second poll budget; workspace-context composition fixtures use a test-owned signal without an unrelated one-second deadline. The LSP sources and the ACL-sandbox sources remain in the Windows denominator: stub-based failure-path suites carry every in-process ACL-sandbox file to 100%, and only the runner entry stays excluded — it executes exclusively as a spawned child outside the instrumented run, its behavior pinned end-to-end by the runner suite. Narrow annotated V8 ignores cover only unreachable branches (peer-platform arms and lifecycle-unreachable guards), with their behavior tests retained on the owning platform.
@@ -36,7 +36,7 @@ Shiki disables lazy TextMate-regex compilation and warms each boot grammar befor
## Alternatives considered
**Make every native Windows result a dependency of `all checks passed`.** This gives the aggregate the highest-fidelity Windows verdict, but makes every merge wait for coverage and the duplicated observational inventory. Requiring the build and targeted native-process suite retains fast native correctness signals while the other results remain automatic.
**Make every native Windows result a dependency of `windows checks passed`.** This gives the aggregate the highest-fidelity Windows verdict, but makes every merge wait for coverage and the duplicated observational inventory. Requiring the build and targeted native-process suite retains fast native correctness signals while the other results remain automatic.
**Run only Wine on pull requests.** Wine reaches blocking win32 toolchain branches quickly, but can report green while a real NT, NTFS, PowerShell, process, or addon contract is broken.
@@ -50,7 +50,7 @@ Shiki disables lazy TextMate-regex compilation and warms each boot grammar befor
## Consequences
Wine provides post-merge toolchain evidence. Native coverage may still be pending or red when `all checks passed` turns green. Observational diagnostics finish inside the required build job. A diagnostic failure leaves the PR check successful; the failed step, workflow warning, and run summary expose the failure.
Wine provides post-merge toolchain evidence. Native coverage may still be pending or red when `windows checks passed` turns green. Observational diagnostics finish inside the required build job. A diagnostic failure leaves the PR check successful; the failed step, workflow warning, and run summary expose the failure.
Every pull request receives a real NT kernel, NTFS, PowerShell, Windows process, native addon, and supported-source coverage signal. Observational checks reuse the successful build workspace, avoiding a second checkout, dependency installation, and workspace build. Their sequential position extends the build job and can delay the required verdict; it also separates diagnostic processes from compilation. The ready and master aggregates share one internal diagnostic inventory; master retains its own workspace build and full checks.
@@ -14,9 +14,9 @@ Wine 在 Linux 内核与区分大小写的 ext4 之上采用 hoisted 依赖布
[ci-master.yml](../../../../.github/workflows/ci-master.yml) 中仅 master 触发的 `windows` 作业在 `ubuntu-latest` 上运行 `windows node 24 / wine`。它保留经过校验和验证的 Windows Node、Wine apt 与 pnpm 缓存、仅限工作区快照的 hoisted 安装,以及运行工作区构建与生产网站的[共享 Wine 门禁脚本](../../../../scripts/wine-windows-gates.sh)。Node 分发文件传输采用有界重试;nodejs.org 的大文件传输停滞时,由支持范围请求的传输镜像续传相同字节,但版本和 SHA-256 权威仍属于 nodejs.org,归档通过该校验前绝不会投入使用。根据[仅 master 平台策略](../../../../.github/workflows/ci-master.yml),Wine 不参与 PR 聚合。[已归档的 Wine 实验](../../archived/process/2026-07-27-wine-windows-gates-experiment.md)保留其实测取舍,而本文负责当前双通道拓扑。
每个拉取请求都会启动三个原生作业:`windows-build`、`windows-coverage` 和 `windows-native-tests`。每个作业都会为 workspace 符号链接启用开发者模式,通过 `pnpm/action-setup` 提供仓库固定版本的 pnpm,在不传输 store 归档的情况下执行不可变安装,并在原生 PowerShell 下运行自己的清单。Windows 故障切换变量(`DSH_CI_FAILOVER_WINDOWS`)默认选择组织自有的 `dsh-windows-2025-16core` 运行器,在 `selfhosted` 下选择公司内部运行器池,在 `blacksmith` 下选择 Blacksmith 运行器(见 [blacksmith 故障切换支路笔记](2026-09-09-blacksmith-failover-leg.zh.md))。Blacksmith 构建和覆盖率作业请求 16 vCPU;逐文件串行的原生测试请求 2 vCPU。各作业的截止时间为 60 至 120 分钟。
每个拉取请求都会在独立的 [CI Windows 工作流](../../../../.github/workflows/ci-windows.yml)中启动三个原生作业:`windows-build`、`windows-coverage` 和 `windows-native-tests`。GitHub 只有在一次运行的所有作业结束后才会重新运行失败的作业,因此独立工作流让维护者能在 Windows 作业仍在运行时重新运行失败的 Linux 作业,反之亦然。每个作业都会为 workspace 符号链接启用开发者模式,通过 `pnpm/action-setup` 提供仓库固定版本的 pnpm,在不传输 store 归档的情况下执行不可变安装,并在原生 PowerShell 下运行自己的清单。Windows 故障切换变量(`DSH_CI_FAILOVER_WINDOWS`)默认选择组织自有的 `dsh-windows-2025-16core` 运行器,在 `selfhosted` 下选择公司内部运行器池,在 `blacksmith` 下选择 Blacksmith 运行器(见 [blacksmith 故障切换支路笔记](2026-09-09-blacksmith-failover-leg.zh.md))。Blacksmith 构建和覆盖率作业请求 16 vCPU;逐文件串行的原生测试请求 2 vCPU。各作业的截止时间为 60 至 120 分钟。
`windows-build` 与 `windows-native-tests` 是 `all checks passed` 的依赖项;其工作区构建和定向原生进程结果具有阻断性。阻断性构建和生产网站检查成功后,`windows-build` 在同一工作区运行 `check:ci:windows-observational-ready`。该步骤使用 `continue-on-error`、关闭 fail-fast,并设置 15 分钟超时;失败时会产生工作流警告,因为静态检查、文档、包与构建产物的阻断性判定由 Linux 负责。必需判定会等待该步骤结束,但不要求其诊断通过。`windows-coverage` 仍在聚合流程的 `needs` 之外,因此逐文件 100% 覆盖率结果会保持红灯并可见,却不会延迟判定。
`windows-build` 与 `windows-native-tests` 是必需判定 `windows checks passed` 的依赖项;其工作区构建和定向原生进程结果具有阻断性。阻断性构建和生产网站检查成功后,`windows-build` 在同一工作区运行 `check:ci:windows-observational-ready`。该步骤使用 `continue-on-error`、关闭 fail-fast,并设置 15 分钟超时;失败时会产生工作流警告,因为静态检查、文档、包与构建产物的阻断性判定由 Linux 负责。必需判定会等待该步骤结束,但不要求其诊断通过。`windows-coverage` 仍在聚合流程的 `needs` 之外,因此逐文件 100% 覆盖率结果会保持红灯并可见,却不会延迟判定。
`windows-coverage` 与 Linux 覆盖率通道一致,不先构建工作区就运行[job 内分区覆盖率](../../../../scripts/coverage-partitions.ts):4 个单 worker 插桩分片与一个双 worker 的豁免重型门禁并行运行;工作区导入通过 tsconfig paths 映射解析到 `src`,消费构建产物的套件在未构建的检出上会自跳。两项覆盖率门禁都通过 `DSH_COVERAGE_TEST_TIMEOUT_MS=90000` 提高 Vitest 的单测试、expect.poll 与 hook 预算。观测步骤复用成功的构建,保留独立的 MPA 文档构建,一起启动相互独立的静态门禁,并让 `publint` 使用 CI worker 默认值,在共享自托管运行器上覆盖为 8 个 worker。其 built-bin 冒烟测试只在其他所有观测性门禁结算后启动,包括失败的门禁。ready 聚合要求此前已成功构建工作区;它仅从内部共用清单中移除该构建及其内部依赖边。这可避免有界的真实应用启动测量与 tool-catalog、NodeNext、包及文档进程争抢资源。translation-pairing 合并套件只导入 `scripts/` 源码和子进程,因此放入豁免重型套件门禁;V8 插桩不会为它贡献任何阈值覆盖率,却会放大 Git 进程延迟。Lefthook 并发 fixture 保留原有结果,采用 30 秒单用例预算与 10 秒进程就绪探测;安装器则允许被抢占的 lock 持有者在独占创建后用 5 秒发布记录。directory-picker 组合为防抖配置写入提供显式的 15 秒轮询预算;workspace-context 组合 fixture 使用测试自有、没有无关 1 秒截止时间的信号。LSP 源码与 ACL 沙箱源码仍计入 Windows 分母:基于 stub 的失败路径套件把每个进程内 ACL 沙箱文件都带到 100%,只有 runner 入口保持排除——它只作为 spawn 出的子进程在插桩运行之外执行,其行为由 runner 套件端到端钉住。窄范围且带注释的 V8 ignore 只覆盖不可达分支(另一平台专属分支、生命周期内不可达的防御守卫),其行为测试仍保留在所属平台。
@@ -36,7 +36,7 @@ Shiki 会禁用 TextMate 正则的延迟编译,并在用户内容进入保持
## 曾考虑的替代方案
**让每项原生 Windows 结果都成为 `all checks passed` 的依赖项。** 这会为聚合流程提供保真度最高的 Windows 判定,但也会让每次合并等待覆盖率和重复的观测性清单。要求构建与定向原生进程套件通过,可以保留快速的原生正确性信号,同时继续自动产生其他结果。
**让每项原生 Windows 结果都成为 `windows checks passed` 的依赖项。** 这会为聚合流程提供保真度最高的 Windows 判定,但也会让每次合并等待覆盖率和重复的观测性清单。要求构建与定向原生进程套件通过,可以保留快速的原生正确性信号,同时继续自动产生其他结果。
**只在拉取请求上运行 Wine。** Wine 能快速触达阻断性 win32 工具链分支,但即使真实 NT、NTFS、PowerShell、进程或原生插件约定已经损坏,也可能报告绿灯。
@@ -50,7 +50,7 @@ Shiki 会禁用 TextMate 正则的延迟编译,并在用户内容进入保持
## 后果
Wine 提供合并后的工具链证据。`all checks passed` 变绿时,原生覆盖率仍可能处于待处理或红灯状态。观测诊断在必需的构建作业内完成。诊断失败时,PR 检查仍显示成功;失败步骤、工作流警告和运行摘要会展示该失败。
Wine 提供合并后的工具链证据。`windows checks passed` 变绿时,原生覆盖率仍可能处于待处理或红灯状态。观测诊断在必需的构建作业内完成。诊断失败时,PR 检查仍显示成功;失败步骤、工作流警告和运行摘要会展示该失败。
每个拉取请求都会获得真实 NT 内核、NTFS、PowerShell、Windows 进程、原生插件和受支持源码覆盖率信号。观测检查复用成功构建的工作区,省去第二次检出、依赖安装和工作区构建。顺序执行会延长构建作业,并可能延迟必需判定;它也让诊断进程与编译分开运行。ready 与 master 聚合共用一份内部诊断清单;master 保留自己的工作区构建和完整检查。
@@ -9,8 +9,8 @@
en: 81d36185575e6d77
zh: 19003ab154d93394
/agent-note-windows-python-runtime-ci-stays-on-github-hosted-windows/decision:
en: 1169dd58149ad6b4
zh: aeebaf9ca8769de6
en: 68aff432bf30d821
zh: 6633d254904a39f6
/agent-note-windows-python-runtime-ci-stays-on-github-hosted-windows/alternatives-considered:
en: 817e77f011ea35c8
zh: 2087bc653620a60f
@@ -10,7 +10,7 @@ The Windows x64 target in [build-exe-for-python-sdk.yml](../../../../.github/wor
## Decision
The Windows x64 target always uses its hosted `matrix.runner` — `windows-2025` for pull-request CI — with the standard setup-python toolchain, the pnpm cache restore, and the pkg cache. The failover selector, the job-private Python setup step, the self-hosted dependency install and post-step cleanup, the private setup script, and the routing spec from #3629 are removed. `DSH_CI_FAILOVER_WINDOWS=selfhosted` again retargets only the native Windows jobs in [ci.yml](../../../../.github/workflows/ci.yml); the [failover runbook](2026-07-26-ci-failover-runbook.md) and [python/development.md](../../../../python/development.md) describe hosted-only runtime builds. The migration's UTF-8 mode exports existed because the persistent host used a GBK default code page; hosted images provide the locale the lane previously ran under.
The Windows x64 target always uses its hosted `matrix.runner` — `windows-2025` for pull-request CI — with the standard setup-python toolchain, the pnpm cache restore, and the pkg cache. The failover selector, the job-private Python setup step, the self-hosted dependency install and post-step cleanup, the private setup script, and the routing spec from #3629 are removed. `DSH_CI_FAILOVER_WINDOWS=selfhosted` again retargets only the native Windows jobs in [ci-windows.yml](../../../../.github/workflows/ci-windows.yml); the [failover runbook](2026-07-26-ci-failover-runbook.md) and [python/development.md](../../../../python/development.md) describe hosted-only runtime builds. The migration's UTF-8 mode exports existed because the persistent host used a GBK default code page; hosted images provide the locale the lane previously ran under.
## Alternatives considered
@@ -10,7 +10,7 @@ Status: implemented
## 决策
Windows x64 目标始终使用托管的 `matrix.runner`——PR CI 为 `windows-2025`——配以标准 setup-python 工具链、pnpm 缓存恢复与 pkg 缓存。来自 #3629 的故障切换选择器、作业私有 Python 准备步骤、自托管依赖安装与后置清理、私有准备脚本及路由测试均被移除。`DSH_CI_FAILOVER_WINDOWS=selfhosted` 再次只重定向 [ci.yml](../../../../.github/workflows/ci.yml) 中的原生 Windows 作业;[故障切换手册](2026-07-26-ci-failover-runbook.zh.md)与 [python/development.zh.md](../../../../python/development.zh.md) 描述仅托管的 runtime 构建。迁移中的 UTF-8 模式导出之所以存在,是因为持久主机使用 GBK 默认代码页;托管镜像提供该通道此前运行的区域设置。
Windows x64 目标始终使用托管的 `matrix.runner`——PR CI 为 `windows-2025`——配以标准 setup-python 工具链、pnpm 缓存恢复与 pkg 缓存。来自 #3629 的故障切换选择器、作业私有 Python 准备步骤、自托管依赖安装与后置清理、私有准备脚本及路由测试均被移除。`DSH_CI_FAILOVER_WINDOWS=selfhosted` 再次只重定向 [ci-windows.yml](../../../../.github/workflows/ci-windows.yml) 中的原生 Windows 作业;[故障切换手册](2026-07-26-ci-failover-runbook.zh.md)与 [python/development.zh.md](../../../../python/development.zh.md) 描述仅托管的 runtime 构建。迁移中的 UTF-8 模式导出之所以存在,是因为持久主机使用 GBK 默认代码页;托管镜像提供该通道此前运行的区域设置。
## 已考虑的替代方案
@@ -9,8 +9,8 @@
en: 43ecfec1601f933e
zh: 3db5365403ffeac5
/agent-note-blacksmith-as-a-failover-leg-for-ci-pools/decision:
en: fac195b5006faa2d
zh: 7c0ea86638c54e95
en: 667aae1665477840
zh: ff95c9c5fd9d1793
/agent-note-blacksmith-as-a-failover-leg-for-ci-pools/alternatives-considered:
en: a605440fed0ccc2a
zh: 621b35ad2850ca1b
@@ -12,7 +12,7 @@ The [failover runbook](2026-07-26-ci-failover-runbook.md) routes degraded enterp
`DSH_CI_FAILOVER_LINUX` and `DSH_CI_FAILOVER_WINDOWS` additionally accept the value `blacksmith`. When any repository writer sets it, the platform's participating jobs retarget onto Blacksmith's hosted runners at each job's configured vCPU size; any other value (or unset) keeps today's runner, and `selfhosted` keeps routing to the in-house pools. Like the existing values this is writer-manageable repository state — no merge, re-run the affected jobs — and it costs money only while set, which is why it is an explicit outage or experiment choice, never the default.
Participating jobs under `blacksmith`: the three enterprise Linux workers, the `node-compat` legs, the `all-checks-passed` verdict, and the three Windows jobs in [ci.yml](../../../../.github/workflows/ci.yml), using the per-job runner sizes declared there; `expected-filenames`; the sandbox bwrap leg; and the two manual benchmark matrices, whose 4-32 vCPU tiers map to Blacksmith equivalents while the 64/96-core rows keep the fleet labels because Blacksmith offers no such tier. The blacksmith branches carry no Dependabot exclusion: Blacksmith runners are ephemeral, so the persistent-VM rationale that keeps Dependabot on the hosted pool does not apply. The python-runtime builder stays hosted in full: its PR and master callers (`ci.yml`'s and `ci-master.yml`'s `python-runtime` jobs) pass real-API credentials (`DEEPSEEK_API_KEY_EXTERNAL`) into the reusable workflow, its macOS and arm64 single-exe legs need GitHub's images, and its wheel output feeds the PyPI publication chain. The docs-pages workflow is a tag-dispatched release publication, so it stays hosted too.
Participating jobs under `blacksmith`: the three enterprise Linux workers, the `node-compat` legs, and the `all-checks-passed` verdict in [ci.yml](../../../../.github/workflows/ci.yml), plus the three Windows jobs and the `windows-checks-passed` verdict in [ci-windows.yml](../../../../.github/workflows/ci-windows.yml), plus the `python-checks-passed` verdict in [ci-python.yml](../../../../.github/workflows/ci-python.yml), using the per-job runner sizes declared there; `expected-filenames`; the sandbox bwrap leg; and the two manual benchmark matrices, whose 4-32 vCPU tiers map to Blacksmith equivalents while the 64/96-core rows keep the fleet labels because Blacksmith offers no such tier. The blacksmith branches carry no Dependabot exclusion: Blacksmith runners are ephemeral, so the persistent-VM rationale that keeps Dependabot on the hosted pool does not apply. The python-runtime builder stays hosted in full: its PR and master callers (the `python-runtime` jobs in `ci-python.yml` and `ci-master.yml`) pass real-API credentials (`DEEPSEEK_API_KEY_EXTERNAL`) into the reusable workflow, its macOS and arm64 single-exe legs need GitHub's images, and its wheel output feeds the PyPI publication chain. The docs-pages workflow is a tag-dispatched release publication, so it stays hosted too.
Everything else keeps its own runner under every value: jobs pinned to standard hosted runners by workflow spec (`node-24-bench`, `python-sdk`, the master Wine gate, `release-publish` and `release-vendor-publish`, the Cloudflare preview, the weighted-approval pair); jobs that hold credentials (the real-API e2e lanes, the issue App-token jobs, the npm and PyPI publishers); jobs that need a host capability Blacksmith lacks (the Landlock legs and their pack verification, the Seatbelt leg, the linux-arm64 single-exe leg); and the release-shaped chains — the npm publishers, the PyPI publishers, the node-addon-system release chain, and the Pages deployment workflow — which stay entirely hosted because their artifacts feed published registry output. The release rehearsal jobs in `release.yml` and `release-vendor.yml` (`dependencies`, `pack`) keep their existing pre-change routing unchanged: they carry only the pre-existing `selfhosted` branch, so under `blacksmith` they fall back to their hosted `ubuntu-24.04` default exactly as under an unset variable, and they gain no `blacksmith` branch.
@@ -12,7 +12,7 @@ Status: implemented
`DSH_CI_FAILOVER_LINUX` 与 `DSH_CI_FAILOVER_WINDOWS` 额外接受值 `blacksmith`。任何仓库写者设置它时,该平台参与切换的作业按各作业配置的 vCPU 档位重定向到 Blacksmith 托管运行器;任何其它值(或未设置)保持今天的运行器,`selfhosted` 继续路由到自有池。与既有取值一样,这是写者可管理的仓库状态——无需合并,重跑受影响的作业即可——并且只在设置期间产生费用,这正是它作为明确的故障或实验选择、而非默认的原因。
`blacksmith` 下参与切换的作业:[ci.yml](../../../../.github/workflows/ci.yml) 中三个企业级 Linux 工作作业、`node-compat` 各腿、`all-checks-passed` 判定作业与三个 Windows 作业,使用该工作流为各作业声明的运行器规格;`expected-filenames`;sandbox 的 bwrap 腿;以及两个手动基准矩阵——其 4-32 vCPU 档位映射到 Blacksmith 等价档,64/96 核行保留自有池标签,因为 Blacksmith 没有对应档位。blacksmith 分支不带 Dependabot 排除:Blacksmith 运行器是临时的,因此把 Dependabot 留在托管池的持久虚拟机理由不适用。python-runtime 构建器整体保持托管:其 PR 与 master 调用方(`ci.yml` 与 `ci-master.yml` 的 `python-runtime` 作业)会把真实 API 凭据(`DEEPSEEK_API_KEY_EXTERNAL`)传入该可复用工作流,其 macOS 与 arm64 单文件分支需要 GitHub 镜像,且其 wheel 产物供给 PyPI 发布链。docs-pages 工作流是 tag 派发的发布,因此同样保持托管。
`blacksmith` 下参与切换的作业:[ci.yml](../../../../.github/workflows/ci.yml) 中三个企业级 Linux 工作作业、`node-compat` 各腿与 `all-checks-passed` 判定作业,以及 [ci-windows.yml](../../../../.github/workflows/ci-windows.yml) 中三个 Windows 作业与 `windows-checks-passed` 判定作业,以及 [ci-python.yml](../../../../.github/workflows/ci-python.yml) 中的 `python-checks-passed` 判定作业,使用这些工作流为各作业声明的运行器规格;`expected-filenames`;sandbox 的 bwrap 腿;以及两个手动基准矩阵——其 4-32 vCPU 档位映射到 Blacksmith 等价档,64/96 核行保留自有池标签,因为 Blacksmith 没有对应档位。blacksmith 分支不带 Dependabot 排除:Blacksmith 运行器是临时的,因此把 Dependabot 留在托管池的持久虚拟机理由不适用。python-runtime 构建器整体保持托管:其 PR 与 master 调用方(`ci-python.yml` 与 `ci-master.yml` 中的 `python-runtime` 作业)会把真实 API 凭据(`DEEPSEEK_API_KEY_EXTERNAL`)传入该可复用工作流,其 macOS 与 arm64 单文件分支需要 GitHub 镜像,且其 wheel 产物供给 PyPI 发布链。docs-pages 工作流是 tag 派发的发布,因此同样保持托管。
其余作业在任何取值下都保留自己的运行器:被 workflow spec 钉死在标准托管运行器上的作业(`node-24-bench`、`python-sdk`、master 的 Wine 门禁、`release-publish` 与 `release-vendor-publish`、Cloudflare preview、weighted-approval 一对);持有凭据的作业(真实 API 的 e2e 通道、issue App token 作业、npm 与 PyPI 发布作业);需要 Blacksmith 缺失的宿主能力的作业(Landlock 各腿及其打包校验、Seatbelt 腿、linux-arm64 单文件分支);以及发布形态的链——npm 发布、PyPI 发布、node-addon-system 发布链与 Pages 部署工作流——它们整体保持托管,因为其产物供给已发布的 registry 输出。`release.yml` 与 `release-vendor.yml` 中的发布演练作业(`dependencies`、`pack`)保持改动前既有的路由不变:它们只带既有的 `selfhosted` 分支,因此在 `blacksmith` 取值下与变量未设置时一样回落到托管的 `ubuntu-24.04` 默认,并不新增 `blacksmith` 分支。
@@ -9,8 +9,8 @@
en: e5c984fa32cc5e69
zh: 25d5742b2106d830
/agent-note-pr-artifact-builds-and-aggregate-typescript-checks/decision:
en: 26a903fdfeffcea7
zh: f6ea094ec32a7b14
en: 8353d57b20ca01c3
zh: e0b74906846d759c
/agent-note-pr-artifact-builds-and-aggregate-typescript-checks/alternatives-considered:
en: f61da066f9ddcd62
zh: 816fd6e0063017cd
@@ -20,7 +20,7 @@ The Python executable builder selects `build:artifacts` artifact compilation wit
PR artifact builds in CI invoke the root manifest's `build:artifacts`; a PR that edits these root manifest scripts activates the separate native-addon platform matrix, a trade-off [the build split](2026-10-11-build-artifacts-check-split.md) records. The vendor rehearsal and benchmark lanes invoke the internal build helpers directly.
Job inventory, runner selectors, and benchmark sample counts stay fixed; gate and worker ceilings do not increase. The two compatibility cases run one job at a time; the two-target Python runtime matrix starts after the keyless Python SDK job succeeds. Four Linux lanes, the Python SDK, three Windows lanes, and one compatibility job permit at most nine worker jobs initially. Once the SDK job finishes, the remaining seven independent lanes, one compatibility job, and two runtime targets permit at most ten. The runtime plan and wheel stages precede those targets and each use one job. This permits runtime validation to overlap the Node 24.9 build within the original ten-job ceiling. Coverage partitions reuse compact file-cost metadata within their platform and runner pool. Parallel release packing uses pnpm's existing bounded scheduler; serial publication keeps family order.
Job inventory, runner selectors, and benchmark sample counts stay fixed; gate and worker ceilings do not increase. The two compatibility cases run one job at a time; both x64 Python runtime targets start after the keyless Python SDK job succeeds in CI Python. Four Linux lanes, one compatibility job, three Windows lanes, and the Python SDK job permit at most nine worker jobs initially. Once the SDK job finishes, those four Linux lanes, one compatibility job, and three Windows lanes run beside the two runtime targets, permitting at most ten. Each runtime caller's plan and wheel stages precede its target and each use one job. This permits runtime validation to overlap the Node 24.9 build within the original ten-job ceiling. Coverage partitions reuse compact file-cost metadata within their platform and runner pool. Parallel release packing uses pnpm's existing bounded scheduler; serial publication keeps family order.
Each Node version runs the same eight source compatibility specs in one serial Vitest invocation. Both file parallelism and the CLI worker limit are fixed to one, and the gate sets `VITEST_MAX_WORKERS=1` because that environment variable overrides Vitest's CLI limit. Files retain fork isolation; the build-backed Node 24.9 lazy-search smoke keeps its separate build dependency. The original seven-spec paired samples preserve all 253 assertion outcomes and reduce startup-only median time from 11.52 to 8.69 seconds, without increasing owned descendant-process peak.
@@ -20,7 +20,7 @@ Python executable builder 在验证 runtime closure 后,通过 `--build-artifa
CI 的 PR 产物构建调用 root manifest 中的 `build:artifacts`;修改 root manifest 中这些 script 的 PR 会激活独立的 native-addon 平台矩阵,[构建拆分](2026-10-11-build-artifacts-check-split.zh.md)记录这一取舍。vendor 演练与 benchmark lane 直接调用内部 build helper。
job 清单、runner 选择和 benchmark sample 数量保持固定;gate 与 worker 上限不提高。两个 compatibility case 每次运行一个 job;双目标 Python runtime 矩阵在 keyless Python SDK job 成功后启动。四个 Linux lane、Python SDK、三个 Windows lane 和一个 compatibility job,初始最多占用九个 worker job。SDK job 完成后,剩余七个独立 lane、一个 compatibility job 和两个 runtime target,最多占用十个 job。runtime plan 与 wheel 阶段先于这些 target,各占一个 job。这允许 runtime 验证与 Node 24.9 构建重叠,同时保持原有十个 job 的上限。覆盖率分区在各自的平台和 runner pool 内复用精简的文件成本元数据。并行 release 打包使用 pnpm 现有的有界调度器;串行发布保持 family 顺序。
job 清单、runner 选择和 benchmark sample 数量保持固定;gate 与 worker 上限不提高。两个 compatibility case 每次运行一个 job;两个 x64 Python runtime target 都在 CI Python 中于 keyless Python SDK job 成功后启动。四个 Linux lane、一个 compatibility job、三个 Windows lane 与 Python SDK job,初始最多占用九个 worker job。SDK job 完成后,这四个 Linux lane、一个 compatibility job 与三个 Windows lane 与两个 runtime target 并行,最多占用十个 job。每个 runtime 调用方的 plan 与 wheel 阶段先于其 target,各占一个 job。这允许 runtime 验证与 Node 24.9 构建重叠,同时保持原有十个 job 的上限。覆盖率分区在各自的平台和 runner pool 内复用精简的文件成本元数据。并行 release 打包使用 pnpm 现有的有界调度器;串行发布保持 family 顺序。
每个 Node 版本在一次串行 Vitest 调用中运行相同的八个 source compatibility spec。文件并行和 CLI worker 上限均固定为一,gate 还设置 `VITEST_MAX_WORKERS=1`,因为该环境变量会覆盖 Vitest 的 CLI 上限。文件保持 fork 隔离;依赖构建的 Node 24.9 lazy-search smoke 保留独立的 build 依赖。原来七个 spec 的成对样本保持全部 253 个 assertion outcome,将启动开销比较的 median 从 11.52 秒降至 8.69 秒,且不增加所拥有 descendant process 的峰值。
@@ -21,8 +21,8 @@
en: 0f98f5b5f78b5603
zh: 95ee8bafa66d6b1b
/agent-note-installed-wheel-python-runtime-pull-request-validation/decision/required-targets:
en: 7a33cde000508475
zh: 142fb5ec6b59a280
en: ff9f43ef5d3480c8
zh: eb8d94581522f1ec
/agent-note-installed-wheel-python-runtime-pull-request-validation/existing-decisions-and-supersession:
en: 04aa439d4e607324
zh: f4c1d24b6a418cc4
@@ -30,7 +30,7 @@ Fork and Dependabot pull requests never receive the repository secret. Their nat
### Required targets
The pull-request `python-runtime` job calls the reusable builder for Linux x64 and Windows x64; master pushes select Linux arm64 and both macOS architectures under the [master-only platform policy](../../../../.github/workflows/ci-master.yml). Its aggregate result remains a dependency of `all checks passed`, so a failed, cancelled, or missing native carrier blocks the required verdict. The [sdk-runtime README](../../../../python/sdk-runtime/README.md) owns the Windows target and its PowerShell-specific minimal snapshot.
The pull-request `python-runtime` job in CI Python calls the reusable builder for Linux x64 and Windows x64; master pushes select Linux arm64 and both macOS architectures under the [master-only platform policy](../../../../.github/workflows/ci-master.yml). Its aggregate result is a dependency of CI Python's required verdict, `python checks passed`, so a failed, cancelled, or missing native carrier blocks the required verdict. The [sdk-runtime README](../../../../python/sdk-runtime/README.md) owns the Windows target and its PowerShell-specific minimal snapshot.
## Existing decisions and supersession
@@ -30,7 +30,7 @@ Fork 与 Dependabot 拉取请求永远不会获得仓库密钥。它们的原生
### 必需目标
拉取请求的 `python-runtime` job 针对 Linux x64 与 Windows x64 调用可复用构建器;master 推送根据[仅 master 平台策略](../../../../.github/workflows/ci-master.yml)选择 Linux arm64 与两种 macOS 架构。其聚合结果仍是 `all checks passed` 的依赖项,因此任一原生载体失败、取消或缺失都会阻止必需判定通过。[sdk-runtime README](../../../../python/sdk-runtime/README.zh.md) 负责 Windows 目标及其 PowerShell 专属极简快照。
拉取请求的 `python-runtime` job 在 CI Python 中针对 Linux x64 与 Windows x64 调用可复用构建器;master 推送根据[仅 master 平台策略](../../../../.github/workflows/ci-master.yml)选择 Linux arm64 与两种 macOS 架构。其聚合结果是 CI Python 必需判定 `python checks passed` 的依赖项,因此任一原生载体失败、取消或缺失都会阻止必需判定通过。[sdk-runtime README](../../../../python/sdk-runtime/README.zh.md) 负责 Windows 目标及其 PowerShell 专属极简快照。
## Existing decisions and supersession
+1 -1
View File
@@ -1,6 +1,6 @@
# AGENTS.md — GitHub Actions
Run jobs on Windows runners (`windows-*` labels) under native `pwsh`. Native Windows build and process checks contribute to the pull-request `all checks passed` verdict; Wine runs Windows Node on hosted Linux only in `ci-master.yml`. Python runtime CI checks Linux/Windows x64 on pull requests and Linux ARM64 plus both macOS architectures on master pushes; releases retain all five targets ([platform policy](workflows/ci-master.yml)). `ci.yml` is pull-request-only. Master-only platform checks, Linux/Windows self-hosted standbys, and manual runner benchmarks live in `ci-master.yml`, which listens to master pushes and `workflow_dispatch`, not `pull_request`; separating workflow triggers keeps master-only jobs out of PR check panels. The master standbys validate the self-hosted failover targets; preserve the existing per-platform switches (values `selfhosted` for the in-house standbys and `blacksmith` for Blacksmith's hosted runners) and the Dependabot hosted fallback under the `selfhosted` values ([failover runbook](../.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md), [blacksmith failover leg note](../.agents/notes/implemented/process/2026-09-09-blacksmith-failover-leg.md)).
Run jobs on Windows runners (`windows-*` labels) under native `pwsh`. Native Windows pull-request jobs live in `ci-windows.yml` and Python pull-request jobs in `ci-python.yml`, so each PR workflow can be re-run independently; their checks feed the required `windows checks passed` and `python checks passed` verdicts, as Linux jobs in `ci.yml` feed `all checks passed`; Wine runs Windows Node on hosted Linux only in `ci-master.yml`. Python runtime CI checks Linux/Windows x64 on pull requests and Linux ARM64 plus both macOS architectures on master pushes; releases retain all five targets ([platform policy](workflows/ci-master.yml)). `ci.yml`, `ci-windows.yml`, and `ci-python.yml` are pull-request-only. Master-only platform checks, Linux/Windows self-hosted standbys, and manual runner benchmarks live in `ci-master.yml`, which listens to master pushes and `workflow_dispatch`, not `pull_request`; separating workflow triggers keeps master-only jobs out of PR check panels. The master standbys validate the self-hosted failover targets; preserve the existing per-platform switches (values `selfhosted` for the in-house standbys and `blacksmith` for Blacksmith's hosted runners) and the Dependabot hosted fallback under the `selfhosted` values ([failover runbook](../.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md), [blacksmith failover leg note](../.agents/notes/implemented/process/2026-09-09-blacksmith-failover-leg.md)).
The manual [Windows packaging workflow](workflows/windows-package.yml) uses the dedicated self-hosted Windows x64 label `dsh-win-package-trial` under native `pwsh`; preserve this routing separately from the automatic CI lanes.
+87
View File
@@ -0,0 +1,87 @@
name: CI Python
# Python pull-request lanes run in their own workflow so a slow runtime build
# does not keep the CI run in progress: GitHub re-runs failed jobs only after
# every job of a run finishes, so each workflow can be re-run without waiting
# for the others. Branch protection requires this workflow's "python checks
# passed" verdict beside CI's "all checks passed" and CI Windows' "windows
# checks passed".
on:
pull_request:
permissions:
contents: read
env:
PRIMARY_NODE_VERSION: '24'
# CI runs must never report to the production telemetry endpoint baked
# into apps/cli/cordis.yml (AppCLIEntry disables the row when set).
DSH_TELEMETRY_DISABLED: '1'
# Cancel a superseded pull-request run on a new push, as in ci.yml.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
python-sdk:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
name: python 3.10 / keyless SDK
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/setup-python@v6.3.0
with:
python-version: '3.10'
cache: pip
- name: Install uv
run: python -m pip install uv==0.11.23
- uses: actions/setup-node@v7.0.0
with:
node-version: 24
- name: Test production blame scoring
run: uv run --python 3.10 --with-requirements .github/review-ownership/requirements.txt python -m unittest discover -s .github/review-ownership -p 'test_*.py'
- name: Run complete keyless Python suite
run: uv run --python 3.10 --group test --project python/sdk pytest
# The reusable builder owns each published executable, wheel, clean-install,
# keyless black-box, and trusted real-API path. Linux and Windows x64 block
# pull requests; Linux ARM64 and both macOS architectures run in
# ci-master.yml.
python-runtime:
if: github.event_name == 'pull_request'
needs: python-sdk
name: python runtime / release-shaped matrix
uses: ./.github/workflows/build-exe-for-python-sdk.yml
with:
targets: node24-linux-x64,node24-win-x64
ci: true
secrets:
DEEPSEEK_API_KEY_EXTERNAL: ${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}
# Stable required check for the Python lanes, with the same semantics as
# ci.yml's "all checks passed": every blocking job in THIS workflow is
# listed in `needs`. The bookkeeping job follows the Linux failover switch
# like the other verdicts.
python-checks-passed:
name: python checks passed
runs-on: >-
${{ vars.DSH_CI_FAILOVER_LINUX == 'blacksmith' && 'blacksmith-4vcpu-ubuntu-2404'
|| vars.DSH_CI_FAILOVER_LINUX == 'selfhosted'
&& github.event.pull_request.user.login != 'dependabot[bot]'
&& fromJSON('["self-hosted", "linux", "x64", "vm-backup"]')
|| 'ubuntu-latest' }}
needs: [python-sdk, python-runtime]
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
steps:
- name: Fail if any needed job did not succeed
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped')
run: |
echo "::error::Needed job results: ${{ join(needs.*.result, ', ') }}"
exit 1
- name: Python checks passed
run: echo "All needed jobs succeeded (${{ join(needs.*.result, ', ') }})"
+255
View File
@@ -0,0 +1,255 @@
name: CI Windows
# Native Windows pull-request lanes run in their own workflow so a slow Windows
# job does not keep the CI run in progress: GitHub re-runs failed jobs only
# after every job of a run finishes, so each workflow can be re-run without
# waiting for the others. Branch protection requires this workflow's
# "windows checks passed" verdict beside CI's "all checks passed" and CI
# Python's "python checks passed".
on:
pull_request:
permissions:
contents: read
env:
PRIMARY_NODE_VERSION: '24'
# CI runs must never report to the production telemetry endpoint baked
# into apps/cli/cordis.yml (AppCLIEntry disables the row when set).
DSH_TELEMETRY_DISABLED: '1'
# Cancel a superseded pull-request run on a new push, as in ci.yml.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# Native builds and process tests block pull requests independently of coverage.
# Observational checks reuse the successful build and remain non-blocking.
# Dependabot PRs are excluded from the self-hosted pool and stay queued for
# the hosted runner.
# Blacksmith uses 2 vCPU for file-serial native tests and 16 for coverage
# and the shared build/observational workspace.
windows-build:
if: github.event_name == 'pull_request'
runs-on: >-
${{ vars.DSH_CI_FAILOVER_WINDOWS == 'blacksmith' && 'blacksmith-16vcpu-windows-2025'
|| vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted'
&& github.event.pull_request.user.login != 'dependabot[bot]'
&& fromJSON('["self-hosted", "dsh-win-ci", "windows"]')
|| 'dsh-windows-2025-16core' }}
name: windows node 24 / build & observational
timeout-minutes: 60
env:
# A failing build or site aborts the sibling gate on the same runner.
DSH_GATE_FAIL_FAST: '1'
steps:
- uses: actions/checkout@v7.0.1
with:
persist-credentials: false
- name: Enable Developer Mode (symlink support)
shell: pwsh
run: >-
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
/t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
- uses: pnpm/action-setup@v5.0.0
with:
dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
- uses: actions/setup-node@v7.0.0
with:
node-version: ${{ env.PRIMARY_NODE_VERSION }}
- name: Install (immutable)
shell: pwsh
# See 2026-08-30-windows-refs-store-block-clone-install for the
# ReFS block-clone rationale; detect the workspace filesystem and
# pass --package-import-method=clone only on ReFS.
run: >-
$drive = (Split-Path -Qualifier $env:GITHUB_WORKSPACE).TrimEnd(':');
$fs = (Get-Volume -DriveLetter $drive).FileSystem;
if ($fs -eq 'ReFS') {
corepack pnpm install --frozen-lockfile --package-import-method=clone
} else {
pnpm install --frozen-lockfile
}
- name: Run blocking Windows builds
shell: pwsh
run: pnpm run check:ci:windows-blocking
- name: Parse Windows runner supervision scripts
shell: pwsh
run: |
foreach ($path in @('apps/desktop/scripts/windows-runner-task.ps1', 'apps/desktop/scripts/run-windows-runner-task.ps1', 'apps/desktop/tests/windows-runner-task.ps1', 'apps/desktop/tests/windows-runner-launcher.ps1')) {
$tokens = $null
$parseErrors = $null
$null = [Management.Automation.Language.Parser]::ParseFile((Join-Path $env:GITHUB_WORKSPACE $path), [ref]$tokens, [ref]$parseErrors)
if ($parseErrors.Count) { throw ($parseErrors -join "`n") }
}
- name: Verify windowless Windows runner launcher
shell: pwsh
run: ./apps/desktop/tests/windows-runner-launcher.ps1
- name: Run Windows observational gates on built artifacts
id: observational
continue-on-error: true
timeout-minutes: 15
shell: pwsh
env:
DSH_GATE_FAIL_FAST: ''
DSH_PUBLINT_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '8' || '' }}
run: pnpm run check:ci:windows-observational-ready
- name: Report Windows observational failures
continue-on-error: true
if: steps.observational.outcome == 'failure'
shell: pwsh
run: |
$message = 'Windows observational checks failed; see the observational step for diagnostics.'
Write-Output "::warning::$message"
$message | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
windows-coverage:
if: github.event_name == 'pull_request'
runs-on: >-
${{ vars.DSH_CI_FAILOVER_WINDOWS == 'blacksmith' && 'blacksmith-16vcpu-windows-2025'
|| vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted'
&& github.event.pull_request.user.login != 'dependabot[bot]'
&& fromJSON('["self-hosted", "dsh-win-ci", "windows"]')
|| 'dsh-windows-2025-16core' }}
name: windows node 24 / coverage
timeout-minutes: 120
env:
# Shared hosts expose host CPUs, not an isolated allocation.
DSH_COVERAGE_MAX_WORKERS: ${{ vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '6' || '' }}
DSH_COVERAGE_PARTITIONS: ${{ vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '4' || '' }}
DSH_GATE_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '3' || '' }}
DSH_COVERAGE_TEST_TIMEOUT_MS: '90000'
# A gate failure aborts the sibling gate instead of waiting out its
# multi-minute instrumented run.
DSH_GATE_FAIL_FAST: '1'
steps:
- uses: actions/checkout@v7.0.1
with:
persist-credentials: false
# Checkout removes the gitignored duration history, so restore it from
# the GitHub cache before coverage and save the updated one afterwards.
# The coordinator then weights partitions by measured durations on the
# second run instead of degrading to a file-count split. GitHub cache
# entries are immutable, so the save key is unique per run and the
# restore matches the newest entry through the stable prefix.
- name: Restore coverage duration history
uses: actions/cache/restore@v6.1.0
with:
path: .coverage-times.json
key: coverage-times-Windows-${{ runner.environment }}-${{ vars.DSH_CI_FAILOVER_WINDOWS || 'enterprise' }}-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
coverage-times-Windows-${{ runner.environment }}-${{ vars.DSH_CI_FAILOVER_WINDOWS || 'enterprise' }}-
- name: Enable Developer Mode (symlink support)
shell: pwsh
run: >-
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
/t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
- uses: pnpm/action-setup@v5.0.0
with:
dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
- uses: actions/setup-node@v7.0.0
with:
node-version: ${{ env.PRIMARY_NODE_VERSION }}
- name: Install (immutable)
shell: pwsh
# See 2026-08-30-windows-refs-store-block-clone-install for the
# ReFS block-clone rationale; detect the workspace filesystem and
# pass --package-import-method=clone only on ReFS.
run: >-
$drive = (Split-Path -Qualifier $env:GITHUB_WORKSPACE).TrimEnd(':');
$fs = (Get-Volume -DriveLetter $drive).FileSystem;
if ($fs -eq 'ReFS') {
corepack pnpm install --frozen-lockfile --package-import-method=clone
} else {
pnpm install --frozen-lockfile
}
# No build before coverage, matching the Linux lane: workspace imports
# resolve to src through the tsconfig paths map, and the lib-consuming
# suites (webworker-packer image-loadable, webworker-runtime
# transform-corpus, client ui-trajectory client-bundle) self-skip on
# unbuilt checkouts.
- name: Run Windows coverage
shell: pwsh
run: pnpm run check:ci:coverage
- name: Save coverage duration history
# Coverage flakes must not prevent the cache from building; the
# measured durations remain useful even when a partition failed. The
# per-run key keeps every save a fresh immutable cache entry.
if: ${{ !cancelled() }}
uses: actions/cache/save@v6.1.0
with:
path: .coverage-times.json
key: coverage-times-Windows-${{ runner.environment }}-${{ vars.DSH_CI_FAILOVER_WINDOWS || 'enterprise' }}-${{ github.run_id }}-${{ github.run_attempt }}
windows-native-tests:
if: github.event_name == 'pull_request'
runs-on: >-
${{ vars.DSH_CI_FAILOVER_WINDOWS == 'blacksmith' && 'blacksmith-2vcpu-windows-2025'
|| vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted'
&& github.event.pull_request.user.login != 'dependabot[bot]'
&& fromJSON('["self-hosted", "dsh-win-ci", "windows"]')
|| 'dsh-windows-2025-16core' }}
name: windows node 24 / native tests
timeout-minutes: 60
steps:
- uses: actions/checkout@v7.0.1
with:
persist-credentials: false
- name: Enable Developer Mode (symlink support)
shell: pwsh
run: >-
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
/t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
- uses: pnpm/action-setup@v5.0.0
with:
dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
- uses: actions/setup-node@v7.0.0
with:
node-version: ${{ env.PRIMARY_NODE_VERSION }}
- name: Install (immutable)
shell: pwsh
# See 2026-08-30-windows-refs-store-block-clone-install for the
# ReFS block-clone rationale; detect the workspace filesystem and
# pass --package-import-method=clone only on ReFS.
run: >-
$drive = (Split-Path -Qualifier $env:GITHUB_WORKSPACE).TrimEnd(':');
$fs = (Get-Volume -DriveLetter $drive).FileSystem;
if ($fs -eq 'ReFS') {
corepack pnpm install --frozen-lockfile --package-import-method=clone
} else {
pnpm install --frozen-lockfile
}
- name: Run Windows-specific native tests
shell: pwsh
run: >-
pnpm exec vitest run
--no-file-parallelism
--testTimeout 90000
packages/shell/tool-pwsh/tests/loader.spec.ts
packages/workflow/workflow-ptc/tests/workflow-ptc.spec.ts
packages/experimental/tool-ralph/tests/integration.spec.ts
packages/subprocess/subprocess-local/tests/process-exit.spec.ts
# Stable required check for the Windows lanes, with the same semantics as
# ci.yml's "all checks passed": every blocking job in THIS workflow is
# listed in `needs`. windows-coverage stays outside the verdict, so its
# per-file result remains visible without blocking. The bookkeeping job
# follows the Linux failover switch like the CI verdict.
windows-checks-passed:
name: windows checks passed
runs-on: >-
${{ vars.DSH_CI_FAILOVER_LINUX == 'blacksmith' && 'blacksmith-4vcpu-ubuntu-2404'
|| vars.DSH_CI_FAILOVER_LINUX == 'selfhosted'
&& github.event.pull_request.user.login != 'dependabot[bot]'
&& fromJSON('["self-hosted", "linux", "x64", "vm-backup"]')
|| 'ubuntu-latest' }}
needs: [windows-build, windows-native-tests]
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
steps:
- name: Fail if any needed job did not succeed
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped')
run: |
echo "::error::Needed job results: ${{ join(needs.*.result, ', ') }}"
exit 1
- name: Windows checks passed
run: echo "All needed jobs succeeded (${{ join(needs.*.result, ', ') }})"
+8 -254
View File
@@ -12,7 +12,7 @@ env:
DSH_TELEMETRY_DISABLED: '1'
# Cancel a superseded pull-request run on a new push so a fresh head does not
# queue a second full 9-job run behind a stale one (paid enterprise runners
# queue a second full run behind a stale one (paid enterprise runners
# would otherwise stack with no auto-cancellation).
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -36,9 +36,8 @@ jobs:
# Blacksmith's hosted runners sized per workload (see
# .agents/notes/implemented/process/2026-09-09-blacksmith-failover-leg.md). The
# in-house pool's readiness is re-proven on every master push by the
# serial-linux-selfhosted standby lane in ci-master.yml. The Windows failover
# switch is the separate DSH_CI_FAILOVER_WINDOWS variable on the windows-native
# job below.
# serial-linux-selfhosted standby lane in ci-master.yml. Native Windows jobs
# and their separate DSH_CI_FAILOVER_WINDOWS switch live in ci-windows.yml.
node-24:
if: github.event_name == 'pull_request'
runs-on: >-
@@ -478,258 +477,13 @@ jobs:
- name: Check Loader internal shape detection
run: pnpm exec vitest run packages/boot/app-boot/tests/loader-shape.compat.spec.ts
python-sdk:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
name: python 3.10 / keyless SDK
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/setup-python@v6.3.0
with:
python-version: '3.10'
cache: pip
- name: Install uv
run: python -m pip install uv==0.11.23
- uses: actions/setup-node@v7.0.0
with:
node-version: 24
- name: Test production blame scoring
run: uv run --python 3.10 --with-requirements .github/review-ownership/requirements.txt python -m unittest discover -s .github/review-ownership -p 'test_*.py'
- name: Run complete keyless Python suite
run: uv run --python 3.10 --group test --project python/sdk pytest
# The reusable builder owns each published executable, wheel, clean-install,
# keyless black-box, and trusted real-API path. Linux/Windows x64 block PRs;
# Linux ARM64 and both macOS architectures run in ci-master.yml.
python-runtime:
if: github.event_name == 'pull_request'
needs: python-sdk
name: python runtime / release-shaped matrix
uses: ./.github/workflows/build-exe-for-python-sdk.yml
with:
targets: node24-linux-x64,node24-win-x64
ci: true
secrets:
DEEPSEEK_API_KEY_EXTERNAL: ${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}
# Native builds and process tests block pull requests independently of coverage.
# Observational checks reuse the successful build and remain non-blocking.
# Dependabot PRs are excluded from the self-hosted pool and stay queued for
# the hosted runner.
# Blacksmith uses 2 vCPU for file-serial native tests and 16 for coverage
# and the shared build/observational workspace.
windows-build:
if: github.event_name == 'pull_request'
runs-on: >-
${{ vars.DSH_CI_FAILOVER_WINDOWS == 'blacksmith' && 'blacksmith-16vcpu-windows-2025'
|| vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted'
&& github.event.pull_request.user.login != 'dependabot[bot]'
&& fromJSON('["self-hosted", "dsh-win-ci", "windows"]')
|| 'dsh-windows-2025-16core' }}
name: windows node 24 / build & observational
timeout-minutes: 60
env:
# A failing build or site aborts the sibling gate on the same runner.
DSH_GATE_FAIL_FAST: '1'
steps:
- uses: actions/checkout@v7.0.1
with:
persist-credentials: false
- name: Enable Developer Mode (symlink support)
shell: pwsh
run: >-
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
/t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
- uses: pnpm/action-setup@v5.0.0
with:
dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
- uses: actions/setup-node@v7.0.0
with:
node-version: ${{ env.PRIMARY_NODE_VERSION }}
- name: Install (immutable)
shell: pwsh
# See 2026-08-30-windows-refs-store-block-clone-install for the
# ReFS block-clone rationale; detect the workspace filesystem and
# pass --package-import-method=clone only on ReFS.
run: >-
$drive = (Split-Path -Qualifier $env:GITHUB_WORKSPACE).TrimEnd(':');
$fs = (Get-Volume -DriveLetter $drive).FileSystem;
if ($fs -eq 'ReFS') {
corepack pnpm install --frozen-lockfile --package-import-method=clone
} else {
pnpm install --frozen-lockfile
}
- name: Run blocking Windows builds
shell: pwsh
run: pnpm run check:ci:windows-blocking
- name: Parse Windows runner supervision scripts
shell: pwsh
run: |
foreach ($path in @('apps/desktop/scripts/windows-runner-task.ps1', 'apps/desktop/scripts/run-windows-runner-task.ps1', 'apps/desktop/tests/windows-runner-task.ps1', 'apps/desktop/tests/windows-runner-launcher.ps1')) {
$tokens = $null
$parseErrors = $null
$null = [Management.Automation.Language.Parser]::ParseFile((Join-Path $env:GITHUB_WORKSPACE $path), [ref]$tokens, [ref]$parseErrors)
if ($parseErrors.Count) { throw ($parseErrors -join "`n") }
}
- name: Verify windowless Windows runner launcher
shell: pwsh
run: ./apps/desktop/tests/windows-runner-launcher.ps1
- name: Run Windows observational gates on built artifacts
id: observational
continue-on-error: true
timeout-minutes: 15
shell: pwsh
env:
DSH_GATE_FAIL_FAST: ''
DSH_PUBLINT_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '8' || '' }}
run: pnpm run check:ci:windows-observational-ready
- name: Report Windows observational failures
continue-on-error: true
if: steps.observational.outcome == 'failure'
shell: pwsh
run: |
$message = 'Windows observational checks failed; see the observational step for diagnostics.'
Write-Output "::warning::$message"
$message | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
windows-coverage:
if: github.event_name == 'pull_request'
runs-on: >-
${{ vars.DSH_CI_FAILOVER_WINDOWS == 'blacksmith' && 'blacksmith-16vcpu-windows-2025'
|| vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted'
&& github.event.pull_request.user.login != 'dependabot[bot]'
&& fromJSON('["self-hosted", "dsh-win-ci", "windows"]')
|| 'dsh-windows-2025-16core' }}
name: windows node 24 / coverage
timeout-minutes: 120
env:
# Shared hosts expose host CPUs, not an isolated allocation.
DSH_COVERAGE_MAX_WORKERS: ${{ vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '6' || '' }}
DSH_COVERAGE_PARTITIONS: ${{ vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '4' || '' }}
DSH_GATE_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '3' || '' }}
DSH_COVERAGE_TEST_TIMEOUT_MS: '90000'
# A gate failure aborts the sibling gate instead of waiting out its
# multi-minute instrumented run.
DSH_GATE_FAIL_FAST: '1'
steps:
- uses: actions/checkout@v7.0.1
with:
persist-credentials: false
# Checkout removes the gitignored duration history, so restore it from
# the GitHub cache before coverage and save the updated one afterwards.
# The coordinator then weights partitions by measured durations on the
# second run instead of degrading to a file-count split. GitHub cache
# entries are immutable, so the save key is unique per run and the
# restore matches the newest entry through the stable prefix.
- name: Restore coverage duration history
uses: actions/cache/restore@v6.1.0
with:
path: .coverage-times.json
key: coverage-times-Windows-${{ runner.environment }}-${{ vars.DSH_CI_FAILOVER_WINDOWS || 'enterprise' }}-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
coverage-times-Windows-${{ runner.environment }}-${{ vars.DSH_CI_FAILOVER_WINDOWS || 'enterprise' }}-
- name: Enable Developer Mode (symlink support)
shell: pwsh
run: >-
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
/t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
- uses: pnpm/action-setup@v5.0.0
with:
dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
- uses: actions/setup-node@v7.0.0
with:
node-version: ${{ env.PRIMARY_NODE_VERSION }}
- name: Install (immutable)
shell: pwsh
# See 2026-08-30-windows-refs-store-block-clone-install for the
# ReFS block-clone rationale; detect the workspace filesystem and
# pass --package-import-method=clone only on ReFS.
run: >-
$drive = (Split-Path -Qualifier $env:GITHUB_WORKSPACE).TrimEnd(':');
$fs = (Get-Volume -DriveLetter $drive).FileSystem;
if ($fs -eq 'ReFS') {
corepack pnpm install --frozen-lockfile --package-import-method=clone
} else {
pnpm install --frozen-lockfile
}
# No build before coverage, matching the Linux lane: workspace imports
# resolve to src through the tsconfig paths map, and the lib-consuming
# suites (webworker-packer image-loadable, webworker-runtime
# transform-corpus, client ui-trajectory client-bundle) self-skip on
# unbuilt checkouts.
- name: Run Windows coverage
shell: pwsh
run: pnpm run check:ci:coverage
- name: Save coverage duration history
# Coverage flakes must not prevent the cache from building; the
# measured durations remain useful even when a partition failed. The
# per-run key keeps every save a fresh immutable cache entry.
if: ${{ !cancelled() }}
uses: actions/cache/save@v6.1.0
with:
path: .coverage-times.json
key: coverage-times-Windows-${{ runner.environment }}-${{ vars.DSH_CI_FAILOVER_WINDOWS || 'enterprise' }}-${{ github.run_id }}-${{ github.run_attempt }}
windows-native-tests:
if: github.event_name == 'pull_request'
runs-on: >-
${{ vars.DSH_CI_FAILOVER_WINDOWS == 'blacksmith' && 'blacksmith-2vcpu-windows-2025'
|| vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted'
&& github.event.pull_request.user.login != 'dependabot[bot]'
&& fromJSON('["self-hosted", "dsh-win-ci", "windows"]')
|| 'dsh-windows-2025-16core' }}
name: windows node 24 / native tests
timeout-minutes: 60
steps:
- uses: actions/checkout@v7.0.1
with:
persist-credentials: false
- name: Enable Developer Mode (symlink support)
shell: pwsh
run: >-
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
/t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
- uses: pnpm/action-setup@v5.0.0
with:
dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
- uses: actions/setup-node@v7.0.0
with:
node-version: ${{ env.PRIMARY_NODE_VERSION }}
- name: Install (immutable)
shell: pwsh
# See 2026-08-30-windows-refs-store-block-clone-install for the
# ReFS block-clone rationale; detect the workspace filesystem and
# pass --package-import-method=clone only on ReFS.
run: >-
$drive = (Split-Path -Qualifier $env:GITHUB_WORKSPACE).TrimEnd(':');
$fs = (Get-Volume -DriveLetter $drive).FileSystem;
if ($fs -eq 'ReFS') {
corepack pnpm install --frozen-lockfile --package-import-method=clone
} else {
pnpm install --frozen-lockfile
}
- name: Run Windows-specific native tests
shell: pwsh
run: >-
pnpm exec vitest run
--no-file-parallelism
--testTimeout 90000
packages/shell/tool-pwsh/tests/loader.spec.ts
packages/workflow/workflow-ptc/tests/workflow-ptc.spec.ts
packages/experimental/tool-ralph/tests/integration.spec.ts
packages/subprocess/subprocess-local/tests/process-exit.spec.ts
# 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`. Native Windows build and process checks are required; Wine and
# the deferred Python runtime targets live in ci-master.yml and do not
# participate in this PR verdict. `needs` cannot cross workflow files.
# `needs`. `needs` cannot cross workflow files: native Windows jobs report
# the separate "windows checks passed" verdict from ci-windows.yml, Python
# jobs report "python checks passed" from ci-python.yml, and Wine plus the
# master-only runtime targets live in ci-master.yml.
# An explicit status function runs the verdict after failed/skipped needs
# without keeping a cancelled workflow alive for an obsolete verdict.
all-checks-passed:
@@ -749,7 +503,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-bench, node-24-consumers, node-compat, python-sdk, python-runtime, windows-build, windows-native-tests]
needs: [node-24, node-24-coverage, node-24-bench, node-24-consumers, node-compat]
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
steps:
- name: Fail if any needed job did not succeed
@@ -3,9 +3,9 @@ import { cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/pr
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'
import { zstdDecompress } from 'node:zlib'
import { zstdDecompressSync } from 'node:zlib'
import { resolveExampleLaunch } from '@deepseek-ai/dsh-loader-smoke'
import { scanZstdFrames } from '@deepseek-ai/dsh-session-persistence-jsonl/src/zstd.js'
import { execa } from 'execa'
import { describe, expect, it, onTestFinished } from 'vitest'
import { workspaceDependencyPaths, type PrimaryRuntimeManifest } from '@deepseek-ai/dsh-tool-workspace-dependencies'
@@ -17,7 +17,6 @@ const launch = resolveExampleLaunch({
srcBin: fileURLToPath(new URL('../../../src/bin.ts', import.meta.url)),
mode: 'lib',
})
const decompress = promisify(zstdDecompress)
function waitForLine(
@@ -199,11 +198,30 @@ describe('Python SDK dsh profile keyless smoke', () => {
expect(exit.exitCode, `signal=${String(exit.signal)}; stderr=${stderr}`).toBe(0)
const sessionsRoot = join(root, '.dsh', 'sessions')
const files = await readdir(sessionsRoot, { recursive: true })
const log = files.find(file => file.endsWith('.jsonl.zstd'))
// Session logs are JSONL across one zstd frame per batch, header frame
// first. Decompress frames explicitly: a single call over concatenated
// frames returned only the first frame on Node 24.19 and returns every
// frame on 24.22, so neither behavior may back the assertion.
const logs: { compressed: Buffer; header: Record<string, unknown> }[] = []
for (const file of files.filter(file => file.endsWith('.jsonl.zstd'))) {
const compressed = await readFile(join(sessionsRoot, file))
expect(compressed.subarray(0, 4).toString('hex')).toBe('28b52ffd')
const first = scanZstdFrames(compressed).frames[0]
if (first === undefined) continue
const headerLine = zstdDecompressSync(compressed.subarray(first.start, first.end)).toString().trim().split('\n')[0]
if (headerLine === undefined) continue
logs.push({ compressed, header: JSON.parse(headerLine) as Record<string, unknown> })
}
const log = logs.find(entry => entry.header.type === 'session' && entry.header.id === 'main')
expect(log).toBeDefined()
const compressed = await readFile(join(sessionsRoot, log!))
expect(compressed.subarray(0, 4).toString('hex')).toBe('28b52ffd')
expect(JSON.parse((await decompress(compressed)).toString())).toMatchObject({ type: 'session', id: 'main' })
const { frames, tornStart } = scanZstdFrames(log!.compressed)
expect(tornStart).toBeUndefined()
const records = frames
.flatMap(({ start, end }) => zstdDecompressSync(log!.compressed.subarray(start, end)).toString().trim().split('\n'))
.map(line => JSON.parse(line) as Record<string, unknown>)
expect(records[0]).toMatchObject({ type: 'session', id: 'main' })
expect(records.length).toBeGreaterThan(1)
expect(records.some(record => record.type === 'turn/end')).toBe(true)
} finally {
// No-op after exit; reject: false settles on every outcome, so cleanup never races teardown.
child.kill('SIGKILL')
+2 -2
View File
@@ -30,8 +30,8 @@
en: 2efb7c4e482413fc
zh: 75386476a283111a
/development-guide/contributor-reference/ci-gates:
en: 16e515e3d94f1cfc
zh: fe8cf9deec64581a
en: dfa3a24d9070478f
zh: fa889b3536ab58e2
/development-guide/contributor-reference/daily-commands:
en: 08aa6ec1cdabfeb7
zh: dcce5ae9d3c829d0
+1 -1
View File
@@ -155,7 +155,7 @@ Outside CI (`CI` other than `true`), default worker counts follow the [local wor
### CI gates
The keyless [CI workflow](../.github/workflows/ci.yml) groups independent gates into broad lanes and runs a smaller compatibility signal across supported Node versions. Artifact consumers wait for one build within their lane. Coverage partitions reuse file timings scoped to their platform and runner pool to balance work within the configured worker limit. Required benchmarks run separately on standard GitHub-hosted Linux; the [benchmark runner reference](../benchmarks/AGENTS.md) owns routing and the job timeout. The separate real-API workflow runs `pnpm run test:e2e` with its configured worker bound. See [scripts/run-gates.ts](../scripts/run-gates.ts) and the workflow files for the current gate and job inventory.
The keyless [CI workflow](../.github/workflows/ci.yml) groups independent gates into broad lanes and runs a smaller compatibility signal across supported Node versions. Artifact consumers wait for one build within their lane. Coverage partitions reuse file timings scoped to their platform and runner pool to balance work within the configured worker limit. Native Windows lanes run in the separate [CI Windows workflow](../.github/workflows/ci-windows.yml) and Python checks in the separate [CI Python workflow](../.github/workflows/ci-python.yml), each of which can be re-run independently. Required benchmarks run separately on standard GitHub-hosted Linux; the [benchmark runner reference](../benchmarks/AGENTS.md) owns routing and the job timeout. The separate real-API workflow runs `pnpm run test:e2e` with its configured worker bound. See [scripts/run-gates.ts](../scripts/run-gates.ts) and the workflow files for the current gate and job inventory.
The credential-free dsh dependency-layout and dsh/vendor pack rehearsals use the existing Linux self-hosted pool only when `DSH_CI_FAILOVER_LINUX=selfhosted` and the event is a trusted master push or same-repository, non-fork, non-Dependabot pull request. All other cases, including manual dispatch, use `ubuntu-24.94`; manual publication stays hosted. See the [release rehearsal runner reference](../.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md) for persistent-store isolation and fallback limits.
+1 -1
View File
@@ -160,7 +160,7 @@ pi-ai 检查会在内存中重新生成预期元数据。数据过期时,检
### CI 门禁
keyless [CI 工作流](../.github/workflows/ci.yml) 将独立门禁分组到若干宽粒度 lane,并在受支持的 Node 版本上运行一组较小的兼容性检查。产物消费方在各自 lane 内等待一次 build。覆盖率分区复用按平台和运行器池隔离的文件耗时,在配置的 worker 上限内均衡工作量。必需 benchmark 在标准 GitHub 托管 Linux 上独立运行;[benchmark 运行器说明](../benchmarks/AGENTS.md)拥有路由及 job 超时。单独的真实 API 工作流按其配置的 worker 上限运行 `pnpm run test:e2e`。当前门禁和 job 清单以 [scripts/run-gates.ts](../scripts/run-gates.ts) 和工作流文件为准。
keyless [CI 工作流](../.github/workflows/ci.yml) 将独立门禁分组到若干宽粒度 lane,并在受支持的 Node 版本上运行一组较小的兼容性检查。产物消费方在各自 lane 内等待一次 build。覆盖率分区复用按平台和运行器池隔离的文件耗时,在配置的 worker 上限内均衡工作量。原生 Windows lane 运行在独立的 [CI Windows 工作流](../.github/workflows/ci-windows.yml)中,Python 检查运行在独立的 [CI Python 工作流](../.github/workflows/ci-python.yml)中,两者都可单独重新运行。必需 benchmark 在标准 GitHub 托管 Linux 上独立运行;[benchmark 运行器说明](../benchmarks/AGENTS.md)拥有路由及 job 超时。单独的真实 API 工作流按其配置的 worker 上限运行 `pnpm run test:e2e`。当前门禁和 job 清单以 [scripts/run-gates.ts](../scripts/run-gates.ts) 和工作流文件为准。
不带凭据的 dsh 依赖布局检查与 dsh/vendor 打包演练仅在 `DSH_CI_FAILOVER_LINUX=selfhosted`,且事件为受信任的 master 推送或同仓库、非 fork、非 Dependabot 拉取请求时使用现有 Linux 自托管池。其余情况(包括手动触发)均使用 `ubuntu-24.94`;手动发布仍使用托管运行器。持久化存储隔离与回退限制见[发布演练运行器说明](../.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md)。
+6 -2
View File
@@ -25,7 +25,11 @@ interface CompatibilityJob {
}
const workflow = yaml.load(readFileSync(resolve(import.meta.dirname, '../.github/workflows/ci.yml'), 'utf8')) as {
jobs: { 'node-compat': CompatibilityJob; 'python-sdk': { 'runs-on': string } }
jobs: { 'node-compat': CompatibilityJob }
}
// Python keeps its own required verdict in ci-python.yml.
const pythonWorkflow = yaml.load(readFileSync(resolve(import.meta.dirname, '../.github/workflows/ci-python.yml'), 'utf8')) as {
jobs: { 'python-sdk': { 'runs-on': string } }
}
const job = workflow.jobs['node-compat']
const labels = ['self-hosted', 'linux', 'x64', 'vm-backup']
@@ -86,7 +90,7 @@ describe('Node compatibility self-hosted routing', () => {
expect(job.env.DSH_GATE_CONCURRENCY).toBe('${{ matrix.gate_concurrency }}')
expect(job.steps.map(step => step.run)).toContain('pnpm run check:node-compat')
expect(job.steps.map(step => step.run)).toContain('pnpm exec vitest run packages/boot/app-boot/tests/loader-shape.compat.spec.ts')
expect(workflow.jobs['python-sdk']['runs-on']).toBe('ubuntu-latest')
expect(pythonWorkflow.jobs['python-sdk']['runs-on']).toBe('ubuntu-latest')
})
it('isolates version installs and enables hosted package caching only on hosted runners', () => {
+120 -46
View File
@@ -14,12 +14,18 @@ const runnerPrivatePnpmDestination = /^\$\{\{ runner\.temp \}\}\/setup-pnpm-\$\{
const nativeWindowsPnpmDestination = '${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}'
describe('CI workflow', () => {
it('keeps at most ten worker jobs active while runtime targets follow the SDK suite', () => {
it('keeps at most ten worker jobs active across PR workflows while Python runtime targets follow the SDK suite', () => {
const ci = loadWorkflow('.github/workflows/ci.yml')
const ciWindows = loadWorkflow('.github/workflows/ci-windows.yml')
const ciPython = loadWorkflow('.github/workflows/ci-python.yml')
const compatibility = workflowJob(ci, 'node-compat')
const runtime = workflowJob(ci, 'python-runtime')
if (!isRecord(ci.jobs) || !isRecord(compatibility.strategy) || !isRecord(runtime.with)) {
throw new TypeError('CI must define jobs, compatibility scheduling, and runtime inputs')
const runtime = workflowJob(ciPython, 'python-runtime')
if (!isRecord(ci.jobs)
|| !isRecord(ciWindows.jobs)
|| !isRecord(ciPython.jobs)
|| !isRecord(compatibility.strategy)
|| !isRecord(runtime.with)) {
throw new TypeError('CI, CI Windows, and CI Python must define jobs, compatibility scheduling, and runtime inputs')
}
const compatibilityLimit = compatibility.strategy['max-parallel']
const targets = runtime.with.targets
@@ -34,17 +40,31 @@ describe('CI workflow', () => {
})
expect(workflowJob(ci, 'all-checks-passed').needs).toContain('node-compat')
const ordinaryJobs = Object.entries(ci.jobs)
.filter(([name]) => !['node-compat', 'python-runtime', 'all-checks-passed'].includes(name))
.filter(([name]) => !['node-compat', 'all-checks-passed'].includes(name))
for (const [name, job] of ordinaryJobs) {
if (!isRecord(job)) throw new TypeError(`${name} must define a job`)
expect(job).not.toHaveProperty('strategy')
expect(job).not.toHaveProperty('uses')
}
expect(ordinaryJobs.map(([name]) => name)).toContain('python-sdk')
// Python reports its own required verdict, so neither its jobs nor a
// reusable workflow caller remain in the Linux or Windows workflow.
expect(Object.keys(ci.jobs).filter(name => name.startsWith('python'))).toEqual([])
expect(Object.keys(ciWindows.jobs)).not.toContain('python-runtime')
const pythonSdk = workflowJob(ciPython, 'python-sdk')
expect(pythonSdk).not.toHaveProperty('strategy')
expect(pythonSdk).not.toHaveProperty('uses')
expect(runtime.needs).toBe('python-sdk')
expect(runtime.strategy).toBeUndefined()
expect(runtime.with).toMatchObject({ targets: 'node24-linux-x64,node24-win-x64' })
expect(runtime.uses).toBe('./.github/workflows/build-exe-for-python-sdk.yml')
const windowsJobs = Object.entries(ciWindows.jobs)
.filter(([name]) => name !== 'windows-checks-passed')
for (const [name, job] of windowsJobs) {
if (!isRecord(job)) throw new TypeError(`${name} must define a job`)
expect(job).not.toHaveProperty('strategy')
expect(job).not.toHaveProperty('uses')
}
const builder = loadWorkflow('.github/workflows/build-exe-for-python-sdk.yml')
if (!isRecord(builder.jobs)) throw new TypeError('Python runtime builder must define jobs')
@@ -63,19 +83,54 @@ describe('CI workflow', () => {
if (!isRecord(build.strategy)) throw new TypeError('Python runtime build must define its target matrix')
expect(build.strategy.matrix).toEqual({ include: '${{ fromJSON(needs.plan.outputs.matrix) }}' })
const beforeSdk = ordinaryJobs.length + compatibilityLimit
const afterSdk = ordinaryJobs.length - 1 + compatibilityLimit + Math.max(1, targets.split(',').length)
// Python's runtime targets start only after the SDK suite; Windows starts
// immediately. Plan and wheel stages precede the two-target matrix.
const windowsActive = windowsJobs.length
const beforeSdk = ordinaryJobs.length + compatibilityLimit + windowsActive + 1
const afterSdk = ordinaryJobs.length + compatibilityLimit + windowsActive + Math.max(1, targets.split(',').length)
expect(beforeSdk).toBeLessThanOrEqual(10)
expect(afterSdk).toBeLessThanOrEqual(10)
})
it('keeps the union of required PR verdicts equal to the pre-split blocking set', () => {
const verdicts = [
['ci.yml', 'all-checks-passed'],
['ci-windows.yml', 'windows-checks-passed'],
['ci-python.yml', 'python-checks-passed'],
] as const
const blocking: string[] = []
for (const [file, name] of verdicts) {
const workflow = loadWorkflow('.github/workflows/' + file)
const verdict = workflowJob(workflow, name)
if (!isRecord(workflow.jobs) || !Array.isArray(verdict.needs)) {
throw new TypeError(`${file} must define jobs and the ${name} verdict needs`)
}
const jobs = workflow.jobs
expect(verdict.needs.every(id => typeof id === 'string' && id in jobs)).toBe(true)
blocking.push(...(verdict.needs as string[]))
}
// Splitting the workflows only moves jobs between verdicts; the blocking
// set itself must stay identical, or branch protection would accept a
// failure it rejected before the split.
expect(blocking.sort()).toEqual([
'node-24',
'node-24-bench',
'node-24-consumers',
'node-24-coverage',
'node-compat',
'python-runtime',
'python-sdk',
'windows-build',
'windows-native-tests',
])
})
it('retains aggregate diagnostics in required builds and protected publication', () => {
const ci = loadWorkflow('.github/workflows/ci.yml')
for (const [name, command] of [
['node-24-consumers', 'pnpm run check:ci:consumers'],
['windows-build', 'pnpm run check:ci:windows-blocking'],
for (const [file, name, command] of [
['ci.yml', 'node-24-consumers', 'pnpm run check:ci:consumers'],
['ci-windows.yml', 'windows-build', 'pnpm run check:ci:windows-blocking'],
] as const) {
const job = workflowJob(ci, name)
const job = workflowJob(loadWorkflow('.github/workflows/' + file), name)
expect(job.steps).toContainEqual(expect.objectContaining({ run: command }))
}
for (const [file, command] of [
@@ -182,7 +237,7 @@ describe('CI workflow', () => {
})
it('skips coverage-history uploads on cancellation but retains Wine cleanup', () => {
const coverage = workflowJob(loadWorkflow('.github/workflows/ci.yml'), 'windows-coverage')
const coverage = workflowJob(loadWorkflow('.github/workflows/ci-windows.yml'), 'windows-coverage')
const wine = workflowJob(loadWorkflow('.github/workflows/ci-master.yml'), 'windows')
expect(coverage.steps).toContainEqual(expect.objectContaining({
name: 'Save coverage duration history', if: '${{ !cancelled() }}',
@@ -191,10 +246,10 @@ describe('CI workflow', () => {
})
it.each([
['node-24-coverage', 'Linux', 'LINUX', 'pnpm run check:ci:coverage'],
['windows-coverage', 'Windows', 'WINDOWS', 'pnpm run check:ci:coverage'],
])('reuses %s coverage timings only within its platform and runner pool', (jobName, platform, variable, command) => {
const job = workflowJob(loadWorkflow('.github/workflows/ci.yml'), jobName)
['node-24-coverage', 'ci.yml', 'Linux', 'LINUX', 'pnpm run check:ci:coverage'],
['windows-coverage', 'ci-windows.yml', 'Windows', 'WINDOWS', 'pnpm run check:ci:coverage'],
])('reuses %s coverage timings only within its platform and runner pool', (jobName, file, platform, variable, command) => {
const job = workflowJob(loadWorkflow('.github/workflows/' + file), jobName)
if (!Array.isArray(job.steps)) throw new TypeError(`${jobName} must define steps`)
const steps = job.steps.filter(isRecord)
const checkout = steps.findIndex(step => step.uses === 'actions/checkout@v7.0.1')
@@ -219,7 +274,7 @@ describe('CI workflow', () => {
})
it('isolates every pnpm action setup destination per runner', () => {
const files = ['.github/workflows/ci.yml', '.github/workflows/ci-master.yml']
const files = ['.github/workflows/ci.yml', '.github/workflows/ci-windows.yml', '.github/workflows/ci-master.yml']
const setups: Array<{ jobName: string; step: unknown }> = []
for (const file of files) {
const workflow: unknown = yaml.load(readFileSync(resolve(root, file), 'utf8'))
@@ -304,13 +359,16 @@ describe('CI workflow', () => {
}
})
it('keeps split native Windows PR jobs with failover, plus a master-only standby', () => {
it('keeps split native Windows PR jobs with failover in their own workflow, plus a master-only standby', () => {
const workflow = loadWorkflow('.github/workflows/ci.yml')
const windowsWorkflow = loadWorkflow('.github/workflows/ci-windows.yml')
const masterWorkflow = loadWorkflow('.github/workflows/ci-master.yml')
if (!isRecord(workflow.jobs)
|| !isRecord(workflow.jobs['windows-build'])
|| !isRecord(workflow.jobs['windows-coverage'])
|| !isRecord(workflow.jobs['windows-native-tests'])
|| !isRecord(windowsWorkflow.jobs)
|| !isRecord(windowsWorkflow.jobs['windows-build'])
|| !isRecord(windowsWorkflow.jobs['windows-coverage'])
|| !isRecord(windowsWorkflow.jobs['windows-native-tests'])
|| !isRecord(windowsWorkflow.jobs['windows-checks-passed'])
|| !isRecord(workflow.jobs['node-24'])
|| !isRecord(workflow.jobs['node-24-coverage'])
|| !isRecord(workflow.jobs['node-24-bench'])
@@ -319,12 +377,13 @@ describe('CI workflow', () => {
|| !isRecord(workflow.jobs['all-checks-passed'])
|| !isRecord(masterWorkflow.jobs)
|| !isRecord(masterWorkflow.jobs['serial-windows'])) {
throw new TypeError('CI workflow must define windows-build, windows-coverage, windows-native-tests, node-24, node-24-coverage, node-24-bench, node-24-consumers, node-compat, and all-checks-passed; ci-master must define serial-windows')
throw new TypeError('CI workflow must define node-24, node-24-coverage, node-24-bench, node-24-consumers, node-compat, and all-checks-passed; CI Windows must define windows-build, windows-coverage, windows-native-tests, and windows-checks-passed; ci-master must define serial-windows')
}
const windowsBuild = workflow.jobs['windows-build']
const windowsCoverage = workflow.jobs['windows-coverage']
const windowsNativeTests = workflow.jobs['windows-native-tests']
const windowsBuild = windowsWorkflow.jobs['windows-build']
const windowsCoverage = windowsWorkflow.jobs['windows-coverage']
const windowsNativeTests = windowsWorkflow.jobs['windows-native-tests']
const windowsAggregate = windowsWorkflow.jobs['windows-checks-passed']
const serialWindows = masterWorkflow.jobs['serial-windows']
const node24 = workflow.jobs['node-24']
const node24Coverage = workflow.jobs['node-24-coverage']
@@ -332,9 +391,10 @@ describe('CI workflow', () => {
const node24Consumers = workflow.jobs['node-24-consumers']
const nodeCompat = workflow.jobs['node-compat']
const aggregate = workflow.jobs['all-checks-passed']
if (!Array.isArray(aggregate.needs)) {
throw new TypeError('CI aggregate must define needs')
if (!Array.isArray(aggregate.needs) || !Array.isArray(windowsAggregate.needs)) {
throw new TypeError('CI and CI Windows aggregates must define needs')
}
expect(Object.keys(workflow.jobs).filter(name => name.startsWith('windows'))).toEqual([])
// The split native jobs all resolve their pool through the Windows switch.
for (const [jobName, job] of [['windows-build', windowsBuild], ['windows-coverage', windowsCoverage], ['windows-native-tests', windowsNativeTests]] as const) {
expect(typeof job['runs-on']).toBe('string')
@@ -414,7 +474,7 @@ describe('CI workflow', () => {
expect(nativeTestCommand).toContain('tool-pwsh/tests/loader.spec.ts')
expect(nativeTestCommand).toContain('workflow-ptc.spec.ts')
expect(workflow.jobs['windows-observational']).toBeUndefined()
expect(windowsWorkflow.jobs['windows-observational']).toBeUndefined()
expect(windowsBuild['continue-on-error']).not.toBe(true)
const observational = buildCommands.find(step => step.id === 'observational')
expect(observational).toMatchObject({
@@ -473,10 +533,13 @@ describe('CI workflow', () => {
expect(serialGate).toBeDefined()
expect(serialGate!.env).toMatchObject({ DSH_COVERAGE_TEST_TIMEOUT_MS: '90000' })
expect(aggregate.needs).not.toContain('windows')
// windows-coverage is temporarily non-blocking while Windows ACP
// half-close tests are stabilized; observational stays out too.
expect(aggregate.needs).not.toContain('windows')
expect(aggregate.needs).toContain('windows-build')
expect(windowsAggregate.needs).toEqual(['windows-build', 'windows-native-tests'])
expect(windowsAggregate.name).toBe('windows checks passed')
expect(windowsAggregate['runs-on']).toBe(aggregate['runs-on'])
expect(windowsAggregate.if).toBe(aggregate.if)
// The benchmark lane is a required verdict input and runs alone so its
// wall-clock budgets never share a runner with a concurrent aggregate.
expect(aggregate.needs).toContain('node-24-bench')
@@ -492,10 +555,7 @@ describe('CI workflow', () => {
env: { DSH_GATE_VERBOSE: '1' },
run: 'pnpm run check:ci:bench',
})
expect(aggregate.needs).not.toContain('windows-coverage')
expect(aggregate.needs).toContain('windows-native-tests')
expect(aggregate.needs).not.toContain('windows-observational')
expect(aggregate.needs).not.toContain('serial-windows')
expect(windowsAggregate.needs).not.toContain('serial-windows')
// Linux failover is a separate switch: the three enterprise Linux workers
// and the verdict job resolve their pool through DSH_CI_FAILOVER_LINUX,
@@ -712,6 +772,18 @@ describe('CI workflow', () => {
}
expect(Object.keys(workflow.on).sort()).toEqual(['push', 'workflow_dispatch'])
expect(Object.keys(prWorkflow.on)).toEqual(['pull_request'])
const windowsWorkflow = loadWorkflow('.github/workflows/ci-windows.yml')
if (!isRecord(windowsWorkflow.on)) throw new TypeError('CI Windows workflow must define on')
expect(Object.keys(windowsWorkflow.on)).toEqual(['pull_request'])
expect(windowsWorkflow.concurrency).toEqual(workflow.concurrency)
// Duplicated env must not drift: a Node bump on one lane only would run
// Linux and Windows against different versions.
expect(windowsWorkflow.env).toEqual(prWorkflow.env)
const pythonWorkflow = loadWorkflow('.github/workflows/ci-python.yml')
if (!isRecord(pythonWorkflow.on)) throw new TypeError('CI Python workflow must define on')
expect(Object.keys(pythonWorkflow.on)).toEqual(['pull_request'])
expect(pythonWorkflow.concurrency).toEqual(workflow.concurrency)
expect(pythonWorkflow.env).toEqual(prWorkflow.env)
// Drills share the parent run’s supersession policy.
for (const name of ['serial-linux-selfhosted', 'serial-windows']) {
@@ -789,27 +861,29 @@ describe('CI workflow', () => {
expect(config).not.toContain('packages/lsp/lsp-stdio/src/instance.ts')
})
it('requires release-shaped Python runtime validation on Linux and Windows x64', () => {
const workflow = loadWorkflow('.github/workflows/ci.yml')
it('requires release-shaped Python runtime validation on Linux and Windows x64 in CI Python', () => {
const workflow = loadWorkflow('.github/workflows/ci-python.yml')
const pythonRuntime = workflowJob(workflow, 'python-runtime')
const aggregate = workflowJob(workflow, 'all-checks-passed')
const aggregate = workflowJob(workflow, 'python-checks-passed')
if (!Array.isArray(aggregate.needs)) {
throw new TypeError('CI aggregate must define required job dependencies')
throw new TypeError('CI Python aggregate must define required job dependencies')
}
expect(pythonRuntime).toMatchObject({
if: "github.event_name == 'pull_request'",
name: 'python runtime / release-shaped matrix',
uses: './.github/workflows/build-exe-for-python-sdk.yml',
with: {
targets: 'node24-linux-x64,node24-win-x64',
ci: true,
},
with: { targets: 'node24-linux-x64,node24-win-x64', ci: true },
secrets: {
DEEPSEEK_API_KEY_EXTERNAL: '${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}',
},
})
expect(aggregate.needs).toContain('python-runtime')
expect(aggregate.needs).toEqual(['python-sdk', 'python-runtime'])
for (const file of ['ci.yml', 'ci-windows.yml']) {
const other = loadWorkflow('.github/workflows/' + file)
if (!isRecord(other.jobs)) throw new TypeError(`${file} must define jobs`)
expect(other.jobs).not.toHaveProperty('python-runtime')
}
})
it('builds SSH executables only for explicit manual or release requests', () => {
@@ -1308,7 +1382,7 @@ describe('Weighted approval workflow', () => {
if: "failure() && steps.revoke.outputs.active == 'true'",
run: 'node .github/review-ownership/check-approval.mjs error',
})
const pythonJob = workflowJob(loadWorkflow('.github/workflows/ci.yml'), 'python-sdk')
const pythonJob = workflowJob(loadWorkflow('.github/workflows/ci-python.yml'), 'python-sdk')
expect(pythonJob.steps).toContainEqual({
name: 'Test production blame scoring',
run: "uv run --python 3.10 --with-requirements .github/review-ownership/requirements.txt python -m unittest discover -s .github/review-ownership -p 'test_*.py'",
@@ -29,6 +29,8 @@ const roots: string[] = []
const dshBuildWorkflows = [
'build-exe-for-python-sdk.yml',
'ci.yml',
'ci-python.yml',
'ci-windows.yml',
'e2e.yml',
'release.yml',
'release-publish.yml',
+11 -6
View File
@@ -49,10 +49,12 @@ function evaluateCondition(expression: string, cancelled: boolean, results: stri
}, { timeout: 1000 }) as boolean
}
const verdicts = [['ci.yml', 'all-checks-passed'], ['ci-windows.yml', 'windows-checks-passed'], ['ci-python.yml', 'python-checks-passed']] as const
describe('master-only platform scheduling', () => {
it.each(['success', 'failure', 'skipped', 'cancelled'])(
'reports %s dependencies in active runs but never starts a cancelled-run verdict', (result) => {
const aggregate = workflow('ci.yml').jobs['all-checks-passed']!
it.each(verdicts.flatMap(([file, job]) => ['success', 'failure', 'skipped', 'cancelled'].map(result => [file, job, result] as const)))(
'%s %s reports %s dependencies in active runs but never starts a cancelled-run verdict', (file, job, result) => {
const aggregate = workflow(file).jobs[job]!
const results = aggregate.needs!.map(() => 'success')
results[0] = result
const condition = aggregate.if as string
@@ -72,8 +74,8 @@ describe('master-only platform scheduling', () => {
expect(evaluateCondition(workflow('ci.yml').jobs['all-checks-passed']!.if as string, true, ['success'])).toBe(false)
})
it('keeps only Linux and Windows x64 runtimes in required PR CI', () => {
const pr = workflow('ci.yml')
it('keeps the required Python PR verdict and its two x64 targets in CI Python', () => {
const pr = workflow('ci-python.yml')
expect(Object.keys(pr.on)).toEqual(['pull_request'])
expect(pr.jobs['python-runtime']).toMatchObject({
if: "github.event_name == 'pull_request'",
@@ -82,7 +84,7 @@ describe('master-only platform scheduling', () => {
})
expect(pr.jobs.windows).toBeUndefined()
expect(JSON.stringify(pr.jobs)).not.toMatch(/wine-windows-gates|check:windows-wine/)
const aggregate = pr.jobs['all-checks-passed']!
const aggregate = pr.jobs['python-checks-passed']!
expect(aggregate.needs).toContain('python-runtime')
expect(aggregate.needs).not.toContain('windows')
expect(aggregate.needs!.every(id => id in pr.jobs)).toBe(true)
@@ -90,6 +92,9 @@ describe('master-only platform scheduling', () => {
expect(aggregate.steps).toContainEqual(expect.objectContaining({
if: "contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped')",
}))
for (const file of ['ci.yml', 'ci-windows.yml']) {
expect(workflow(file).jobs['python-runtime']).toBeUndefined()
}
})
it('runs all three deferred carriers on master pushes with fail-loud API credentials', () => {