From ab1ee996852a6e7ffe482f86ea7c9ea4ed931ded Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 6 Sep 2026 12:58:21 +0800 Subject: [PATCH 1/6] ci: defer macOS ARM runtime and Wine checks to master --- ...rial-cross-platform-ci-reference.i18n.yaml | 4 +- ...7-21-serial-cross-platform-ci-reference.md | 6 +- ...1-serial-cross-platform-ci-reference.zh.md | 6 +- .../2026-07-26-ci-failover-runbook.i18n.yaml | 4 +- .../process/2026-07-26-ci-failover-runbook.md | 4 +- .../2026-07-26-ci-failover-runbook.zh.md | 4 +- ...8-native-windows-pull-request-ci.i18n.yaml | 4 +- ...26-08-08-native-windows-pull-request-ci.md | 4 +- ...08-08-native-windows-pull-request-ci.zh.md | 4 +- ...26-09-06-master-only-platform-ci.i18n.yaml | 6 + .../2026-09-06-master-only-platform-ci.md | 33 ++++++ .../2026-09-06-master-only-platform-ci.zh.md | 33 ++++++ ...talled-python-wheel-black-box-ci.i18n.yaml | 4 +- ...-23-installed-python-wheel-black-box-ci.md | 8 +- ...-installed-python-wheel-black-box-ci.zh.md | 8 +- .../workflows/build-exe-for-python-sdk.yml | 4 +- .github/workflows/ci-master.yml | 95 ++++++++++++--- .github/workflows/ci.yml | 98 +-------------- docs/testing.i18n.yaml | 4 +- docs/testing.md | 2 + docs/testing.zh.md | 2 + python/development.i18n.yaml | 4 +- python/development.md | 4 +- python/development.zh.md | 4 +- scripts/ci-workflow.spec.ts | 38 ++---- scripts/tests/ci-master-platforms.spec.ts | 112 ++++++++++++++++++ scripts/wine-windows-gates.sh | 4 +- 27 files changed, 321 insertions(+), 182 deletions(-) create mode 100644 .agents/notes/implemented/process/2026-09-06-master-only-platform-ci.i18n.yaml create mode 100644 .agents/notes/implemented/process/2026-09-06-master-only-platform-ci.md create mode 100644 .agents/notes/implemented/process/2026-09-06-master-only-platform-ci.zh.md create mode 100644 scripts/tests/ci-master-platforms.spec.ts diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml index c7bb8c6d0a..3263246d05 100644 --- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md -2026-07-21-serial-cross-platform-ci-reference.md: e7d1caefabe3e90a84ea8eaea67381ea4f63f6f7 -2026-07-21-serial-cross-platform-ci-reference.zh.md: 57279e662aaf0c361bc2641cc5c6e757c4199ebb +2026-07-21-serial-cross-platform-ci-reference.md: e92104cce5b726797f7b4d88c98cf3b837cba6b4 +2026-07-21-serial-cross-platform-ci-reference.zh.md: a8787006b7d44ecab94f01b771b62bfa0ae3224b diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md index e7d1caefab..e92104cce5 100644 --- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md +++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md @@ -16,7 +16,7 @@ Real-kernel sandbox proofs require specific hosted operating systems and archite ## Decision -[CI](../../../../.github/workflows/ci.yml) (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 consolidated Linux and Wine-hosted Windows jobs plus the Node compatibility and Python contracts on standard GitHub-hosted capacity; an independent native Windows job reports the complete Windows inventory without participating in the required aggregate. On a push to `master`, the active references are `serial / linux (self-hosted standby)` on the in-house `vm-backup` pool and `serial / windows (self-hosted standby)` on the in-house `dsh-win-ci` pool — the hot-standby drills that continuously re-prove the failover targets described in the [failover runbook](2026-07-26-ci-failover-runbook.md). 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) (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, and Python checks; [platform scheduling](2026-09-06-master-only-platform-ci.md) 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. @@ -28,13 +28,13 @@ The standalone [Sandbox](../../../../.github/workflows/sandbox.yml) workflow bel Master reference jobs are diagnostic and do not participate in the pull request's required `all checks passed` result. The ci-master and Sandbox workflows keep their cross-platform references on master pushes. Performance is evaluated from completed hosted-job timestamps and reported as a measurement; it is not encoded as a `timeout-minutes` value. -The active serial references run on the self-hosted `vm-backup` (`serial / linux`) and `dsh-win-ci` (`serial / windows`) pools; the one remaining disabled hosted serial reference (`serial-macos`) uses `macos-latest`, and there is no standard-hosted `serial / linux` label. The required pull-request Windows job runs under Wine on `ubuntu-latest`, while the independent pull-request native job uses the hosted `dsh-windows-2025-16core` runner under normal operation and the self-hosted `[self-hosted, dsh-win-ci, windows]` pool under failover (see the [failover runbook](2026-07-26-ci-failover-runbook.md)), and is absent from the required aggregate under the [dual Windows decision](2026-08-08-native-windows-pull-request-ci.md). Required pull-request jobs use portable standard capacity under the [required-CI decision](../../archived/process/2026-07-23-portable-required-pull-request-ci.md). Higher-core hosted runners remain manual benchmarks because a correctness path must remain runnable without repository-external runner configuration. +The active serial references run on the self-hosted `vm-backup` (`serial / linux`) and `dsh-win-ci` (`serial / windows`) pools; the one remaining disabled hosted serial reference (`serial-macos`) uses `macos-latest`, and there is no standard-hosted `serial / linux` label. The master-only Wine job runs on `ubuntu-latest`, while the pull-request native jobs use the hosted `dsh-windows-2025-16core` runner under normal operation and the self-hosted `[self-hosted, dsh-win-ci, windows]` pool under failover (see the [failover runbook](2026-07-26-ci-failover-runbook.md)), with build and targeted process checks required under the [native Windows decision](2026-08-08-native-windows-pull-request-ci.md). Required pull-request jobs use portable standard capacity under the [required-CI decision](../../archived/process/2026-07-23-portable-required-pull-request-ci.md). Higher-core hosted runners remain manual benchmarks because a correctness path must remain runnable without repository-external runner configuration. ## Alternatives considered - **Set each timeout equal to its latency target** - rejected because scheduling variance would cancel correct work and suppress the evidence needed to diagnose a regression. - **Trust only the concurrent primary inventory** - rejected because scheduling and validation share implementation assumptions; a serial aggregate is an independent completeness check. -- **Run the serial references on every pull request** - rejected because they duplicate complete cross-platform aggregates and add macOS work to every change; the required jobs already execute the blocking Linux and Wine-hosted Windows contracts, and the independent native job supplies the complete Windows result. +- **Run the serial references on every pull request** - rejected because they duplicate complete cross-platform aggregates and add macOS work to every change; the required jobs already execute the blocking Linux and native Windows checks, and the other native jobs supply the complete Windows result. - **Run the real-kernel Sandbox matrix on every pull request** - rejected because its four statuses do not participate in branch protection, while repeated installs, Landlock builds, and macOS unit parity consume runner capacity without changing the merge verdict. The master run retains the platform and installed-launcher signal. - **Use one operating-system matrix** - rejected because three named jobs make the reference surface visible without another selection mechanism. - **Run the serial reference on larger runners** - rejected because both required CI and its independent reference must remain runnable when organization-owned pools cannot allocate jobs. diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md index 57279e662a..a8787006b7 100644 --- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md +++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md @@ -16,7 +16,7 @@ Status: implemented ## 决策 -[CI](../../../../.github/workflows/ci.yml)(仅 pull request)与 [CI master](../../../../.github/workflows/ci-master.yml)(master 推送 + `workflow_dispatch`)为拉取请求事件与 master 推送事件赋予互补的职责。拉取请求在 GitHub 标准托管容量上运行合并后的 Linux 和由 Wine 承载的 Windows 作业,以及 Node 兼容性与 Python 约定;一个独立的原生 Windows 作业会报告完整的 Windows 清单,但不参与必需聚合流程。向 `master` 推送时,当前启用的参考作业是公司自有 `vm-backup` 池上的 `serial / linux (self-hosted standby)` 和 `dsh-win-ci` 池上的 `serial / windows (self-hosted standby)`——这些热备演练持续验证[故障切换手册](2026-07-26-ci-failover-runbook.zh.md)所描述的切换目标。不存在标准托管的 `serial / linux` 定义;标准托管的 `serial / macos` 仍处于禁用状态,并由 `TODO(hosted-serial-ci)` 标记,直到其可移植容量恢复。当前 `serial / windows` 定义是公司自有 `dsh-win-ci` 池的 standby。各自独立的作业定义有意显式保留简短的代码检出、运行时设置和依赖锁定的安装步骤,而不是用矩阵或可复用工作流隐藏操作系统差异。`workflow_dispatch` 仅用于运行器基准测试。 +[CI](../../../../.github/workflows/ci.yml)(仅 pull request)与 [CI master](../../../../.github/workflows/ci-master.yml)(master 推送 + `workflow_dispatch`)为拉取请求事件与 master 推送事件赋予互补的职责。拉取请求运行 Linux、原生 Windows、Node 兼容性与 Python 检查;[平台调度](2026-09-06-master-only-platform-ci.zh.md)将 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 则在安装采用符号链接的工作区前启用开发人员模式。 @@ -28,13 +28,13 @@ macOS 参考流程使用 fork 进程运行常规 Vitest 项目。macOS arm64 上 master 分支的参考作业仅用于诊断,不参与拉取请求所要求的 `all checks passed` 结果。ci-master 与 Sandbox 工作流把跨平台参考流程保留在 master 推送上。系统根据已完成托管作业的时间戳评估性能,并将其报告为测量结果,而不是写成 `timeout-minutes` 值。 -当前启用的参考流程运行在公司自有 `vm-backup`(`serial / linux`)与 `dsh-win-ci`(`serial / windows`)自托管池上;唯一剩余的禁用托管参考作业(`serial-macos`)使用 `macos-latest`,且不存在标准托管的 `serial / linux` 标签。拉取请求必需的 Windows 作业在 `ubuntu-latest` 上通过 Wine 运行,而独立的拉取请求原生作业在正常运行下使用托管的 `dsh-windows-2025-16core` 运行器,故障切换时使用自托管 `[self-hosted, dsh-win-ci, windows]` 池(参见[故障切换手册](2026-07-26-ci-failover-runbook.zh.md)),依据[双 Windows 决策](2026-08-08-native-windows-pull-request-ci.zh.md)不参与必需聚合流程。依据[必需 CI 决策](../../archived/process/2026-07-23-portable-required-pull-request-ci.md),拉取请求必需作业使用可移植的标准容量。更高核心数的托管运行器仍仅用于手动基准测试,因为正确性路径必须无需仓库外部的运行器配置即可运行。 +当前启用的参考流程运行在公司自有 `vm-backup`(`serial / linux`)与 `dsh-win-ci`(`serial / windows`)自托管池上;唯一剩余的禁用托管参考作业(`serial-macos`)使用 `macos-latest`,且不存在标准托管的 `serial / linux` 标签。仅 master 触发的 Wine 作业在 `ubuntu-latest` 上运行,而拉取请求原生作业在正常运行下使用托管的 `dsh-windows-2025-16core` 运行器,故障切换时使用自托管 `[self-hosted, dsh-win-ci, windows]` 池(参见[故障切换手册](2026-07-26-ci-failover-runbook.zh.md)),依据[原生 Windows 决策](2026-08-08-native-windows-pull-request-ci.zh.md),其中构建与定向进程检查参与必需聚合流程。依据[必需 CI 决策](../../archived/process/2026-07-23-portable-required-pull-request-ci.md),拉取请求必需作业使用可移植的标准容量。更高核心数的托管运行器仍仅用于手动基准测试,因为正确性路径必须无需仓库外部的运行器配置即可运行。 ## 曾考虑的替代方案 - **将每个超时值设为相应延迟目标**:不予采纳,因为调度波动会中止原本正确的执行,并使诊断回归所需的证据无法产生。 - **仅信任并发执行的主门禁清单**:不予采纳,因为调度逻辑与校验逻辑共享实现假设;串行聚合流程是一项独立的完整性检查。 -- **在每个拉取请求上运行串行参考作业**:不予采纳,因为这些作业会重复完整的跨平台聚合流程,并为每项改动增加 macOS 工作;必需作业已经执行阻塞性的 Linux 和由 Wine 承载的 Windows 约定,而独立原生作业提供完整的 Windows 结果。 +- **在每个拉取请求上运行串行参考作业**:不予采纳,因为这些作业会重复完整的跨平台聚合流程,并为每项改动增加 macOS 工作;必需作业已经执行阻塞性的 Linux 和原生 Windows 检查,而其它原生作业提供完整的 Windows 结果。 - **在每个拉取请求上运行真实内核 Sandbox 矩阵**:不予采纳,因为它的四个状态不参与分支保护,而重复安装、Landlock 构建以及为保持平台一致而运行的 macOS 单元测试会消耗运行器容量,却不会改变合并裁决。master 上的运行保留平台与已安装 launcher 的信号。 - **使用一个操作系统矩阵**:不予采纳,因为三个具名作业无需另一套选择机制,就能让参考流程的构成清晰可见。 - **在大型运行器上运行串行参考流程**:不予采纳,因为当组织自有运行器池无法分配作业时,必需 CI 及其独立参考流程都必须仍可运行。 diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml index a00d6ced10..50e7fa9040 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md -2026-07-26-ci-failover-runbook.md: a3c824cb54f5f24d02cee256c1e384061ac457f8 -2026-07-26-ci-failover-runbook.zh.md: 114b14dd5edcd8dab6bda40b78342738e3513392 +2026-07-26-ci-failover-runbook.md: 9fbdd76ce3a376ea5b4e86584f14c3558bddff9e +2026-07-26-ci-failover-runbook.zh.md: fda030d9c628709c31ec53e767c74989f130a7b1 diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md index a3c824cb54..9fbdd76ce3 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md @@ -6,7 +6,7 @@ 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 verdict's other required dependencies (`node-24-bench`, `node-compat`, `python-sdk`, `windows`) stay on standard hosted runners by design (the portable boundary); in a broader GitHub-hosted capacity failure that also takes out the standard pools, those dependencies still block `all checks passed`. An outage therefore needs a switch any responder with repository write access can throw without merging anything. +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 verdict's other required dependencies (`node-24-bench`, `node-compat`, `python-sdk`, `python-runtime`) stay on standard hosted runners by design (the portable boundary); in a broader GitHub-hosted capacity failure that also takes out the standard pools, those dependencies still block `all checks passed`. An outage therefore needs a switch any responder with repository write access can throw without merging anything. ## Decision @@ -16,7 +16,7 @@ Each of the three required Linux worker jobs, the native Windows jobs, and the ` The exemption is narrower than "a drill always finishes", in two ways. GitHub keeps a single pending entry per group, so a newer pending run displaces an older one and intermediate push runs still end as `cancelled` during busy periods. And the expression is evaluated against the *newly triggered* run, so a run whose own event is not `push` — a benchmark dispatched on master within `ci-master.yml`, sharing its group `CI master-` — evaluates to `true` and does cancel a drill that is mid-flight. That is a rare manual action and the next master push restores the evidence, so it does not warrant further mechanism. What the carve-out buys is that the lane periodically reaches a verdict at all, which is what makes it usable as evidence. -The decision belongs at workflow level because cancellation applies to the whole superseded run: a job-level `concurrency` group does not exempt its job. The negated form is load-bearing rather than cosmetic: naming `pull_request` alone would also stop cancelling `workflow_dispatch`, and each runner benchmark fans out to twelve larger runners for up to fifteen minutes inside this same group on master, so a re-dispatch would queue ahead of a drill instead of replacing a stale measurement. What bounds the cost is that a master push in `ci-master.yml` carries only `wine-apt-cache` and these two drills; the pull-request jobs live in the separate `ci.yml` (which does not see `push`), and the benchmarks are `workflow_dispatch`-gated within `ci-master.yml`. `scripts/ci-workflow.spec.ts` pins that push-reachable set — classifying by exact condition, since a negated event test mentions the event it excludes — so a new push-reachable job cannot quietly start accumulating uncancelled runs. +The decision belongs at workflow level because cancellation applies to the whole superseded run: a job-level `concurrency` group does not exempt its job. The negated form is load-bearing rather than cosmetic: naming `pull_request` alone would also stop cancelling `workflow_dispatch`, and each runner benchmark fans out to twelve larger runners for up to fifteen minutes inside this same group on master, so a re-dispatch would queue ahead of a drill instead of replacing a stale measurement. What bounds the cost is that a master push in `ci-master.yml` carries the [post-merge runtime and Wine checks](2026-09-06-master-only-platform-ci.md) and these two drills; the pull-request jobs live in the separate `ci.yml` (which does not see `push`), and the benchmarks are `workflow_dispatch`-gated within `ci-master.yml`. `scripts/ci-workflow.spec.ts` pins that push-reachable set — classifying by exact condition, since a negated event test mentions the event it excludes — so a new push-reachable job cannot quietly start accumulating uncancelled runs. ### Release rehearsals share the Linux switch diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md index 114b14dd5e..fda030d9c6 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md @@ -6,7 +6,7 @@ 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-24-bench`、`node-compat`、`python-sdk`、`windows`)按设计留在标准托管运行器上(可移植边界);若更大范围的 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)运行在托管的 `dsh-windows-2025-16core` 大型运行器上。当企业池发生故障——作业无限排队或企业标签消失——所有开启的拉取请求都无法合并,而"合并一个修复"这一常规恢复手段本身正被那些无法运行的必需检查死锁。**适用范围:两个独立开关,每个平台一个。**`DSH_CI_FAILOVER_LINUX` 恢复企业级 Linux 池故障(三个必需的 Linux 工作作业加 `all checks passed` 判定作业);`DSH_CI_FAILOVER_WINDOWS` 恢复托管 Windows 池故障(原生 Windows 作业)。Linux 池故障无需重定向 Windows 作业,反之亦然。判定作业的其余必需依赖(`node-24-bench`、`node-compat`、`python-sdk`、`python-runtime`)按设计留在标准托管运行器上(可移植边界);若更大范围的 GitHub 托管容量故障连标准池一并击倒,这些依赖仍会阻塞 `all checks passed`。因此故障需要一个任何具备仓库写权限的响应者都能在不合并任何代码的情况下触发的开关。 ## 决策 @@ -16,7 +16,7 @@ Status: implemented 这项豁免比「演练总能跑完」要窄,有两点限制。其一,GitHub 每个组只保留一个待运行条目,更新的待运行条目会顶掉更早的,繁忙时段中间的推送运行仍会以 `cancelled` 结束。其二,该表达式是针对**新触发的运行**求值的,因此自身事件不是 `push` 的运行——例如在 `ci-master.yml` 内的 master 上派发的基准测试,与其演练共用 `CI master-` 组——求值为 `true`,会取消正在运行中的演练。这属于罕见的手动操作,且下一次 master 推送即可恢复证据,因此不值得为它再加机制。这项豁免换来的是该通道**周期性**地得出结论,而这正是它能作为证据的前提。 -这个决定必须放在工作流级:取消作用于被取代的整个运行,作业级 `concurrency` 组并不能豁免其所属作业。采用否定式写法而非仅指名 `pull_request`,是有实质作用的:后者会连 `workflow_dispatch` 一起停止取消,而每次运行器基准测试会在 master 上的同一并发组内同时占用 12 台大规格运行器、最长 15 分钟,届时重复派发会排在演练之前,而不是替换掉已过时的测量。成本之所以可控,是因为 `ci-master.yml` 中一次 master 推送只承载 `wine-apt-cache` 和这两条演练;拉取请求作业位于独立的 `ci.yml`(不监听 `push`),而基准测试在 `ci-master.yml` 内受 `workflow_dispatch` 门控。`scripts/ci-workflow.spec.ts` 会锁定这个推送可达集合——按条件精确匹配,因为否定式事件判断会包含它所排除的事件名——使新的推送可达作业无法悄悄开始累积未取消的运行。 +这个决定必须放在工作流级:取消作用于被取代的整个运行,作业级 `concurrency` 组并不能豁免其所属作业。采用否定式写法而非仅指名 `pull_request`,是有实质作用的:后者会连 `workflow_dispatch` 一起停止取消,而每次运行器基准测试会在 master 上的同一并发组内同时占用 12 台大规格运行器、最长 15 分钟,届时重复派发会排在演练之前,而不是替换掉已过时的测量。成本之所以可控,是因为 `ci-master.yml` 中一次 master 推送承载[合并后的运行时与 Wine 检查](2026-09-06-master-only-platform-ci.zh.md)和这两条演练;拉取请求作业位于独立的 `ci.yml`(不监听 `push`),而基准测试在 `ci-master.yml` 内受 `workflow_dispatch` 门控。`scripts/ci-workflow.spec.ts` 会锁定这个推送可达集合——按条件精确匹配,因为否定式事件判断会包含它所排除的事件名——使新的推送可达作业无法悄悄开始累积未取消的运行。 ### 发布演练共用 Linux 开关 diff --git a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml index 2ad645d695..d3e10ab382 100644 --- a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml +++ b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md -2026-08-08-native-windows-pull-request-ci.md: ade3b19bc1adbcd75ec7d3908670b9664186cba8 -2026-08-08-native-windows-pull-request-ci.zh.md: 10db2657a7f02813152d2905693627e44ce6caf7 +2026-08-08-native-windows-pull-request-ci.md: 511d3d146282c9d5635ab72e9ad86a87a899b22a +2026-08-08-native-windows-pull-request-ci.zh.md: 75bd3e14907f9ccd81e9ae229a08f8e13e7cb7ca diff --git a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md index ade3b19bc1..511d3d1462 100644 --- a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md +++ b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md @@ -12,7 +12,7 @@ A coverage audit found that stale branch state had restored temporary exclusions ## Decision -The required `windows` job in [ci.yml](../../../../.github/workflows/ci.yml) remains `windows node 24 / wine blocking` 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. The stable `windows` job id remains a dependency of `all checks passed`. 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. +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](2026-09-06-master-only-platform-ci.md). 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 also starts four independent native jobs on the organization-owned `dsh-windows-2025-16core` runner: `windows-build`, `windows-coverage`, `windows-native-tests`, and `windows-observational`. 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 retargets all four jobs to the in-house pool. Per-job deadlines range from 60 to 120 minutes and bound stuck work without treating a performance target as a correctness deadline. @@ -50,7 +50,7 @@ Shiki disables lazy TextMate-regex compilation and warms each boot grammar befor ## Consequences -Wine preserves the required aggregate's existing critical path and job identity. Native coverage and observational results can still be pending or red when `all checks passed` turns green, so branch protection consumes Wine plus the targeted native build and process checks while reviewers and follow-up automation consume the remaining native results. +Wine provides post-merge toolchain evidence. Native coverage and observational results can still be pending or red when `all checks passed` turns green, so branch protection consumes the targeted native build and process checks while reviewers and follow-up automation consume the remaining native results. Every pull request nevertheless receives a real NT kernel, NTFS, PowerShell, Windows process, native addon, and supported-source coverage signal. The native jobs duplicate setup across the build, coverage, and observational workspaces and repeat builds in the build and observational ones, but they lower each job's process count and expose path, watcher, lifecycle, and fixture defects hidden by the compatibility lane. diff --git a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md index 10db2657a7..75bd3e1490 100644 --- a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md +++ b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md @@ -12,7 +12,7 @@ Status: implemented ## 决策 -[ci.yml](../../../../.github/workflows/ci.yml) 中必需的 `windows` 作业仍是在 `ubuntu-latest` 上运行的 `windows node 24 / wine blocking`。它保留经过校验和验证的 Windows Node、Wine apt 与 pnpm 缓存、仅限工作区快照的 hoisted 安装,以及运行工作区构建与生产网站的[共享 Wine 门禁脚本](../../../../scripts/wine-windows-gates.sh)。Node 分发文件传输采用有界重试;nodejs.org 的大文件传输停滞时,由支持范围请求的传输镜像续传相同字节,但版本和 SHA-256 权威仍属于 nodejs.org,归档通过该校验前绝不会投入使用。稳定的 `windows` 作业 ID 仍是 `all checks passed` 的依赖项。[已归档的 Wine 实验](../../archived/process/2026-07-27-wine-windows-gates-experiment.md)保留其实测取舍,而本文负责当前双通道拓扑。 +[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 平台策略](2026-09-06-master-only-platform-ci.zh.md),Wine 不参与 PR 聚合。[已归档的 Wine 实验](../../archived/process/2026-07-27-wine-windows-gates-experiment.md)保留其实测取舍,而本文负责当前双通道拓扑。 每个拉取请求还会在组织自有的 `dsh-windows-2025-16core` 运行器上启动 4 个相互独立的原生作业:`windows-build`、`windows-coverage`、`windows-native-tests` 与 `windows-observational`。每个作业都会为工作区符号链接启用开发人员模式,通过 `pnpm/action-setup` 提供仓库固定版本的 pnpm,在不传输 store 归档的情况下执行不可变安装,并在原生 PowerShell 下运行自己的清单。Windows 故障切换变量会把这 4 个作业全部重定向到公司内部运行器池。各作业采用 60 至 120 分钟的截止时间,以约束卡住的工作,同时不把性能目标当作正确性截止时间。 @@ -50,7 +50,7 @@ Shiki 会禁用 TextMate 正则的延迟编译,并在用户内容进入保持 ## 后果 -Wine 保留必需聚合流程现有的关键路径和作业身份。`all checks passed` 变绿时,原生覆盖率与观测性结果仍可能处于待处理或红灯状态,因此分支保护采用 Wine 加定向原生构建和进程检查,而评审者和后续自动化采用其余原生结果。 +Wine 提供合并后的工具链证据。`all checks passed` 变绿时,原生覆盖率与观测性结果仍可能处于待处理或红灯状态,因此分支保护采用定向原生构建和进程检查,而评审者和后续自动化采用其余原生结果。 尽管如此,每个拉取请求都会获得真实 NT 内核、NTFS、PowerShell、Windows 进程、原生插件和受支持源码覆盖率信号。原生作业会在构建、覆盖率与观测性工作区中重复设置流程,并在构建与观测性工作区中重复构建,但它们会降低每个作业的进程数,并暴露兼容性通道掩盖的路径、watcher、生命周期与 fixture 缺陷。 diff --git a/.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.i18n.yaml b/.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.i18n.yaml new file mode 100644 index 0000000000..1ce78b6e0f --- /dev/null +++ b/.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-09-06-master-only-platform-ci.md +2026-09-06-master-only-platform-ci.md: 28284206c8c6d3fbb5de8ecadbcdf2035a5bb8c0 +2026-09-06-master-only-platform-ci.zh.md: eed843b0d235c80256343890e91b1de84f482174 diff --git a/.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.md b/.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.md new file mode 100644 index 0000000000..28284206c8 --- /dev/null +++ b/.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.md @@ -0,0 +1,33 @@ +# Agent Note: Master-only platform CI + +Status: implemented + +English | [中文](2026-09-06-master-only-platform-ci.zh.md) + +## Problem + +Python runtime builds on macOS Intel and ARM and Linux ARM64, plus Windows build/site checks through Wine, consume paid hosted capacity on each pull-request revision. Native Linux and Windows x64 already provide required executable and installed-wheel evidence, and native Windows checks cover the build and process behavior before merge. + +## Decision + +[CI](../../../../.github/workflows/ci.yml) requires Python runtime validation on Linux x64 and Windows x64. [CI master](../../../../.github/workflows/ci-master.yml) selects Linux ARM64, macOS ARM64, and macOS x64 through the same reusable builder on master pushes only. Both callers pass `ci: true` and the explicit external API secret, preserving complete keyless installed-wheel scenarios and fail-loud trusted live tests. Fork and Dependabot pull requests remain keyless; runner trust and fallback selectors are unchanged. Python releases retain all five targets. + +Wine runs once as an independent hosted Ubuntu master job. Its existing image-keyed apt cache restore/save also supplies default-branch cache production, so it needs no separate cache-seeding job. The native Linux and Windows serial aggregates do not invoke Wine. Keeping Wine hosted avoids shared-host apt transactions and shared Wine-prefix cleanup on the persistent Linux VM. The script owns a scratch snapshot, a checkout-local Wine prefix, and a checksum-verified Windows Node cache; provisioning, failure propagation, and always-run cleanup remain intact. + +The parent and reusable runtime workflows preserve running master-push checks against subsequent master pushes. GitHub concurrency still permits replacement of pending runs; manual benchmarks can cancel the parent run. A master push schedules all three selected carriers but does not guarantee every intermediate commit reaches a result. PR, manual, and release cancellation retain their existing behavior. + +This decision partially supersedes scheduling in the [installed-wheel validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md), [native Windows CI](2026-08-08-native-windows-pull-request-ci.md), [serial references](2026-07-21-serial-cross-platform-ci-reference.md), and [failover runbook](2026-07-26-ci-failover-runbook.md). Those notes remain active for artifact provenance, platform fidelity, serial completeness, and trust rules. + +## Alternatives considered + +**Keep every target and Wine required on pull requests.** This detects platform-specific defects before merge but repeats paid native builds for every revision. The chosen policy explicitly accepts post-merge discovery for these four checks. + +**Wait until release or require manual dispatch.** This loses the automatic default-branch signal. Master pushes retain scheduled checks without shrinking the release matrix. + +**Fold Wine into a self-hosted serial aggregate.** The aggregate does not already cover Wine. Adding it would change persistent-host dependencies, shared cache ownership, and cleanup isolation; the scheduling optimization does not need that migration. + +## Consequences + +A macOS, Linux ARM64, or Wine-specific regression can merge while required PR checks are green. Master failures remain ordinary failing jobs, not `continue-on-error` observations. Linux/Windows x64 installed-wheel checks and native Windows build/process checks continue to block the PR aggregate; its dependencies never name the removed Wine PR job. + +The [routing regression](../../../../scripts/tests/ci-master-platforms.spec.ts) runs through the existing script-spec coverage inventory and checks target partitioning, master-only conditions, credential forwarding, cancellation, Wine uniqueness, valid aggregate dependencies, and the full release matrix. Executed negative controls remove the Intel target, misroute Wine, and restore the stale aggregate dependency; each produces its intended failure. Real platform execution remains CI-owned; local scheduling tests do not claim native runtime or Wine execution. diff --git a/.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.zh.md b/.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.zh.md new file mode 100644 index 0000000000..eed843b0d2 --- /dev/null +++ b/.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.zh.md @@ -0,0 +1,33 @@ +# Agent Note: 仅 master 执行的平台 CI + +Status: implemented + +[English](2026-09-06-master-only-platform-ci.md) | 中文 + +## Problem + +macOS Intel、ARM 与 Linux ARM64 上的 Python 运行时构建,以及通过 Wine 执行的 Windows 构建和网站检查,会在每次拉取请求修订时消耗付费托管容量。原生 Linux 与 Windows x64 已提供必需的可执行文件和安装后 wheel 包证据,原生 Windows 检查也会在合并前覆盖构建与进程行为。 + +## Decision + +[CI](../../../../.github/workflows/ci.yml) 要求 Linux x64 与 Windows x64 上的 Python 运行时验证。[CI master](../../../../.github/workflows/ci-master.yml) 仅在 master 推送时通过同一可复用构建器选择 Linux ARM64、macOS ARM64 与 macOS x64。两个调用方均传入 `ci: true` 和显式外部 API 密钥,保留完整的无密钥安装后 wheel 包场景及可信 live 测试的明确失败。Fork 与 Dependabot 拉取请求仍不带密钥;运行器信任与回退选择器保持不变。Python 发布保留全部五个目标。 + +Wine 作为独立的托管 Ubuntu master 作业运行一次。其现有的按镜像标识的 apt 缓存恢复和保存也负责生成默认分支缓存,因此不需要单独的缓存预热作业。原生 Linux 与 Windows 串行聚合不调用 Wine。Wine 保持托管运行,避免在持久 Linux VM 上执行共享宿主机 apt 事务和共享 Wine prefix 清理。脚本负责临时快照、checkout 内的 Wine prefix 和经过校验和验证的 Windows Node 缓存;环境准备、失败传播及始终执行的清理保持不变。 + +父工作流与可复用运行时工作流均保留正在执行的 master 推送检查,不被后续 master 推送取消。GitHub 并发机制仍允许替换待执行的运行;手动基准测试可以取消父工作流。master 推送会调度全部三个选定载体,但不保证每个中间提交都得到结果。PR(Pull Request)、手动和发布运行的取消行为保持不变。 + +本决策部分取代[安装后 wheel 包验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)、[原生 Windows CI](2026-08-08-native-windows-pull-request-ci.zh.md)、[串行参考](2026-07-21-serial-cross-platform-ci-reference.zh.md)和[故障切换手册](2026-07-26-ci-failover-runbook.zh.md)中的调度策略。这些记录仍保留产物来源、平台保真度、串行完整性与信任规则的决策价值。 + +## Alternatives considered + +**在拉取请求上保留全部目标和 Wine 必需检查。** 这能在合并前发现平台特定缺陷,但会在每次修订时重复付费原生构建。所选策略明确接受这四项检查在合并后发现问题。 + +**等到发布或要求手动派发。** 这会失去自动的默认分支信号。master 推送保留定期触发的检查,不缩减发布矩阵。 + +**把 Wine 合入自托管串行聚合。** 聚合并未覆盖 Wine。加入它会改变持久宿主机依赖、共享缓存归属与清理隔离;此次调度优化不需要这种迁移。 + +## Consequences + +macOS、Linux ARM64 或 Wine 特定回归可能在必需 PR 检查为绿时合并。master 失败仍是普通失败作业,不是 `continue-on-error` 观测项。Linux/Windows x64 安装后 wheel 包检查及原生 Windows 构建和进程检查继续阻塞 PR 聚合;其依赖绝不引用已移除的 Wine PR 作业。 + +[路由回归测试](../../../../scripts/tests/ci-master-platforms.spec.ts) 通过现有脚本 spec 覆盖率清单运行,检查目标划分、仅 master 条件、凭据传递、取消、Wine 唯一性、聚合依赖有效性及完整发布矩阵。已执行的负对照移除 Intel 目标、错误路由 Wine 并恢复失效聚合依赖;每项均产生预期失败。真实平台执行仍由 CI 负责;本地调度测试不声称执行了原生运行时或 Wine。 diff --git a/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.i18n.yaml b/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.i18n.yaml index 9d2528abc4..6593e72d9c 100644 --- a/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.md -2026-08-23-installed-python-wheel-black-box-ci.md: 3f9bc480cf050b88e08e4d15b8a6827c5677dc76 -2026-08-23-installed-python-wheel-black-box-ci.zh.md: 06fe92bead466ed383d9b12776e5db61625867c8 +2026-08-23-installed-python-wheel-black-box-ci.md: 8821b93d0d1c5a32c7dbd97f67d78f7559769791 +2026-08-23-installed-python-wheel-black-box-ci.zh.md: 05972560816193ff0b93323e6dcbea3ad5020215 diff --git a/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.md b/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.md index 3f9bc480cf..8821b93d0d 100644 --- a/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.md +++ b/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.md @@ -24,17 +24,17 @@ Linux additionally retains its manylinux 2.28 clean-install smoke and GLIBC chec ### Real DeepSeek API -Trusted pull requests run a second installed-wheel check on every native target with `DEEPSEEK_API_KEY_EXTERNAL`, mapped only into a preflight and the live test step. The preflight fails when the secret is empty, so the provider suite cannot self-skip to green. The test starts the public SDK against `https://api.deepseek.com`, asks the model to write an exact sentinel file through the platform shell, asks a second turn in the same session to read it, and verifies the external line content, final responses, completed turn reasons, model-requested tool calls, and the existence and Zstandard framing of its session log. Decoded record content and completed-turn durability are deterministic keyless obligations owned by the restart snapshot rather than inferred from compressed live-provider bytes. +Trusted pull requests and master pushes run a second installed-wheel check on each selected native target with `DEEPSEEK_API_KEY_EXTERNAL`, mapped only into a preflight and the live test step. The preflight fails when the secret is empty, so the provider suite cannot self-skip to green. The test starts the public SDK against `https://api.deepseek.com`, asks the model to write an exact sentinel file through the platform shell, asks a second turn in the same session to read it, and verifies the external line content, final responses, completed turn reasons, model-requested tool calls, and the existence and Zstandard framing of its session log. Decoded record content and completed-turn durability are deterministic keyless obligations owned by the restart snapshot rather than inferred from compressed live-provider bytes. Fork and Dependabot pull requests never receive the repository secret. Their native jobs run the complete keyless path and skip both secret-bearing steps; `pull_request_target` is forbidden because it would execute untrusted code with the key. ### Required targets -The pull-request `python-runtime` job calls the reusable builder for Linux x64, Linux arm64, macOS arm64, macOS x64, and Windows x64. 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 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](../process/2026-09-06-master-only-platform-ci.md). 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. ## Existing decisions and supersession -This decision supersedes the single-target topology in the archived [required Python runtime pull-request validation](../../archived/testing/2026-08-12-required-python-runtime-pull-request-ci.md) while retaining its requirement that the real executable, snapshots, wheels, and clean installation meet before merge. [docs/architecture.md](../../../../docs/architecture.md) owns the launched application and customization surface; the [single-file Python SDK runtime distribution](../architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) remains authoritative for SEA packaging, native sidecars, wheel tags, and release artifacts. +This decision supersedes the single-target topology in the archived [required Python runtime pull-request validation](../../archived/testing/2026-08-12-required-python-runtime-pull-request-ci.md) while retaining its requirement that the real executable, snapshots, wheels, and clean installation meet in each selected target check. [docs/architecture.md](../../../../docs/architecture.md) owns the launched application and customization surface; the [single-file Python SDK runtime distribution](../architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) remains authoritative for SEA packaging, native sidecars, wheel tags, and release artifacts. ## Alternatives considered @@ -48,4 +48,4 @@ This decision supersedes the single-target topology in the archived [required Py ## Consequences -Every pull request pays for five native executable and wheel builds plus deterministic installed-artifact scenarios. Trusted same-repository pull requests also pay for one two-turn DeepSeek task per target. In exchange, the required result describes the files Python users install, proves every published carrier before merge, and cannot pass by importing the checkout or silently skipping the real provider. +Every pull request pays for two native executable and wheel builds plus deterministic installed-artifact scenarios. Trusted same-repository pull requests also pay for one two-turn DeepSeek task per target. In exchange, the required result describes the files Python users install, proves the selected carriers before merge, and cannot pass by importing the checkout or silently skipping the real provider. diff --git a/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md b/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md index 06fe92bead..0597256081 100644 --- a/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md +++ b/.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md @@ -24,17 +24,17 @@ Linux 另外保留 manylinux 2.28 干净安装冒烟测试与 GLIBC 检查。mac ### 真实 DeepSeek API -可信拉取请求会在每个原生目标上运行第二项安装后 wheel 检查,并且只在预检与 live 测试步骤中把 `DEEPSEEK_API_KEY_EXTERNAL` 映射进去。密钥为空时预检失败,因此提供方测试不能通过自行 skip 产生假绿。该测试通过公开 SDK 访问 `https://api.deepseek.com`,要求模型通过当前平台 shell 写入内容精确的 sentinel 文件,再在同一 session 的第二个轮次中读取它,并校验外部文件行内容、最终响应、已完成的轮次结束原因、模型请求的工具调用,以及 session 日志存在且采用 Zstandard framing。解码后的记录内容与已完成轮次的持久性是由 restart 快照负责的确定性 keyless 要求,不从压缩后的 live 提供方字节推断。 +可信拉取请求与 master 推送会在各自选定的原生目标上运行第二项安装后 wheel 检查,并且只在预检与 live 测试步骤中把 `DEEPSEEK_API_KEY_EXTERNAL` 映射进去。密钥为空时预检失败,因此提供方测试不能通过自行 skip 产生假绿。该测试通过公开 SDK 访问 `https://api.deepseek.com`,要求模型通过当前平台 shell 写入内容精确的 sentinel 文件,再在同一 session 的第二个轮次中读取它,并校验外部文件行内容、最终响应、已完成的轮次结束原因、模型请求的工具调用,以及 session 日志存在且采用 Zstandard framing。解码后的记录内容与已完成轮次的持久性是由 restart 快照负责的确定性 keyless 要求,不从压缩后的 live 提供方字节推断。 Fork 与 Dependabot 拉取请求永远不会获得仓库密钥。它们的原生 job 运行完整 keyless 路径并跳过两个带密钥的步骤;禁止使用 `pull_request_target`,因为它会让不可信代码带着密钥执行。 ### 必需目标 -拉取请求的 `python-runtime` job 会针对 Linux x64、Linux arm64、macOS arm64、macOS x64 与 Windows x64 调用可复用构建器。其聚合结果仍是 `all checks passed` 的依赖项,因此任一原生载体失败、取消或缺失都会阻止必需判定通过。[sdk-runtime README](../../../../python/sdk-runtime/README.zh.md) 负责 Windows 目标及其 PowerShell 专属极简快照。 +拉取请求的 `python-runtime` job 针对 Linux x64 与 Windows x64 调用可复用构建器;master 推送根据[仅 master 平台策略](../process/2026-09-06-master-only-platform-ci.zh.md)选择 Linux arm64 与两种 macOS 架构。其聚合结果仍是 `all checks passed` 的依赖项,因此任一原生载体失败、取消或缺失都会阻止必需判定通过。[sdk-runtime README](../../../../python/sdk-runtime/README.zh.md) 负责 Windows 目标及其 PowerShell 专属极简快照。 ## Existing decisions and supersession -本决策取代已归档的[必需 Python 运行时拉取请求验证](../../archived/testing/2026-08-12-required-python-runtime-pull-request-ci.md)中的单目标拓扑,同时保留真实可执行文件、快照、wheel 包与干净安装必须在合并前相遇的要求。[docs/architecture.md](../../../../docs/architecture.zh.md) 负责启动应用与自定义接口;[单文件 Python SDK 运行时 distribution](../architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)继续负责 SEA 打包、原生 sidecar、wheel 包标签与发布产物。 +本决策取代已归档的[必需 Python 运行时拉取请求验证](../../archived/testing/2026-08-12-required-python-runtime-pull-request-ci.md)中的单目标拓扑,同时保留真实可执行文件、快照、wheel 包与干净安装必须在各选定目标的检查中相遇的要求。[docs/architecture.md](../../../../docs/architecture.zh.md) 负责启动应用与自定义接口;[单文件 Python SDK 运行时 distribution](../architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)继续负责 SEA 打包、原生 sidecar、wheel 包标签与发布产物。 ## Alternatives considered @@ -48,4 +48,4 @@ Fork 与 Dependabot 拉取请求永远不会获得仓库密钥。它们的原生 ## Consequences -每个拉取请求都会承担五个原生可执行文件及 wheel 包构建,并运行确定性的安装后产物场景。可信的同仓库拉取请求还会在每个目标上承担一次双轮 DeepSeek 任务。相应地,必需结果描述 Python 用户实际安装的文件,在合并前证明每个已发布载体,并且不能通过导入 checkout 或静默跳过真实提供方而通过。 +每个拉取请求都会承担两个原生可执行文件及 wheel 包构建,并运行确定性的安装后产物场景。可信的同仓库拉取请求还会在每个目标上承担一次双轮 DeepSeek 任务。相应地,必需结果描述 Python 用户实际安装的文件,在合并前证明选定载体,并且不能通过导入 checkout 或静默跳过真实提供方而通过。 diff --git a/.github/workflows/build-exe-for-python-sdk.yml b/.github/workflows/build-exe-for-python-sdk.yml index 908103483d..ae7dd61554 100644 --- a/.github/workflows/build-exe-for-python-sdk.yml +++ b/.github/workflows/build-exe-for-python-sdk.yml @@ -22,7 +22,7 @@ on: required: false default: false ci: - description: Run as the required all-target Python runtime pull-request check. + description: Run Python runtime CI validation for the selected targets. type: boolean required: false default: false @@ -46,7 +46,7 @@ concurrency: # github.workflow identifies the caller inside a reusable workflow and keeps # an ordinary CI run from cancelling a full release validation on the same ref. group: build-single-exe-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }} permissions: contents: read diff --git a/.github/workflows/ci-master.yml b/.github/workflows/ci-master.yml index 3721045072..2edb868b81 100644 --- a/.github/workflows/ci-master.yml +++ b/.github/workflows/ci-master.yml @@ -14,8 +14,8 @@ on: - larger-runner-benchmark - consolidated-runner-benchmark -# A master push may carry only the two self-hosted standby drills and the Wine -# apt cache seeder; those drills outlast the interval between master merges, so +# Master runs platform runtime checks, Wine, and two self-hosted standby drills. +# The drills outlast the interval between master merges, so # push is exempt from cancellation (see ci-failover-runbook). workflow_dispatch # keeps cancelling: a re-dispatched runner benchmark holds up to 12 larger # runners for 15 minutes in this same group. @@ -33,34 +33,95 @@ env: DSH_TELEMETRY_DISABLED: '1' jobs: - # Master seeds the Wine apt-archive cache in the default-branch scope, - # which every pull request's windows job can restore; saves from - # pull-request runs are scoped to their own merge ref and help nobody - # else. Runs in seconds when the image version already has a cache. - wine-apt-cache: + # These native runtime carriers are post-merge checks; release keeps all targets. + python-runtime: + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + name: python runtime / macOS and Linux ARM64 + uses: ./.github/workflows/build-exe-for-python-sdk.yml + with: + targets: node24-linux-arm64,node24-macos-arm64,node24-macos-x64 + ci: true + secrets: + DEEPSEEK_API_KEY_EXTERNAL: ${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }} + + # Wine runs once here, independently of the native serial standby aggregates. + windows: if: github.event_name == 'push' && github.ref == 'refs/heads/master' - name: wine apt cache runs-on: ubuntu-latest - timeout-minutes: 10 + name: windows node 24 / wine + timeout-minutes: 15 steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - uses: pnpm/action-setup@v4 + with: + dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }} + + - uses: actions/setup-node@v6 + with: + node-version: ${{ env.PRIMARY_NODE_VERSION }} + + - name: Configure pnpm store path + id: pnpm-store + run: | + store_root="$HOME/.local/share/pnpm/store" + echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV" + store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent) + echo "path=$store_path" >> "$GITHUB_OUTPUT" + + - uses: actions/cache/restore@v4 + with: + path: ${{ steps.pnpm-store.outputs.path }} + key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm- + + # Master runs restore and seed the image-specific Wine dependency cache. - name: Compose Wine apt cache key id: wine-cache-key run: echo "key=wine-debs-${ImageOS:-linux}-${ImageVersion:-v0}" >> "$GITHUB_OUTPUT" - uses: actions/cache@v4 - id: wine-cache with: path: ~/wine-debs key: ${{ steps.wine-cache-key.outputs.key }} - - name: Download the Wine dependency closure - if: steps.wine-cache.outputs.cache-hit != 'true' + # Runner provisioning only — a developer machine installs Wine through + # its own package manager; the gate script assumes a wine binary and + # fails loud without one. Wine from the apt cache when present; else + # download the full dependency closure once and keep it for the next + # run. The `wine` dispatcher package (not bare `wine64`) is what puts a + # binary on PATH. + - name: Install Wine run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends --download-only wine - mkdir -p "$HOME/wine-debs" - cp /var/cache/apt/archives/*.deb "$HOME/wine-debs/" - du -sh "$HOME/wine-debs" + if compgen -G "$HOME/wine-debs/*.deb" > /dev/null; then + # The restored archive is the full --download-only closure of + # `wine` for this runner image, so installing the .debs directly + # with dpkg needs no repository access. apt-get would instead + # re-download the same 100+ MB closure from the mirror, which has + # stalled the job past its budget on a degraded runner network. + # If the archive cannot satisfy the closure, fall back to the apt + # network install. + if ! sudo DEBIAN_FRONTEND=noninteractive dpkg -i "$HOME"/wine-debs/*.deb; then + sudo DEBIAN_FRONTEND=noninteractive dpkg --configure -a || true + sudo apt-get install -y --no-install-recommends "$HOME"/wine-debs/*.deb + fi + else + sudo apt-get update + sudo apt-get install -y --no-install-recommends --download-only wine + mkdir -p "$HOME/wine-debs" + cp /var/cache/apt/archives/*.deb "$HOME/wine-debs/" 2>/dev/null || true + sudo apt-get install -y --no-install-recommends wine + fi + + - name: Run the Wine Windows gates + run: bash scripts/wine-windows-gates.sh + + - name: Shut down wineserver + if: always() + run: wineserver -k 2>/dev/null || true # Hot-standby drill for the in-house self-hosted pool: every master move # re-runs the complete unsharded aggregate on the persistent 64-core VM, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c55aa4e58f..e673679bb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -364,108 +364,18 @@ jobs: 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. All native release targets are - # required because a platform wheel cannot be validated by another carrier. + # 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' name: python runtime / release-shaped matrix uses: ./.github/workflows/build-exe-for-python-sdk.yml with: - targets: node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64 + targets: node24-linux-x64,node24-win-x64 ci: true secrets: DEEPSEEK_API_KEY_EXTERNAL: ${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }} - # The required pull-request Windows signal: the two blocking win32 surfaces - # (workspace build, production site) execute with real, checksum-verified - # Windows Node under Wine on standard hosted Linux. The independent - # windows-native job below keeps the complete native-kernel inventory — - # including the observational portability gates this lane does not run — - # on real Windows. This job only provisions runner state (caches, - # apt); scripts/wine-windows-gates.sh owns the gate logic and is the same - # script the optional local gate `pnpm run check:windows-wine` runs. - # Current topology and fidelity limits live in - # .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md - windows: - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - name: windows node 24 / wine blocking - timeout-minutes: 15 - steps: - - uses: actions/checkout@v6 - with: - persist-credentials: false - - - uses: pnpm/action-setup@v4 - with: - dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }} - - - uses: actions/setup-node@v6 - with: - node-version: ${{ env.PRIMARY_NODE_VERSION }} - - - name: Configure pnpm store path - id: pnpm-store - run: | - store_root="$HOME/.local/share/pnpm/store" - echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV" - store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent) - echo "path=$store_path" >> "$GITHUB_OUTPUT" - - - uses: actions/cache/restore@v4 - with: - path: ${{ steps.pnpm-store.outputs.path }} - key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm- - - # Master's wine-apt-cache job in ci-master.yml seeds the default-branch - # scope every pull request can read; a save from this job only reaches - # reruns of the same merge ref. - - name: Compose Wine apt cache key - id: wine-cache-key - run: echo "key=wine-debs-${ImageOS:-linux}-${ImageVersion:-v0}" >> "$GITHUB_OUTPUT" - - - uses: actions/cache@v4 - with: - path: ~/wine-debs - key: ${{ steps.wine-cache-key.outputs.key }} - - # Runner provisioning only — a developer machine installs Wine through - # its own package manager; the gate script assumes a wine binary and - # fails loud without one. Wine from the apt cache when present; else - # download the full dependency closure once and keep it for the next - # run. The `wine` dispatcher package (not bare `wine64`) is what puts a - # binary on PATH. - - name: Install Wine - run: | - if compgen -G "$HOME/wine-debs/*.deb" > /dev/null; then - # The restored archive is the full --download-only closure of - # `wine` for this runner image, so installing the .debs directly - # with dpkg needs no repository access. apt-get would instead - # re-download the same 100+ MB closure from the mirror, which has - # stalled the job past its budget on a degraded runner network. - # If the archive cannot satisfy the closure, fall back to the apt - # network install. - if ! sudo DEBIAN_FRONTEND=noninteractive dpkg -i "$HOME"/wine-debs/*.deb; then - sudo DEBIAN_FRONTEND=noninteractive dpkg --configure -a || true - sudo apt-get install -y --no-install-recommends "$HOME"/wine-debs/*.deb - fi - else - sudo apt-get update - sudo apt-get install -y --no-install-recommends --download-only wine - mkdir -p "$HOME/wine-debs" - cp /var/cache/apt/archives/*.deb "$HOME/wine-debs/" 2>/dev/null || true - sudo apt-get install -y --no-install-recommends wine - fi - - - name: Run the Wine Windows gates - run: bash scripts/wine-windows-gates.sh - - - name: Shut down wineserver - if: always() - run: wineserver -k 2>/dev/null || true - # Every pull request also gets real Windows-kernel signals. The former # monolithic windows-native job is split into smaller jobs so one slow # coverage gate does not hold up build/static results, while the total @@ -708,7 +618,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, windows-build, windows-native-tests] + needs: [node-24, node-24-coverage, node-24-bench, node-24-consumers, node-compat, python-sdk, python-runtime, windows-build, windows-native-tests] if: always() && github.event_name == 'pull_request' steps: - name: Fail if any needed job did not succeed diff --git a/docs/testing.i18n.yaml b/docs/testing.i18n.yaml index 9d61eac1d4..867bccc380 100644 --- a/docs/testing.i18n.yaml +++ b/docs/testing.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/testing.md -testing.md: b227aea937c63e641580486e6481234168231258 -testing.zh.md: f3938fc7773ed2dcd1a37e257310a060d8dafa6e +testing.md: 6caee19d4eabd0c285cd9fb985fd212526782f06 +testing.zh.md: b4dd9a21e8c0fdc336936e6d4297f34d6e82f135 diff --git a/docs/testing.md b/docs/testing.md index b227aea937..6caee19d4e 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -16,6 +16,8 @@ How this repo tests, tier by tier, and the rules that keep a green suite meaning Session fixtures retain headers and payloads but omit body sequence/time envelopes; replay synthesizes them. Replay, record, and refresh select each parent/child role's highest generation. Current v2 uses `.v2`, one row per event, and embedded compact Assistant streams; retained v0 (suffixless) and v1 (`.v1`) may keep canonical packed rows for migration coverage. [The migrator](../scripts/migrate-packed-session-fixtures.ts) rewrites older historical layouts. +[Python runtime and Wine scheduling](../.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.md). + ## How specs execute Forked workers run several spec files at once, the coverage gate splits into concurrent partitions beside the other gates in its job, and the self-hosted runners share one host and one volume. Only the process is isolated: ports, predictable paths, external namespaces, and inherited children are not. Own each acquired resource through its teardown, and read a spec that passes only when it runs alone as a defect in the spec rather than an unstable runner. [dsh-ci-test-reliability](../.agents/skills/dsh-ci-test-reliability/SKILL.md) owns the allocation, restoration, synchronization, timeout-budget, platform, and teardown rules; its [flake diagnosis workflow](../.agents/skills/dsh-ci-test-reliability/references/ci-flake-diagnosis.md) classifies an existing probabilistic failure. diff --git a/docs/testing.zh.md b/docs/testing.zh.md index f3938fc777..b4dd9a21e8 100644 --- a/docs/testing.zh.md +++ b/docs/testing.zh.md @@ -16,6 +16,8 @@ Session fixture 保留 header 与 payload,但省略正文 seq/time envelope;replay 会合成这些 envelope。Replay、record 与 refresh 会选择每个 parent/child 角色的最高 generation。当前 v2 使用 `.v2`、每个事件一行,并嵌入紧凑 Assistant stream;保留的 v0(无后缀)与 v1(`.v1`)可以为迁移覆盖保留规范 packed row。[迁移器](../scripts/migrate-packed-session-fixtures.ts)会改写更旧的历史布局。 +[Python 运行时与 Wine 调度](../.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.zh.md)。 + ## spec 如何被执行 fork 出的 worker 会同时运行多个 spec 文件,coverage gate 会拆成并发的 partition,与同一个 job 中的其它 gate 并排运行,而自托管 runner 共用同一台宿主机和同一个卷。被隔离的只有进程:端口、可预测路径、外部命名空间和继承而来的子进程都不隔离。为每个占用的资源负责到它的 teardown,并把「只有单独运行时才通过」的 spec 读作该 spec 的缺陷,而不是 runner 不稳定。[dsh-ci-test-reliability](../.agents/skills/dsh-ci-test-reliability/SKILL.md) 负责资源分配、状态恢复、同步、超时预算、平台差异与 teardown 规则;它的 [flake 诊断流程](../.agents/skills/dsh-ci-test-reliability/references/ci-flake-diagnosis.md)用于归类已经存在的概率性失败。 diff --git a/python/development.i18n.yaml b/python/development.i18n.yaml index f1ada26c49..9828188337 100644 --- a/python/development.i18n.yaml +++ b/python/development.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write python/development.md -development.md: aa0144d7eaa66711d0f08316d4445da060918ca8 -development.zh.md: a35f6fc8de1bdbd282fd8999a1440fde0c400b34 +development.md: 39c47b7d2a86995eea017c77fb147c63f90412b1 +development.zh.md: fe7b4dbb2ca2e341dfb9ceb57959b7d9807f54d2 diff --git a/python/development.md b/python/development.md index aa0144d7ea..39c47b7d2a 100644 --- a/python/development.md +++ b/python/development.md @@ -27,7 +27,7 @@ uv run --project python/sdk pytest `python/sdk/tests/test_bundled_runtime.py` exercises available bundled carriers and skips a carrier when its artifact has not been built. For repository-wide test policy, see [Testing](../docs/testing.md). -That suite drives fake runtime peers. `scripts/smoke-python-runtime.py` drives the packaged runtime instead. The required `python-runtime` CI job builds every published native target, installs the matching SDK and runtime wheels into a new Python 3.10 virtual environment, runs outside the checkout with `PYTHONPATH` and `DSH_RUNTIME_MODE` unset, proves that both modules and the executable came from those distributions, and then runs every keyless scenario. A focused local source-SDK run can select one built executable and scenario: +That suite drives fake runtime peers. `scripts/smoke-python-runtime.py` drives the packaged runtime instead. The `python-runtime` CI jobs build Linux x64 and Windows x64 on pull requests, and Linux arm64 plus both macOS architectures on master pushes. Each selected target installs the matching SDK and runtime wheels into a new Python 3.10 virtual environment, runs outside the checkout with `PYTHONPATH` and `DSH_RUNTIME_MODE` unset, proves that both modules and the executable came from those distributions, and then runs every keyless scenario. A focused local source-SDK run can select one built executable and scenario: ```sh uv run --project python/sdk python scripts/smoke-python-runtime.py \ @@ -36,7 +36,7 @@ uv run --project python/sdk python scripts/smoke-python-runtime.py \ Three scenarios compare committed expected output under `scripts/snapshots/python-sdk-single-exe/`. `minimal/model-visible.json` pins the Linux/macOS `sdk-minimal` profile's assembled system prompts, advertised tool schemas, and model-visible messages; `minimal/win-x64/model-visible.json` pins its PowerShell counterpart. A plugin that contributes an unintended system section or user message therefore fails the job, and every message the profile emits is compared. `advanced/` pins one complex process's SDK result and parent/child session logs across every target. `restart/` launches two complete SDK runtime processes against one persistence root and snapshots their isolated model histories, high-level results, and separate durable logs across every target. Rerun the owning scenario with `--update-snapshots` and review that diff before committing it. -Trusted pull requests also run `--scenario sdk-live --installed-wheel` on every native target. That scenario performs two tool-using turns against `https://api.deepseek.com`, verifies the created file externally, and fails when the repository secret is absent instead of self-skipping. Fork and Dependabot pull requests run the complete keyless installed-wheel path but receive no key. +Trusted pull requests and master pushes also run `--scenario sdk-live --installed-wheel` on each selected native target. That scenario performs two tool-using turns against `https://api.deepseek.com`, verifies the created file externally, and fails when the repository secret is absent instead of self-skipping. Fork and Dependabot pull requests run the complete keyless installed-wheel path but receive no key. An interactive smoke test needs `DEEPSEEK_API_KEY` in the environment or repository-root `.env`: diff --git a/python/development.zh.md b/python/development.zh.md index a35f6fc8de..fe7b4dbb2c 100644 --- a/python/development.zh.md +++ b/python/development.zh.md @@ -27,7 +27,7 @@ uv run --project python/sdk pytest `python/sdk/tests/test_bundled_runtime.py` 会运行可用的内置载体;某个载体的产物尚未构建时,会跳过该载体。仓库级测试政策见 [测试](../docs/testing.zh.md)。 -该套件面向的是伪造的运行时对端。`scripts/smoke-python-runtime.py` 面向打包运行时。必需的 `python-runtime` CI 任务会构建每个已发布原生目标,把匹配的 SDK wheel 包与运行时 wheel 包安装进新的 Python 3.10 虚拟环境,在 checkout 外清除 `PYTHONPATH` 与 `DSH_RUNTIME_MODE` 后运行,证明两个模块及可执行文件都来自这些 distribution,然后运行全部 keyless 场景。聚焦的本地源码 SDK 运行可以选择一个已构建可执行文件与场景: +该套件面向的是伪造的运行时对端。`scripts/smoke-python-runtime.py` 面向打包运行时。`python-runtime` CI 任务在拉取请求上构建 Linux x64 与 Windows x64,在 master 推送上构建 Linux arm64 与两种 macOS 架构。每个选定目标把匹配的 SDK wheel 包与运行时 wheel 包安装进新的 Python 3.10 虚拟环境,在 checkout 外清除 `PYTHONPATH` 与 `DSH_RUNTIME_MODE` 后运行,证明两个模块及可执行文件都来自这些 distribution,然后运行全部 keyless 场景。聚焦的本地源码 SDK 运行可以选择一个已构建可执行文件与场景: ```sh uv run --project python/sdk python scripts/smoke-python-runtime.py \ @@ -36,7 +36,7 @@ uv run --project python/sdk python scripts/smoke-python-runtime.py \ 其中三个场景会比对 `scripts/snapshots/python-sdk-single-exe/` 下已提交的期望输出。`minimal/model-visible.json` 固定 Linux/macOS `sdk-minimal` profile 所组装的系统提示词、对外公布的工具 schema 与模型可见消息;`minimal/win-x64/model-visible.json` 固定对应的 PowerShell 版本。因此,插件一旦贡献出计划外的系统分段或 user 消息,该任务即失败,且该 profile 发出的每条消息都会参与比对。`advanced/` 跨所有目标固定一个复杂进程的 SDK 结果及父/子会话日志。`restart/` 针对同一持久化根目录启动两个完整 SDK 运行时进程,并跨所有目标固定其彼此隔离的模型历史、高层结果与独立持久日志。重新运行对应场景时加上 `--update-snapshots`,并在提交前审阅该差异。 -可信拉取请求还会在每个原生目标上运行 `--scenario sdk-live --installed-wheel`。该场景面向 `https://api.deepseek.com` 执行两个使用工具的轮次,从外部验证已创建文件,并在仓库密钥缺失时失败而不是自行 skip。Fork 与 Dependabot 拉取请求会运行完整的 keyless 安装后 wheel 路径,但不会获得密钥。 +可信拉取请求与 master 推送还会在各自选定的原生目标上运行 `--scenario sdk-live --installed-wheel`。该场景面向 `https://api.deepseek.com` 执行两个使用工具的轮次,从外部验证已创建文件,并在仓库密钥缺失时失败而不是自行 skip。Fork 与 Dependabot 拉取请求会运行完整的 keyless 安装后 wheel 路径,但不会获得密钥。 交互式冒烟测试需要环境变量或仓库根目录 `.env` 中存在 `DEEPSEEK_API_KEY`: diff --git a/scripts/ci-workflow.spec.ts b/scripts/ci-workflow.spec.ts index eb06172527..3c9bd896e6 100644 --- a/scripts/ci-workflow.spec.ts +++ b/scripts/ci-workflow.spec.ts @@ -55,11 +55,10 @@ describe('CI workflow', () => { } }) - it('keeps required Wine and split native Windows jobs with failover, plus a master-only standby', () => { + it('keeps split native Windows PR jobs with failover, plus a master-only standby', () => { const workflow = loadWorkflow('.github/workflows/ci.yml') const masterWorkflow = loadWorkflow('.github/workflows/ci-master.yml') if (!isRecord(workflow.jobs) - || !isRecord(workflow.jobs.windows) || !isRecord(workflow.jobs['windows-build']) || !isRecord(workflow.jobs['windows-coverage']) || !isRecord(workflow.jobs['windows-native-tests']) @@ -71,17 +70,14 @@ describe('CI workflow', () => { || !isRecord(workflow.jobs['node-compat']) || !isRecord(workflow.jobs['all-checks-passed']) || !isRecord(masterWorkflow.jobs) - || !isRecord(masterWorkflow.jobs['wine-apt-cache']) || !isRecord(masterWorkflow.jobs['serial-windows'])) { - throw new TypeError('CI workflow must define windows, windows-build, windows-coverage, windows-native-tests, windows-observational, node-24, node-24-coverage, node-24-bench, node-24-consumers, node-compat, and all-checks-passed; ci-master must define wine-apt-cache and serial-windows') + throw new TypeError('CI workflow must define windows-build, windows-coverage, windows-native-tests, windows-observational, node-24, node-24-coverage, node-24-bench, node-24-consumers, node-compat, and all-checks-passed; ci-master must define serial-windows') } - const windows = workflow.jobs.windows const windowsBuild = workflow.jobs['windows-build'] const windowsCoverage = workflow.jobs['windows-coverage'] const windowsNativeTests = workflow.jobs['windows-native-tests'] const windowsObservational = workflow.jobs['windows-observational'] - const wineAptCache = masterWorkflow.jobs['wine-apt-cache'] const serialWindows = masterWorkflow.jobs['serial-windows'] const node24 = workflow.jobs['node-24'] const node24Coverage = workflow.jobs['node-24-coverage'] @@ -89,19 +85,9 @@ 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(windows.steps) || !Array.isArray(aggregate.needs)) { - throw new TypeError('Windows job must define steps and the aggregate must define needs') + if (!Array.isArray(aggregate.needs)) { + throw new TypeError('CI aggregate must define needs') } - const commandSteps = windows.steps.filter((step): step is Record & { run: string } => ( - isRecord(step) && typeof step.run === 'string' - )) - - // Required PR job: Wine on ubuntu-latest, runs wine-windows-gates.sh. - expect(windows['runs-on']).toBe('ubuntu-latest') - expect(windows.name).toBe('windows node 24 / wine blocking') - expect(windows.if).toBe("github.event_name == 'pull_request'") - expect(commandSteps.some(step => step.run.includes('wine-windows-gates.sh'))).toBe(true) - // The split native jobs all resolve their pool through the Windows switch. for (const [jobName, job] of [['windows-build', windowsBuild], ['windows-coverage', windowsCoverage], ['windows-native-tests', windowsNativeTests], ['windows-observational', windowsObservational]] as const) { expect(typeof job['runs-on']).toBe('string') @@ -183,10 +169,6 @@ describe('CI workflow', () => { expect(windowsObservational.name).toBe('windows node 24 / observational') expect(windowsObservational['continue-on-error']).toBe(true) - // wine-apt-cache: master-only, seeds the Wine apt cache, lives in ci-master. - expect(wineAptCache.if).toBe("github.event_name == 'push' && github.ref == 'refs/heads/master'") - expect(wineAptCache['runs-on']).toBe('ubuntu-latest') - // serial-windows: master-only standby, self-hosted, non-blocking, lives in ci-master. expect(serialWindows.if).toBe("github.event_name == 'push' && github.ref == 'refs/heads/master'") expect(serialWindows['runs-on']).toEqual(['self-hosted', 'dsh-win-ci', 'windows']) @@ -223,7 +205,7 @@ describe('CI workflow', () => { // Aggregate: Wine and the required split native jobs are needed; // windows-coverage is temporarily non-blocking while Windows ACP // half-close tests are stabilized; observational stays out too. - expect(aggregate.needs).toContain('windows') + expect(aggregate.needs).not.toContain('windows') expect(aggregate.needs).toContain('windows-build') // The benchmark lane is a required verdict input and runs alone so its // wall-clock budgets never share a runner with a concurrent aggregate. @@ -362,9 +344,7 @@ describe('CI workflow', () => { expect(job.if).toBe("github.event_name == 'push' && github.ref == 'refs/heads/master'") } - // What bounds the cost of exempting push: a master push may only carry the - // cache seeder and the two drills. Any job reachable on push would start - // accumulating uncancelled runs, so the set is pinned here. + // Pin the post-merge runtime, Wine, and standby inventory. const NOT_PUSH_REACHABLE = new Set([ "github.event_name == 'workflow_dispatch' && inputs.suite == 'larger-runner-benchmark'", "github.event_name == 'workflow_dispatch' && inputs.suite == 'consolidated-runner-benchmark'", @@ -379,7 +359,7 @@ describe('CI workflow', () => { }) .map(([name]) => name) .sort() - expect(pushReachable).toEqual(['serial-linux-selfhosted', 'serial-windows', 'wine-apt-cache']) + expect(pushReachable).toEqual(['python-runtime', 'serial-linux-selfhosted', 'serial-windows', 'windows']) // Why workflow_dispatch must keep cancelling: each benchmark fans out to a // dozen larger runners at once, in this same group on master. If it stopped @@ -403,7 +383,7 @@ describe('CI workflow', () => { expect(config).not.toContain('packages/lsp/lsp-stdio/src/instance.ts') }) - it('requires release-shaped Python runtime validation on every published target', () => { + it('requires release-shaped Python runtime validation on Linux and Windows x64', () => { const workflow = loadWorkflow('.github/workflows/ci.yml') const pythonRuntime = workflowJob(workflow, 'python-runtime') const aggregate = workflowJob(workflow, 'all-checks-passed') @@ -416,7 +396,7 @@ describe('CI workflow', () => { name: 'python runtime / release-shaped matrix', uses: './.github/workflows/build-exe-for-python-sdk.yml', with: { - targets: 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64', + targets: 'node24-linux-x64,node24-win-x64', ci: true, }, secrets: { diff --git a/scripts/tests/ci-master-platforms.spec.ts b/scripts/tests/ci-master-platforms.spec.ts new file mode 100644 index 0000000000..b75d65c87f --- /dev/null +++ b/scripts/tests/ci-master-platforms.spec.ts @@ -0,0 +1,112 @@ +/** Scheduling policy for post-merge native runtime carriers and Wine. */ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { load } from 'js-yaml' +import { describe, expect, it } from 'vitest' +import { gatesForMode } from '../run-gates.ts' + +const root = resolve(import.meta.dirname, '../..') +const masterPush = "github.event_name == 'push' && github.ref == 'refs/heads/master'" +const runtimeBuilder = './.github/workflows/build-exe-for-python-sdk.yml' + +interface Job { + if?: string | boolean + uses?: string + needs?: string[] + with?: Record + secrets?: Record + steps?: Array<{ name?: string; run?: string; if?: string; uses?: string; with?: Record }> + 'runs-on'?: string | string[] + 'continue-on-error'?: boolean +} + +interface Workflow { + on: Record + jobs: Record + concurrency?: Record +} + +function workflow(name: string): Workflow { + return load(readFileSync(resolve(root, '.github/workflows', name), 'utf8')) as Workflow +} + +function commands(job: Job): string[] { + return (job.steps ?? []).flatMap(step => step.run ? [step.run] : []) +} + +describe('master-only platform scheduling', () => { + it('keeps only Linux and Windows x64 runtimes in required PR CI', () => { + const pr = workflow('ci.yml') + expect(Object.keys(pr.on)).toEqual(['pull_request']) + expect(pr.jobs['python-runtime']).toMatchObject({ + if: "github.event_name == 'pull_request'", + uses: runtimeBuilder, + with: { ci: true, targets: 'node24-linux-x64,node24-win-x64' }, + }) + 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']! + expect(aggregate.needs).toContain('python-runtime') + expect(aggregate.needs).not.toContain('windows') + expect(aggregate.needs!.every(id => id in pr.jobs)).toBe(true) + expect(aggregate.if).toBe("always() && github.event_name == 'pull_request'") + expect(aggregate.steps).toContainEqual(expect.objectContaining({ + if: "contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped')", + })) + }) + + it('runs all three deferred carriers on master pushes with fail-loud API credentials', () => { + const master = workflow('ci-master.yml') + expect(master.on.push).toEqual({ branches: ['master'] }) + expect(Object.keys(master.on).sort()).toEqual(['push', 'workflow_dispatch']) + const runtime = master.jobs['python-runtime']! + expect(runtime).toMatchObject({ + if: masterPush, + uses: runtimeBuilder, + with: { ci: true, targets: 'node24-linux-arm64,node24-macos-arm64,node24-macos-x64' }, + secrets: { DEEPSEEK_API_KEY_EXTERNAL: '${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}' }, + }) + expect(runtime.needs).toBeUndefined() + expect(runtime['continue-on-error']).toBeUndefined() + const builder = workflow('build-exe-for-python-sdk.yml') + expect(builder.concurrency?.['cancel-in-progress']).toBe( + "${{ github.event_name != 'push' || github.ref != 'refs/heads/master' }}", + ) + const build = builder.jobs.build! + const preflight = build.steps!.find(step => step.name === 'Preflight installed-wheel real API test (POSIX)')! + expect(preflight.if).toContain('inputs.ci') + expect(preflight.if).toContain("github.event_name != 'pull_request'") + expect(preflight.if).toContain('github.event.pull_request.head.repo.fork') + expect(preflight.if).toContain("github.event.pull_request.user.login == 'dependabot[bot]'") + expect(preflight.run).toContain('exit 1') + }) + + it('runs Wine once on hosted master CI and seeds its own apt cache', () => { + const master = workflow('ci-master.yml') + const wine = master.jobs.windows! + expect(wine).toMatchObject({ if: masterPush, 'runs-on': 'ubuntu-latest' }) + expect(wine.needs).toBeUndefined() + expect(wine['continue-on-error']).toBeUndefined() + expect(master.jobs['wine-apt-cache']).toBeUndefined() + expect(Object.values(master.jobs).flatMap(commands).filter(command => command.includes('wine-windows-gates.sh'))) + .toEqual(['bash scripts/wine-windows-gates.sh']) + expect(wine.steps).toContainEqual(expect.objectContaining({ + uses: 'actions/cache@v4', with: { path: '~/wine-debs', key: '${{ steps.wine-cache-key.outputs.key }}' }, + })) + expect(commands(wine).join('\n')).toContain('--download-only wine') + expect(wine.steps).toContainEqual(expect.objectContaining({ name: 'Shut down wineserver', if: 'always()' })) + for (const mode of ['ci-linux-primary', 'ci-windows-complete'] as const) { + expect(gatesForMode(mode).map(gate => gate.displayCommand).join('\n')).not.toMatch(/wine/i) + } + }) + + it('retains the complete release matrix independently of CI scheduling', () => { + const release = workflow('python-release.yml') + const calls = Object.values(release.jobs).filter(job => job.uses === runtimeBuilder) + expect(calls).toHaveLength(1) + expect(calls[0]!.with).toMatchObject({ + release: true, + targets: 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64', + }) + }) +}) diff --git a/scripts/wine-windows-gates.sh b/scripts/wine-windows-gates.sh index f9f04faea0..964ca38023 100755 --- a/scripts/wine-windows-gates.sh +++ b/scripts/wine-windows-gates.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Run the blocking Windows gates (workspace build, production site) with real -# win-x64 Node.js under Wine — the same script the pull-request `windows` job -# in ci.yml executes and the optional local gate `pnpm run check:windows-wine` +# win-x64 Node.js under Wine — the same script the master-only `windows` job +# in ci-master.yml executes and the optional local gate `pnpm run check:windows-wine` # wraps. Owning rationale and fidelity limits: # .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md # From c379894af8c69a316c11f0ed408f05628844072a Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 6 Sep 2026 14:57:22 +0800 Subject: [PATCH 2/6] fix(ci): isolate routing test setup and correct scheduling docs --- ...le-executable-sdk-runtime-distribution.i18n.yaml | 4 ++-- ...ngle-file-executable-sdk-runtime-distribution.md | 2 +- ...e-file-executable-sdk-runtime-distribution.zh.md | 2 +- ...6-08-08-native-windows-pull-request-ci.i18n.yaml | 4 ++-- .../2026-08-08-native-windows-pull-request-ci.md | 4 ++-- .../2026-08-08-native-windows-pull-request-ci.zh.md | 4 ++-- .github/AGENTS.md | 2 +- .github/workflows/ci-master.yml | 2 +- .github/workflows/ci.yml | 7 +++---- docs/testing.i18n.yaml | 4 ++-- docs/testing.md | 4 +--- docs/testing.zh.md | 4 +--- scripts/ci-workflow.spec.ts | 1 - scripts/tests/ci-master-platforms.spec.ts | 13 +++++++++++-- 14 files changed, 30 insertions(+), 27 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml index c2c2fb38b7..712a1af3cb 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md -2026-07-10-single-file-executable-sdk-runtime-distribution.md: ac365ade126cb14d84dc4fe5dcba45e890511621 -2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: df40831d45551d313d82571cfffe95c5f6a19164 +2026-07-10-single-file-executable-sdk-runtime-distribution.md: 756cf419df2dff70973eee9c1598950158dbfc1d +2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: aa9c1c1b93b19a8310fff65bdfafa54f237c0e3c diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md index ac365ade12..756cf419df 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md @@ -46,7 +46,7 @@ The deploy root includes `@deepseek-ai/dsh-mcp-client` as an explicitly supporte [`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → restore direct workspace packages omitted by legacy deploy and reject any remaining manifest gap → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → inject pkg configuration whose bin is `node_modules/@deepseek-ai/dsh/lib/bin.js` and whose assets cover dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea` once per target → write `deepseek-harness-sdk-runtime--` under `dist-exe/` and copy it into the runtime directory. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container because legacy deploy omits that install side effect. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted gives pkg a stable single-instance layout that the explicit materialization pass makes symlink-free; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry. -CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml) is called for all five targets by the [installed-wheel Python runtime pull-request validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md) and the [public publication workflow](../../archived/process/2026-08-11-python-publication-workflow.md); `workflow_dispatch` can still select a subset. Native builds run on linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64 / macos-x64 (`macos-15-intel`) / win-x64 (`windows-2025`), with `~/.pkg-cache` cached where applicable, and pkg handles macOS ad-hoc signing. Each leg installs the release-shaped SDK and runtime wheels into a clean venv outside the checkout, proves their package and executable provenance, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests additionally run a real DeepSeek two-turn tool smoke on every target; fork and Dependabot heads receive no key. Linux inspects the executable and native addon's GLIBC requirements and runs an additional manylinux 2.28 smoke, while macOS checks the runtime, ripgrep, and PTY helper architectures and verifies that all three deployment targets fit the wheel tag. A full five-target run retains six artifacts, each containing one release file: the platform-independent SDK wheel and five native runtime wheels; a subset dispatch retains the SDK wheel and selected runtime wheels. Bare executables and source bundles remain intermediate test inputs. [`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) accepts `python-v` tag pipelines whose version matches the root `package.json`, builds one SDK wheel and five native runtime wheels, then a single serialized job checks and publishes all six to the project PyPI registry. The [`python/sdk-runtime` README](../../../../python/sdk-runtime/README.md) owns the Windows target and the explicit exclusion of Windows arm64. +CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml) runs [installed-wheel validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md) on Linux/Windows x64 for pull requests and Linux ARM64 plus both macOS architectures for master pushes. The [public publication workflow](../../archived/process/2026-08-11-python-publication-workflow.md) calls it for all five targets; `workflow_dispatch` can still select a subset. Native builds run on linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64 / macos-x64 (`macos-15-intel`) / win-x64 (`windows-2025`), with `~/.pkg-cache` cached where applicable, and pkg handles macOS ad-hoc signing. Each leg installs the release-shaped SDK and runtime wheels into a clean venv outside the checkout, proves their package and executable provenance, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests and master pushes additionally run a real DeepSeek two-turn tool smoke on their selected targets; fork and Dependabot heads receive no key. Linux inspects the executable and native addon's GLIBC requirements and runs an additional manylinux 2.28 smoke, while macOS checks the runtime, ripgrep, and PTY helper architectures and verifies that all three deployment targets fit the wheel tag. A full five-target run retains six artifacts, each containing one release file: the platform-independent SDK wheel and five native runtime wheels; a subset dispatch retains the SDK wheel and selected runtime wheels. Bare executables and source bundles remain intermediate test inputs. [`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) accepts `python-v` tag pipelines whose version matches the root `package.json`, builds one SDK wheel and five native runtime wheels, then a single serialized job checks and publishes all six to the project PyPI registry. The [`python/sdk-runtime` README](../../../../python/sdk-runtime/README.md) owns the Windows target and the explicit exclusion of Windows arm64. ### Python SDK distribution: two carriers, exe for production, node for development diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md index df40831d45..aa9c1c1b93 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md @@ -46,7 +46,7 @@ exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真 [`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 注入 pkg 配置,其中 bin 为 `node_modules/@deepseek-ai/dsh/lib/bin.js`,assets 覆盖动态读取的 profile、bundle、前端、preset、原生库与配置文件 → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 将 `deepseek-harness-sdk-runtime--` 写入 `dist-exe/` 并拷回运行时目录。Linux CI 会在匹配的 manylinux 2.28 容器中重新构建 `pty.node`,因为 legacy deploy 会遗漏这一安装副作用。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 为 pkg 提供稳定的单实例布局,再由显式物化步骤消除符号链接;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。 -CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml):[安装后 wheel Python 运行时拉取请求验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)与[公开发布工作流](../../archived/process/2026-08-11-python-publication-workflow.md)都会调用它构建全部五个目标;`workflow_dispatch` 仍可选择部分目标。linux-x64、linux-arm64(`ubuntu-24.04-arm`)、macos-arm64、macos-x64(`macos-15-intel`)与 win-x64(`windows-2025`)分别进行原生构建,并在适用平台缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都把发布形态的 SDK wheel 包与运行时 wheel 包安装到 checkout 外的干净 venv,证明包与可执行文件来源,再通过公开 SDK 与直接 NDJSON JSON-RPC 运行完整 keyless 场景。可信拉取请求还会在每个目标上运行真实 DeepSeek 双轮工具冒烟测试;fork 与 Dependabot head 不会获得密钥。Linux 会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并额外运行 manylinux 2.28 冒烟测试;macOS 则检查 runtime、ripgrep 与 PTY helper 的架构,并验证三个载荷的部署目标都符合 wheel 包标签。完整构建五个目标时保留 6 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 5 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-v` 标签流水线,构建一个 SDK wheel 包和 5 个原生运行时 wheel 包,再由单个串行任务校验并将这 6 个文件发布到项目的 PyPI 注册表。[`python/sdk-runtime` README](../../../../python/sdk-runtime/README.zh.md)负责 Windows 目标及对 Windows arm64 的明确排除。 +CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml):[安装后 wheel 包验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)在拉取请求上运行 Linux/Windows x64,在 master 推送上运行 Linux ARM64 与两种 macOS 架构。[公开发布工作流](../../archived/process/2026-08-11-python-publication-workflow.md)调用它构建全部五个目标;`workflow_dispatch` 仍可选择部分目标。linux-x64、linux-arm64(`ubuntu-24.04-arm`)、macos-arm64、macos-x64(`macos-15-intel`)与 win-x64(`windows-2025`)分别进行原生构建,并在适用平台缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都把发布形态的 SDK wheel 包与运行时 wheel 包安装到 checkout 外的干净 venv,证明包与可执行文件来源,再通过公开 SDK 与直接 NDJSON JSON-RPC 运行完整 keyless 场景。可信拉取请求与 master 推送还会在各自选定的目标上运行真实 DeepSeek 双轮工具冒烟测试;fork 与 Dependabot head 不会获得密钥。Linux 会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并额外运行 manylinux 2.28 冒烟测试;macOS 则检查 runtime、ripgrep 与 PTY helper 的架构,并验证三个载荷的部署目标都符合 wheel 包标签。完整构建五个目标时保留 6 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 5 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-v` 标签流水线,构建一个 SDK wheel 包和 5 个原生运行时 wheel 包,再由单个串行任务校验并将这 6 个文件发布到项目的 PyPI 注册表。[`python/sdk-runtime` README](../../../../python/sdk-runtime/README.zh.md)负责 Windows 目标及对 Windows arm64 的明确排除。 ### Python SDK 分发:双载体,exe 用于生产,`node` 用于开发 diff --git a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml index d3e10ab382..ff4e50c508 100644 --- a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml +++ b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md -2026-08-08-native-windows-pull-request-ci.md: 511d3d146282c9d5635ab72e9ad86a87a899b22a -2026-08-08-native-windows-pull-request-ci.zh.md: 75bd3e14907f9ccd81e9ae229a08f8e13e7cb7ca +2026-08-08-native-windows-pull-request-ci.md: ba63af5a8f1d32035e116b3900eb9d5905f326d0 +2026-08-08-native-windows-pull-request-ci.zh.md: 3a0c8f510f2f8881833633f69d8ac5d7330d5195 diff --git a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md index 511d3d1462..ba63af5a8f 100644 --- a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md +++ b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md @@ -1,4 +1,4 @@ -# Agent Note: Dual Wine and native Windows pull-request CI +# Agent Note: Wine and native Windows CI Status: implemented @@ -6,7 +6,7 @@ English | [中文](2026-08-08-native-windows-pull-request-ci.zh.md) ## Problem -The required pull-request Windows verdict needs a fast win32 toolchain signal without making the aggregate wait for scarce Windows capacity. Wine provides that critical-path signal but runs over a Linux kernel and case-sensitive ext4, uses a hoisted dependency layout, and cannot prove NTFS, DACL, ConPTY, crash durability, or native process behavior. With the native serial references disabled, every pull-request head also needs an automatic real Windows-kernel result. +Wine checks the win32 toolchain over a Linux kernel and case-sensitive ext4 with a hoisted dependency layout. It cannot prove NTFS, DACL, ConPTY, crash durability, or native process behavior. Pull-request correctness therefore needs native Windows build and process checks independently of the post-merge Wine result. A coverage audit found that stale branch state had restored temporary exclusions for supported LSP sources. Native Windows therefore needed to execute the complete supported source inventory at the same 100%-per-file threshold instead of relying on a smaller platform-specific denominator. diff --git a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md index 75bd3e1490..3a0c8f510f 100644 --- a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md +++ b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md @@ -1,4 +1,4 @@ -# Agent Note: Wine 与原生 Windows 双通道拉取请求 CI +# Agent Note: Wine 与原生 Windows CI Status: implemented @@ -6,7 +6,7 @@ Status: implemented ## 问题 -拉取请求必需的 Windows 判定既需要快速的 win32 工具链信号,也不能让聚合流程等待稀缺的 Windows 容量。Wine 提供这项关键路径信号,但它运行在 Linux 内核与区分大小写的 ext4 之上,采用 hoisted 依赖布局,且无法证明 NTFS、DACL、ConPTY、崩溃持久性或原生进程行为。原生串行参考流程停用期间,每个拉取请求分支头还需要自动取得真实 Windows 内核结果。 +Wine 在 Linux 内核与区分大小写的 ext4 之上采用 hoisted 依赖布局检查 win32 工具链。它无法证明 NTFS、DACL、ConPTY、崩溃持久性或原生进程行为。因此,拉取请求的正确性需要原生 Windows 构建和进程检查,独立于合并后的 Wine 结果。 覆盖率审计发现,陈旧分支状态恢复了针对受支持 LSP 源码的临时排除项。因此,原生 Windows 需要按同一逐文件 100% 阈值执行完整的受支持源码清单,而不能依赖缩小后的平台专用分母。 diff --git a/.github/AGENTS.md b/.github/AGENTS.md index 4efddc1bca..100385608f 100644 --- a/.github/AGENTS.md +++ b/.github/AGENTS.md @@ -1,3 +1,3 @@ # AGENTS.md — GitHub Actions -Run jobs on Windows runners (`windows-*` labels) under native `pwsh`. The pull-request `windows` job is the deliberate exception: it runs Windows Node under Wine on hosted Linux and blocks `all checks passed`; `windows-native` runs automatically on `windows-2025` (or the self-hosted `[self-hosted, dsh-win-ci, windows]` pool under `DSH_CI_FAILOVER_WINDOWS=selfhosted`) but reports independently. `ci.yml` is pull-request-only; the master `serial-windows` standby, the Linux `serial-linux-selfhosted` standby, the `wine-apt-cache` seeder, and the two manual runner benchmarks live in `ci-master.yml` (master-push + `workflow_dispatch`). Because `ci-master.yml` does not listen to `pull_request`, those master-only jobs never appear in PR check panels (a job a workflow defines for a given event is listed and shows `skipped` when its `if` is false); keeping them in a separate workflow is what stops PR check circles from showing gray segments. The master `serial-windows` standby continuously validates the self-hosted failover target — see the [failover runbook](../.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md). +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](../.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.md)). `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 and Dependabot hosted fallback ([failover runbook](../.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md)). diff --git a/.github/workflows/ci-master.yml b/.github/workflows/ci-master.yml index 2edb868b81..c795413067 100644 --- a/.github/workflows/ci-master.yml +++ b/.github/workflows/ci-master.yml @@ -78,7 +78,7 @@ jobs: restore-keys: | ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm- - # Master runs restore and seed the image-specific Wine dependency cache. + # Master CI restores and seeds the image-specific Wine dependency cache. - name: Compose Wine apt cache key id: wine-cache-key run: echo "key=wine-debs-${ImageOS:-linux}-${ImageVersion:-v0}" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e673679bb0..fa7ecfdd9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -597,10 +597,9 @@ jobs: # Single stable required check for branch protection: require "all checks # passed" instead of enumerating matrix legs whose names change as lanes and # node versions evolve. Every blocking job in THIS workflow must be listed in - # `needs`. The required Wine job is listed as `windows`; `windows-native` is - # deliberately absent so its independent result never delays or changes this - # verdict. (`needs` cannot reach across workflow files; the master-only jobs in - # ci-master.yml are intentionally not part of this PR verdict.) + # `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. # `if: always()` is load-bearing: without it a failed dependency # would SKIP this job, and GitHub counts a skipped required check as passing # — so this job always runs and fails on any non-success result, including diff --git a/docs/testing.i18n.yaml b/docs/testing.i18n.yaml index 867bccc380..07c65f5661 100644 --- a/docs/testing.i18n.yaml +++ b/docs/testing.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/testing.md -testing.md: 6caee19d4eabd0c285cd9fb985fd212526782f06 -testing.zh.md: b4dd9a21e8c0fdc336936e6d4297f34d6e82f135 +testing.md: 169430c8905d4adee611e2c9947732ffd602481a +testing.zh.md: 8bb3975b3ad07365284a72850e089714fb601566 diff --git a/docs/testing.md b/docs/testing.md index 6caee19d4e..169430c890 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -16,8 +16,6 @@ How this repo tests, tier by tier, and the rules that keep a green suite meaning Session fixtures retain headers and payloads but omit body sequence/time envelopes; replay synthesizes them. Replay, record, and refresh select each parent/child role's highest generation. Current v2 uses `.v2`, one row per event, and embedded compact Assistant streams; retained v0 (suffixless) and v1 (`.v1`) may keep canonical packed rows for migration coverage. [The migrator](../scripts/migrate-packed-session-fixtures.ts) rewrites older historical layouts. -[Python runtime and Wine scheduling](../.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.md). - ## How specs execute Forked workers run several spec files at once, the coverage gate splits into concurrent partitions beside the other gates in its job, and the self-hosted runners share one host and one volume. Only the process is isolated: ports, predictable paths, external namespaces, and inherited children are not. Own each acquired resource through its teardown, and read a spec that passes only when it runs alone as a defect in the spec rather than an unstable runner. [dsh-ci-test-reliability](../.agents/skills/dsh-ci-test-reliability/SKILL.md) owns the allocation, restoration, synchronization, timeout-budget, platform, and teardown rules; its [flake diagnosis workflow](../.agents/skills/dsh-ci-test-reliability/references/ci-flake-diagnosis.md) classifies an existing probabilistic failure. @@ -54,4 +52,4 @@ An e2e assertion re-runs the command or re-reads the file externally; a keyword ## When a snapshot test is required -Every non-trivial model-, protocol-, or human-visible change adds or updates a keyless recorded-session scenario in the same PR; package, e2e, mock-only, and rationale evidence does not replace the assembled transcript. Headless, SDK, ACP, and Web recordings live under `snapshots/session/`, `snapshots/sdk/`, `snapshots/acp/`, and `snapshots/web/`; a Web rendering may explicitly borrow another scenario's canonical session. Expected output that is not driven by a recorded session stays with its owning app, package, or script under `tests/expected/` and does not use the `*.snapshot.ts` suffix. [`dsh-session-snapshot`](../packages/test-support/session-snapshot/README.md) owns the shared storage rules and profile adapters. Agent-loop, session-lifecycle, and `SessionEventMap` changes update both SDK projections: `snapshots/sdk/` owns TypeScript, while required Python-runtime CI owns `scripts/snapshots/python-sdk-single-exe/`. New capability seams and lifecycle or transcript variants name every required tier at plan time. +Every non-trivial model-, protocol-, or human-visible change adds or updates a keyless recorded-session scenario in the same PR; package, e2e, mock-only, and rationale evidence does not replace the assembled transcript. Headless, SDK, ACP, and Web recordings live under `snapshots/session/`, `snapshots/sdk/`, `snapshots/acp/`, and `snapshots/web/`; a Web rendering may explicitly borrow another scenario's canonical session. Expected output that is not driven by a recorded session stays with its owning app, package, or script under `tests/expected/` and does not use the `*.snapshot.ts` suffix. [`dsh-session-snapshot`](../packages/test-support/session-snapshot/README.md) owns the shared storage rules and profile adapters. Agent-loop, session-lifecycle, and `SessionEventMap` changes update both SDK projections: `snapshots/sdk/` owns TypeScript, while [Python-runtime CI](../.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.md) owns `scripts/snapshots/python-sdk-single-exe/`. New capability seams and lifecycle or transcript variants name every required tier at plan time. diff --git a/docs/testing.zh.md b/docs/testing.zh.md index b4dd9a21e8..8bb3975b3a 100644 --- a/docs/testing.zh.md +++ b/docs/testing.zh.md @@ -16,8 +16,6 @@ Session fixture 保留 header 与 payload,但省略正文 seq/time envelope;replay 会合成这些 envelope。Replay、record 与 refresh 会选择每个 parent/child 角色的最高 generation。当前 v2 使用 `.v2`、每个事件一行,并嵌入紧凑 Assistant stream;保留的 v0(无后缀)与 v1(`.v1`)可以为迁移覆盖保留规范 packed row。[迁移器](../scripts/migrate-packed-session-fixtures.ts)会改写更旧的历史布局。 -[Python 运行时与 Wine 调度](../.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.zh.md)。 - ## spec 如何被执行 fork 出的 worker 会同时运行多个 spec 文件,coverage gate 会拆成并发的 partition,与同一个 job 中的其它 gate 并排运行,而自托管 runner 共用同一台宿主机和同一个卷。被隔离的只有进程:端口、可预测路径、外部命名空间和继承而来的子进程都不隔离。为每个占用的资源负责到它的 teardown,并把「只有单独运行时才通过」的 spec 读作该 spec 的缺陷,而不是 runner 不稳定。[dsh-ci-test-reliability](../.agents/skills/dsh-ci-test-reliability/SKILL.md) 负责资源分配、状态恢复、同步、超时预算、平台差异与 teardown 规则;它的 [flake 诊断流程](../.agents/skills/dsh-ci-test-reliability/references/ci-flake-diagnosis.md)用于归类已经存在的概率性失败。 @@ -54,4 +52,4 @@ e2e 断言应重新运行命令或从外部重新读取文件;对 agent 自身 ## 何时需要快照测试 -每项非平凡的模型可见、协议可见或人类可见变更,都在同一 PR 中添加或更新无密钥录制会话场景;包级、e2e、仅 mock 和 PR 理由证据不能取代组装后的 transcript。Headless、SDK、ACP 和 Web 录制分别位于 `snapshots/session/`、`snapshots/sdk/`、`snapshots/acp/` 和 `snapshots/web/`;Web 渲染可以显式借用另一个场景的规范会话。不由录制会话驱动的预期输出保留在所属应用、包或脚本的 `tests/expected/` 下,并且不使用 `*.snapshot.ts` 后缀。[`dsh-session-snapshot`](../packages/test-support/session-snapshot/README.zh.md) 拥有共享存储规则和 profile 适配器。Agent loop、会话生命周期和 `SessionEventMap` 变更应更新两个 SDK 投影:`snapshots/sdk/` 拥有 TypeScript,必需的 Python 运行时 CI 拥有 `scripts/snapshots/python-sdk-single-exe/`。新增 capability seam、生命周期或 transcript 变体应在计划阶段列出每个必需层级。 +每项非平凡的模型可见、协议可见或人类可见变更,都在同一 PR 中添加或更新无密钥录制会话场景;包级、e2e、仅 mock 和 PR 理由证据不能取代组装后的 transcript。Headless、SDK、ACP 和 Web 录制分别位于 `snapshots/session/`、`snapshots/sdk/`、`snapshots/acp/` 和 `snapshots/web/`;Web 渲染可以显式借用另一个场景的规范会话。不由录制会话驱动的预期输出保留在所属应用、包或脚本的 `tests/expected/` 下,并且不使用 `*.snapshot.ts` 后缀。[`dsh-session-snapshot`](../packages/test-support/session-snapshot/README.zh.md) 拥有共享存储规则和 profile 适配器。Agent loop、会话生命周期和 `SessionEventMap` 变更应更新两个 SDK 投影:`snapshots/sdk/` 拥有 TypeScript,[Python 运行时 CI](../.agents/notes/implemented/process/2026-09-06-master-only-platform-ci.zh.md) 拥有 `scripts/snapshots/python-sdk-single-exe/`。新增 capability seam、生命周期或 transcript 变体应在计划阶段列出每个必需层级。 diff --git a/scripts/ci-workflow.spec.ts b/scripts/ci-workflow.spec.ts index 3c9bd896e6..8f0d2e51cd 100644 --- a/scripts/ci-workflow.spec.ts +++ b/scripts/ci-workflow.spec.ts @@ -202,7 +202,6 @@ describe('CI workflow', () => { expect(serialGate).toBeDefined() expect(serialGate!.env).toMatchObject({ DSH_COVERAGE_TEST_TIMEOUT_MS: '90000' }) - // Aggregate: Wine and the required split native jobs are needed; // windows-coverage is temporarily non-blocking while Windows ACP // half-close tests are stabilized; observational stays out too. expect(aggregate.needs).not.toContain('windows') diff --git a/scripts/tests/ci-master-platforms.spec.ts b/scripts/tests/ci-master-platforms.spec.ts index b75d65c87f..b07f71cd62 100644 --- a/scripts/tests/ci-master-platforms.spec.ts +++ b/scripts/tests/ci-master-platforms.spec.ts @@ -95,9 +95,18 @@ describe('master-only platform scheduling', () => { })) expect(commands(wine).join('\n')).toContain('--download-only wine') expect(wine.steps).toContainEqual(expect.objectContaining({ name: 'Shut down wineserver', if: 'always()' })) - for (const mode of ['ci-linux-primary', 'ci-windows-complete'] as const) { - expect(gatesForMode(mode).map(gate => gate.displayCommand).join('\n')).not.toMatch(/wine/i) + // Graph construction needs a pnpm entrypoint but never launches it. + const previous = process.env.npm_execpath + process.env.npm_execpath = '/test/pnpm.cjs' + try { + for (const mode of ['ci-linux-primary', 'ci-windows-complete'] as const) { + expect(gatesForMode(mode).map(gate => gate.displayCommand).join('\n')).not.toMatch(/wine/i) + } + } finally { + if (previous === undefined) Reflect.deleteProperty(process.env, 'npm_execpath') + else process.env.npm_execpath = previous } + expect(process.env.npm_execpath).toBe(previous) }) it('retains the complete release matrix independently of CI scheduling', () => { From f7a18f49ffd815060f6cb35da7bac8056ab95cb4 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 6 Sep 2026 13:00:47 +0800 Subject: [PATCH 3/6] ci: isolate compatibility Node jobs on self-hosted Linux --- ...rial-cross-platform-ci-reference.i18n.yaml | 4 +- ...7-21-serial-cross-platform-ci-reference.md | 2 +- ...1-serial-cross-platform-ci-reference.zh.md | 2 +- .../2026-07-26-ci-failover-runbook.i18n.yaml | 4 +- .../process/2026-07-26-ci-failover-runbook.md | 2 +- .../2026-07-26-ci-failover-runbook.zh.md | 2 +- ...06-node-compatibility-selfhosted.i18n.yaml | 6 + ...026-09-06-node-compatibility-selfhosted.md | 35 +++++ ...-09-06-node-compatibility-selfhosted.zh.md | 35 +++++ .github/workflows/ci.yml | 25 +++- docs/ci-compatible-selfhosted.i18n.yaml | 6 + docs/ci-compatible-selfhosted.md | 39 ++++++ docs/ci-compatible-selfhosted.zh.md | 39 ++++++ scripts/ci-compatible-selfhosted.spec.ts | 131 ++++++++++++++++++ 14 files changed, 321 insertions(+), 11 deletions(-) create mode 100644 .agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.i18n.yaml create mode 100644 .agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md create mode 100644 .agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.zh.md create mode 100644 docs/ci-compatible-selfhosted.i18n.yaml create mode 100644 docs/ci-compatible-selfhosted.md create mode 100644 docs/ci-compatible-selfhosted.zh.md create mode 100644 scripts/ci-compatible-selfhosted.spec.ts diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml index 3263246d05..12c5d5d1cc 100644 --- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md -2026-07-21-serial-cross-platform-ci-reference.md: e92104cce5b726797f7b4d88c98cf3b837cba6b4 -2026-07-21-serial-cross-platform-ci-reference.zh.md: a8787006b7d44ecab94f01b771b62bfa0ae3224b +2026-07-21-serial-cross-platform-ci-reference.md: fb44f1122eae8120615ab5963d40b8fa3fb6b3bb +2026-07-21-serial-cross-platform-ci-reference.zh.md: 9a316b13912347e2bfff0fbf866a8470c7070029 diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md index e92104cce5..fb44f1122e 100644 --- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md +++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md @@ -16,7 +16,7 @@ Real-kernel sandbox proofs require specific hosted operating systems and archite ## Decision -[CI](../../../../.github/workflows/ci.yml) (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, and Python checks; [platform scheduling](2026-09-06-master-only-platform-ci.md) 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) (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 isolated self-hosted routing](2026-09-06-node-compatibility-selfhosted.md), and Python checks; [platform scheduling](2026-09-06-master-only-platform-ci.md) 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. diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md index a8787006b7..9a316b1391 100644 --- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md +++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md @@ -16,7 +16,7 @@ Status: implemented ## 决策 -[CI](../../../../.github/workflows/ci.yml)(仅 pull request)与 [CI master](../../../../.github/workflows/ci-master.yml)(master 推送 + `workflow_dispatch`)为拉取请求事件与 master 推送事件赋予互补的职责。拉取请求运行 Linux、原生 Windows、Node 兼容性与 Python 检查;[平台调度](2026-09-06-master-only-platform-ci.zh.md)将 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)(仅 pull request)与 [CI master](../../../../.github/workflows/ci-master.yml)(master 推送 + `workflow_dispatch`)为拉取请求事件与 master 推送事件赋予互补的职责。拉取请求运行 Linux、原生 Windows、[使用隔离自托管路由的 Node 兼容性](2026-09-06-node-compatibility-selfhosted.zh.md)与 Python 检查;[平台调度](2026-09-06-master-only-platform-ci.zh.md)将 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 则在安装采用符号链接的工作区前启用开发人员模式。 diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml index 50e7fa9040..012d82fc53 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md -2026-07-26-ci-failover-runbook.md: 9fbdd76ce3a376ea5b4e86584f14c3558bddff9e -2026-07-26-ci-failover-runbook.zh.md: fda030d9c628709c31ec53e767c74989f130a7b1 +2026-07-26-ci-failover-runbook.md: 68fcbf956410235bb245354477234fb921561d7c +2026-07-26-ci-failover-runbook.zh.md: 7f3cffe8b34250b32351cd890f7de1c129ca992b diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md index 9fbdd76ce3..68fcbf9564 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md @@ -6,7 +6,7 @@ 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 verdict's other required dependencies (`node-24-bench`, `node-compat`, `python-sdk`, `python-runtime`) stay on standard hosted runners by design (the portable boundary); in a broader GitHub-hosted capacity failure that also takes out the standard pools, those dependencies still block `all checks passed`. An outage therefore needs a switch any responder with repository write access can throw without merging anything. +The three required Linux worker jobs in [CI](../../../../.github/workflows/ci.yml) (`node 24 / static`, `node 24 / coverage`, `node 24 / snapshots and artifacts`) run on the hosted enterprise 32-core pools; the required verdict job that aggregates them (`all checks passed`) runs on standard `ubuntu-latest`; 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](2026-09-06-node-compatibility-selfhosted.md) 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. ## Decision diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md index fda030d9c6..7f3cffe8b3 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md @@ -6,7 +6,7 @@ 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-24-bench`、`node-compat`、`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)运行在托管的 `dsh-windows-2025-16core` 大型运行器上。当企业池发生故障——作业无限排队或企业标签消失——所有开启的拉取请求都无法合并,而"合并一个修复"这一常规恢复手段本身正被那些无法运行的必需检查死锁。**适用范围:两个独立开关,每个平台一个。**`DSH_CI_FAILOVER_LINUX` 恢复企业级 Linux 池故障(三个必需的 Linux 工作作业加 `all checks passed` 判定作业);`DSH_CI_FAILOVER_WINDOWS` 恢复托管 Windows 池故障(原生 Windows 作业)。Linux 池故障无需重定向 Windows 作业,反之亦然。[Node 兼容性作业](2026-09-06-node-compatibility-selfhosted.zh.md)也通过隔离设置跟随 Linux 开关;判定作业的 `node-24-bench`、`python-sdk` 和 `python-runtime` 依赖仍留在标准托管运行器上;若更大范围的 GitHub 托管容量故障连标准池一并击倒,这些依赖仍会阻塞 `all checks passed`。因此故障需要一个任何具备仓库写权限的响应者都能在不合并任何代码的情况下触发的开关。 ## 决策 diff --git a/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.i18n.yaml b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.i18n.yaml new file mode 100644 index 0000000000..f65f8cb889 --- /dev/null +++ b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md +2026-09-06-node-compatibility-selfhosted.md: e62be31b4383d9d13548565dce6371a4b250857d +2026-09-06-node-compatibility-selfhosted.zh.md: ca8457d160dfd5f48010eb80ee03984425e29392 diff --git a/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md new file mode 100644 index 0000000000..e62be31b43 --- /dev/null +++ b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md @@ -0,0 +1,35 @@ +# Agent Note: Isolated Node compatibility jobs on self-hosted Linux + +Status: implemented + +English | [中文](2026-09-06-node-compatibility-selfhosted.zh.md) + +## Problem + +The Node 22.19, 24.9, and 26 compatibility jobs consume hosted Linux minutes even when the repository has selected its existing self-hosted Linux pool. Moving version installers onto a persistent shared machine can create tool-directory collisions and accumulate generated cache files outside runner cleanup. + +## Decision + +[CI](../../../../.github/workflows/ci.yml) applies the Linux failover variable to these three jobs, requiring a non-Dependabot author and a non-fork head repository matching the current repository. The standard hosted fallback remains available. [Runner documentation](../../../../docs/ci-compatible-selfhosted.md) owns setup and cleanup behavior. + +The temporary tool cache trades repeated Node downloads for isolation across concurrent runners and Node versions. pnpm keeps its existing private setup destination and persistent content-addressed store. Compile caches and node-gyp headers use runner temp before the first pnpm invocation. No global Node symlink or system package changes are introduced. Hosted jobs retain their tool and package caching. + +The [failover runbook](2026-07-26-ci-failover-runbook.md) remains the owner of repository trust and pool switching. The [serial reference decision](2026-07-21-serial-cross-platform-ci-reference.md) remains the owner of master scheduling. Neither decision is superseded beyond the compatibility jobs' runner selection; both remain active. + +## Alternatives considered + +**Keep all compatibility jobs hosted.** This avoids extra shared-host load but continues paying for Linux runtime checks that do not require a different operating system or architecture. + +**Use the shared Node installation or global version-manager links.** The jobs must run different Node releases concurrently. Mutable shared links would make the selected version depend on another job's timing. + +**Move the Python SDK job in the same change.** Its setup-python installation and global pip installation of uv need separate isolation evidence. Its short hosted job is not required for the Node optimization. + +## Consequences + +The pool receives three additional jobs per trusted PR; each retains gate concurrency one, including the build-backed Node 22 leg. The September 6 inventory reports 31 Linux registrations, not 31 independent machines. The shared VM's contention and download latency remain rollout risks; the variable preserves hosted recovery. Test inventory, check names, and master scheduling are unchanged. + +## Verification + +The focused [workflow regression](../../../../scripts/ci-compatible-selfhosted.spec.ts) executes the actual routing expressions and environment setup. A negative control removing the fork condition fails the hosted-fallback assertion. It checks Dependabot reruns by a maintainer, repository mismatch, fork flags, disabled variables, and runner-scoped cache paths. + +[Successful standby run 33984559660](https://github.com/deepseek-harness/deepseek-harness/actions/runs/33984559660) at the implementation base supplies Linux Node 24.19.0 and Windows Node 24.20.0 baseline evidence. Linux job 101359402557 uses runner-specific temporary and tool directories on the data volume. [Read-only capability probe 34012679056](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34012679056/job/101431064925) reports Linux x64, 192 online logical CPUs, GCC/G++ 13.3, Make 4.3, and Python 3.12.3. Python 3.10 is absent, reinforcing the separate SDK provisioning requirement. That baseline does not prove these three exact Node versions on self-hosted Linux; their PR matrix execution is the platform verification owner. diff --git a/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.zh.md b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.zh.md new file mode 100644 index 0000000000..ca8457d160 --- /dev/null +++ b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.zh.md @@ -0,0 +1,35 @@ +# Agent Note: 自托管 Linux 上隔离的 Node 兼容性作业 + +Status: implemented + +[English](2026-09-06-node-compatibility-selfhosted.md) | 中文 + +## 问题 + +即使仓库已经选择现有的自托管 Linux 池,Node 22.19、24.9 和 26 兼容性作业仍消耗托管 Linux 分钟数。将版本安装器移到持久化共享机器上可能造成工具目录冲突,并让生成的缓存文件积累在运行器清理范围之外。 + +## 决策 + +[CI](../../../../.github/workflows/ci.yml) 将 Linux 故障切换变量应用于这三个作业,要求作者不是 Dependabot,且非 fork 的头部仓库与当前仓库相同。标准托管回退仍然可用。[运行器文档](../../../../docs/ci-compatible-selfhosted.zh.md) 拥有安装与清理行为的说明。 + +临时工具缓存以重复下载 Node 为代价,换取并发运行器与 Node 版本之间的隔离。pnpm 保留现有的私有安装目录和持久化内容寻址 store。编译缓存与 node-gyp 头文件在首次调用 pnpm 前就使用运行器临时目录。不引入全局 Node 符号链接或系统软件包变更。托管作业保留其工具与软件包缓存。 + +[故障切换手册](2026-07-26-ci-failover-runbook.zh.md) 仍拥有仓库信任与池切换规则。[串行参考决策](2026-07-21-serial-cross-platform-ci-reference.zh.md) 仍拥有 master 调度规则。除兼容性作业的运行器选择外,这两个决策都未被取代;两者均保持活跃。 + +## 曾考虑的替代方案 + +**让所有兼容性作业保持托管。** 这避免额外的共享主机负载,但继续为不需要不同操作系统或架构的 Linux 运行时检查付费。 + +**使用共享 Node 安装或全局版本管理器链接。** 这些作业必须并发运行不同的 Node 版本。可变的共享链接会使选中的版本取决于另一作业的时序。 + +**在同一改动中迁移 Python SDK 作业。** 其 setup-python 安装和通过全局 pip 安装 uv 需要单独的隔离证据。这个短暂的托管作业不是 Node 优化的必需部分。 + +## 后果 + +每个可信 PR(Pull Request)会为池增加三个作业;每个作业保留门禁并发度一,包括需要构建的 Node 22 条目。9 月 6 日的清单报告了 31 个 Linux 注册实例,而不是 31 台独立机器。共享虚拟机的资源争用和下载延迟仍是上线风险;变量保留托管恢复路径。测试清单、检查名称和 master 调度保持不变。 + +## 验证 + +聚焦的[工作流回归测试](../../../../scripts/ci-compatible-selfhosted.spec.ts) 执行真实的路由表达式和环境设置。移除 fork 条件的负对照使托管回退断言失败。它检查维护者重跑 Dependabot PR、仓库不匹配、fork 标志、禁用变量以及运行器范围内的缓存路径。 + +实施基线上的[成功热备运行 33984559660](https://github.com/deepseek-harness/deepseek-harness/actions/runs/33984559660) 提供 Linux Node 24.19.0 和 Windows Node 24.20.0 基线证据。Linux 作业 101359402557 使用数据卷上运行器专属的临时目录和工具目录。[只读能力探测 34012679056](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34012679056/job/101431064925) 报告 Linux x64、192 个在线逻辑 CPU、GCC/G++ 13.3、Make 4.3 和 Python 3.12.3。Python 3.10 缺失,进一步说明 SDK 需要单独配置。该基线不能证明自托管 Linux 上这三个精确 Node 版本的行为;其 PR 矩阵执行拥有平台验证责任。 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa7ecfdd9e..03a0bfbeee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -291,8 +291,14 @@ jobs: node-compat: if: github.event_name == 'pull_request' - # Each compatibility contract receives an independent standard hosted job. - runs-on: ${{ matrix.runner }} + # Only repository-owned PR code may reach the persistent shared VM. + runs-on: >- + ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' + && github.event.pull_request.head.repo.full_name == github.repository + && github.event.pull_request.head.repo.fork == false + && github.event.pull_request.user.login != 'dependabot[bot]' + && fromJSON('["self-hosted", "linux", "x64", "vm-backup"]') + || matrix.runner }} name: ${{ matrix.name }} env: DSH_GATE_CONCURRENCY: ${{ matrix.gate_concurrency }} @@ -321,15 +327,28 @@ jobs: gate_concurrency: '1' steps: - uses: actions/checkout@v6 + with: + persist-credentials: false + + # Shared hosts keep version installs and generated caches inside runner temp. + - name: Isolate compatibility caches + if: runner.environment == 'self-hosted' + run: | + echo "NODE_COMPILE_CACHE=$RUNNER_TEMP/node-compile-cache" >> "$GITHUB_ENV" + echo "npm_config_devdir=$RUNNER_TEMP/node-gyp" >> "$GITHUB_ENV" + echo "PNPM_CONFIG_STORE_DIR=$HOME/.local/share/pnpm/store" >> "$GITHUB_ENV" - uses: pnpm/action-setup@v4 with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }} - uses: actions/setup-node@v6 + env: + RUNNER_TOOL_CACHE: ${{ runner.environment == 'self-hosted' && format('{0}/node-compat-toolcache', runner.temp) || runner.tool_cache }} with: node-version: ${{ matrix.node }} - cache: pnpm + cache: ${{ runner.environment == 'github-hosted' && 'pnpm' || '' }} + package-manager-cache: false - name: Install (immutable) run: pnpm install --frozen-lockfile diff --git a/docs/ci-compatible-selfhosted.i18n.yaml b/docs/ci-compatible-selfhosted.i18n.yaml new file mode 100644 index 0000000000..b127493d64 --- /dev/null +++ b/docs/ci-compatible-selfhosted.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/ci-compatible-selfhosted.md +ci-compatible-selfhosted.md: b8f4efd31449c83d11c1db9802dfe1255743edb1 +ci-compatible-selfhosted.zh.md: 592f9d6b467315b3ed88b81df9fbd90a206f7f8b diff --git a/docs/ci-compatible-selfhosted.md b/docs/ci-compatible-selfhosted.md new file mode 100644 index 0000000000..b8f4efd314 --- /dev/null +++ b/docs/ci-compatible-selfhosted.md @@ -0,0 +1,39 @@ +# Node compatibility CI runners + +English | [中文](ci-compatible-selfhosted.zh.md) + +## Summary + +The three Node compatibility jobs can use the existing Linux self-hosted pool without changing their versions, required checks, or master scheduling. [CI](../.github/workflows/ci.yml) owns the runner selection; the [decision record](../.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md) explains isolation and trade-offs. + +## Table of Contents + +- [Runner selection](#runner-selection) +- [Installation and cleanup](#installation-and-cleanup) +- [Verification](#verification) + + + +## Runner selection + +Node 22.19, 24.9, and 26 select `[self-hosted, linux, x64, vm-backup]` only when `DSH_CI_FAILOVER_LINUX=selfhosted` and the PR author is not Dependabot, the head repository matches the current repository, and the head repository is not a fork. All other cases select `ubuntu-latest`. The Python SDK job remains hosted. + +Each matrix entry runs one repository gate at a time. The matrix retains independent jobs and does not cancel sibling versions on failure. Runner registrations share host resources; their count is not a count of independent machines. + + + +## Installation and cleanup + +Self-hosted Node installations use a tool cache beneath `runner.temp`. pnpm setup uses its runner-and-run-private destination. Node compile caches and node-gyp headers also stay beneath runner temp; the pnpm content-addressed store remains persistent. Hosted jobs retain their normal tool cache and pnpm caching. Self-hosted jobs do not restore or upload hosted package caches. + +The runner owns temporary-directory cleanup between jobs. These jobs do not install system packages or change global Node symlinks. The shared image must already provide the compiler and Python dependencies needed by native npm packages. A cold temporary Node cache requires downloading the selected runtime again. + + + +## Verification + +`pnpm exec vitest run scripts/ci-compatible-selfhosted.spec.ts scripts/ci-workflow.spec.ts` checks routing, hosted fallback, matrix preservation, cache paths, and the executed environment setup. The actual Node matrix on the self-hosted host remains the required platform verification; local workflow tests do not prove native runtime compatibility or capacity under concurrent PR load. + +## Dev Note + +None. diff --git a/docs/ci-compatible-selfhosted.zh.md b/docs/ci-compatible-selfhosted.zh.md new file mode 100644 index 0000000000..592f9d6b46 --- /dev/null +++ b/docs/ci-compatible-selfhosted.zh.md @@ -0,0 +1,39 @@ +# Node 兼容性 CI 运行器 + +[English](ci-compatible-selfhosted.md) | 中文 + +## 摘要 + +三个 Node 兼容性作业可以使用现有的 Linux 自托管池,而不改变其版本、必需检查或 master 调度。[CI](../.github/workflows/ci.yml) 拥有运行器选择逻辑;[决策记录](../.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.zh.md) 解释隔离和取舍。 + +## 目录 + +- [运行器选择](#runner-selection) +- [安装与清理](#installation-and-cleanup) +- [验证](#verification) + + + +## 运行器选择 + +Node 22.19、24.9 和 26 仅在 `DSH_CI_FAILOVER_LINUX=selfhosted`,且 PR(Pull Request)作者不是 Dependabot、头部仓库与当前仓库相同、头部仓库不是 fork 时选择 `[self-hosted, linux, x64, vm-backup]`。其余情况均选择 `ubuntu-latest`。Python SDK 作业仍使用托管运行器。 + +每个矩阵条目一次运行一个仓库门禁。矩阵保留独立作业,不会因某个版本失败而取消其他版本。运行器注册实例共享主机资源;注册数量不等于独立机器数量。 + + + +## 安装与清理 + +自托管 Node 安装使用 `runner.temp` 下的工具缓存。pnpm 设置使用运行器和运行私有的目标目录。Node 编译缓存和 node-gyp 头文件也保留在运行器临时目录下;pnpm 内容寻址 store 保持持久化。托管作业保留其常规工具缓存和 pnpm 缓存。自托管作业不恢复或上传托管软件包缓存。 + +运行器负责作业之间的临时目录清理。这些作业不安装系统软件包,也不修改全局 Node 符号链接。共享镜像必须已提供原生 npm 软件包所需的编译器和 Python 依赖。冷的临时 Node 缓存需要重新下载所选运行时。 + + + +## 验证 + +`pnpm exec vitest run scripts/ci-compatible-selfhosted.spec.ts scripts/ci-workflow.spec.ts` 检查路由、托管回退、矩阵保留、缓存路径和实际执行的环境设置。自托管主机上的真实 Node 矩阵仍是必需的平台验证;本地工作流测试不能证明原生运行时兼容性或并发 PR 负载下的容量。 + +## 开发备注 + +无。 diff --git a/scripts/ci-compatible-selfhosted.spec.ts b/scripts/ci-compatible-selfhosted.spec.ts new file mode 100644 index 0000000000..8c3ddd2165 --- /dev/null +++ b/scripts/ci-compatible-selfhosted.spec.ts @@ -0,0 +1,131 @@ +import { mkdtempSync, readFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { spawnSync } from 'node:child_process' +import { runInNewContext } from 'node:vm' +import * as yaml from 'js-yaml' +import { describe, expect, it } from 'vitest' + +interface Step { + name?: string + uses?: string + if?: string + run?: string + env?: Record + with?: Record +} + +interface CompatibilityJob { + 'runs-on': string + if: string + env: Record + strategy: { 'fail-fast': boolean; matrix: { include: Array<{ node: string | number; name: string; runner: string; gate_concurrency: string }> } } + steps: Step[] +} + +const workflow = yaml.load(readFileSync(resolve(import.meta.dirname, '../.github/workflows/ci.yml'), 'utf8')) as { + jobs: { 'node-compat': CompatibilityJob; 'python-sdk': { 'runs-on': string } } +} +const job = workflow.jobs['node-compat'] +const labels = ['self-hosted', 'linux', 'x64', 'vm-backup'] + +// These expressions use only boolean operators and equal-typed string/boolean +// comparisons, whose results agree in Actions and JavaScript. +function evaluate(expression: string, context: Record): unknown { + const body = expression.trim().slice(3, -2) + return runInNewContext(body, { + ...context, fromJSON: JSON.parse, format: (template: string, value: string) => template.replace('{0}', value), + }, { timeout: 1000 }) as unknown +} + +function route(options: { mode?: string; author?: string; repository?: string; fork?: boolean; actor?: string } = {}): unknown { + return evaluate(job['runs-on'], { + vars: { DSH_CI_FAILOVER_LINUX: options.mode ?? 'selfhosted' }, + github: { + repository: 'deepseek-harness/deepseek-harness', + actor: options.actor ?? 'maintainer', + event: { pull_request: { + user: { login: options.author ?? 'maintainer' }, + head: { repo: { full_name: options.repository ?? 'deepseek-harness/deepseek-harness', fork: options.fork ?? false } }, + } }, + }, + matrix: { runner: 'ubuntu-latest' }, + }) +} + +describe('Node compatibility self-hosted routing', () => { + it('uses the Linux pool only for opted-in repository-owned PRs', () => { + expect(route()).toEqual(labels) + for (const mode of ['', 'hosted', 'unexpected']) expect(route({ mode })).toBe('ubuntu-latest') + expect(route({ author: 'dependabot[bot]', actor: 'maintainer' })).toBe('ubuntu-latest') + expect(route({ repository: 'outsider/fork', fork: true })).toBe('ubuntu-latest') + expect(route({ repository: 'outsider/fork', fork: false })).toBe('ubuntu-latest') + expect(route({ fork: true })).toBe('ubuntu-latest') + expect(route({ repository: '' })).toBe('ubuntu-latest') + }) + + it('preserves all three required version jobs and their concurrency', () => { + expect(job.if).toBe("github.event_name == 'pull_request'") + expect(job.strategy['fail-fast']).toBe(false) + expect(job.strategy.matrix.include).toEqual([ + { node: '22.19', name: 'node 22.19', runner: 'ubuntu-latest', gate_concurrency: '1' }, + { node: '24.9', name: 'node 24.9', runner: 'ubuntu-latest', gate_concurrency: '1' }, + { node: 26, name: 'node 26', runner: 'ubuntu-latest', gate_concurrency: '1' }, + ]) + 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') + }) + + it('isolates version installs and enables hosted package caching only on hosted runners', () => { + const setup = job.steps.find(step => step.uses === 'actions/setup-node@v6')! + expect(setup.env).toEqual({ + RUNNER_TOOL_CACHE: "${{ runner.environment == 'self-hosted' && format('{0}/node-compat-toolcache', runner.temp) || runner.tool_cache }}", + }) + expect(setup.with?.['node-version']).toBe('${{ matrix.node }}') + expect(setup.with?.['package-manager-cache']).toBe(false) + for (const [environment, cache] of [['github-hosted', 'pnpm'], ['self-hosted', '']]) { + const context = { runner: { environment, temp: '/runner/temp', tool_cache: '/runner/toolcache' } } + expect(evaluate(setup.with?.cache as string, context)).toBe(cache) + expect(evaluate(setup.env!.RUNNER_TOOL_CACHE!, context)).toBe( + environment === 'self-hosted' ? '/runner/temp/node-compat-toolcache' : '/runner/toolcache', + ) + } + expect(job.steps[0]?.with).toEqual({ 'persist-credentials': false }) + expect(job.steps.some(step => step.uses?.startsWith('actions/cache/'))).toBe(false) + }) + + it.skipIf(process.platform === 'win32')('configures generated caches before pnpm without changing HOME or global links', () => { + const index = job.steps.findIndex(step => step.name === 'Isolate compatibility caches') + const step = job.steps[index]! + expect(index).toBeGreaterThan(0) + expect(index).toBeLessThan(job.steps.findIndex(candidate => candidate.uses === 'pnpm/action-setup@v4')) + expect(step.if).toBe("runner.environment == 'self-hosted'") + const root = mkdtempSync(join(tmpdir(), 'ci-compatible-selfhosted-')) + try { + const outputs = ['runner-a', 'runner-b'].map((runner) => { + const envFile = join(root, runner + '.env') + const temp = join(root, runner) + const child = spawnSync('bash', ['-e', '-u', '-o', 'pipefail', '-c', step.run!], { + env: { PATH: process.env.PATH, HOME: join(root, 'shared home'), RUNNER_TEMP: temp, GITHUB_ENV: envFile }, + encoding: 'utf8', timeout: 10_000, + }) + expect(child.error).toBeUndefined() + expect(child.signal).toBeNull() + expect(child.status, child.stderr).toBe(0) + const output = readFileSync(envFile, 'utf8') + expect(output).toBe([ + 'NODE_COMPILE_CACHE=' + temp + '/node-compile-cache', + 'npm_config_devdir=' + temp + '/node-gyp', + 'PNPM_CONFIG_STORE_DIR=' + join(root, 'shared home') + '/.local/share/pnpm/store', + '', + ].join('\n')) + return output + }) + expect(outputs[0]).not.toBe(outputs[1]) + } finally { + rmSync(root, { recursive: true, force: true }) + } + }) +}) From a137256f809ad15bb27a2bdbeec66a32d86027ea Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 6 Sep 2026 13:21:17 +0800 Subject: [PATCH 4/6] ci: set compatibility toolcache after runner environment export --- ...06-node-compatibility-selfhosted.i18n.yaml | 4 +- ...026-09-06-node-compatibility-selfhosted.md | 2 +- ...-09-06-node-compatibility-selfhosted.zh.md | 2 +- .github/workflows/ci.yml | 13 ++++- docs/ci-compatible-selfhosted.i18n.yaml | 4 +- docs/ci-compatible-selfhosted.md | 2 +- docs/ci-compatible-selfhosted.zh.md | 2 +- scripts/ci-compatible-selfhosted.spec.ts | 55 +++++++++++++++++-- scripts/ci-compatible-toolcache.mjs | 8 +++ 9 files changed, 79 insertions(+), 13 deletions(-) create mode 100644 scripts/ci-compatible-toolcache.mjs diff --git a/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.i18n.yaml b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.i18n.yaml index f65f8cb889..32e9a52aa9 100644 --- a/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.i18n.yaml +++ b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md -2026-09-06-node-compatibility-selfhosted.md: e62be31b4383d9d13548565dce6371a4b250857d -2026-09-06-node-compatibility-selfhosted.zh.md: ca8457d160dfd5f48010eb80ee03984425e29392 +2026-09-06-node-compatibility-selfhosted.md: 44015255a464e75b481ed41ac22f60421eed6059 +2026-09-06-node-compatibility-selfhosted.zh.md: 2794b47e02537341bae4d1fd2b434ebf3ba0ff3c diff --git a/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md index e62be31b43..44015255a4 100644 --- a/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md +++ b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md @@ -12,7 +12,7 @@ The Node 22.19, 24.9, and 26 compatibility jobs consume hosted Linux minutes eve [CI](../../../../.github/workflows/ci.yml) applies the Linux failover variable to these three jobs, requiring a non-Dependabot author and a non-fork head repository matching the current repository. The standard hosted fallback remains available. [Runner documentation](../../../../docs/ci-compatible-selfhosted.md) owns setup and cleanup behavior. -The temporary tool cache trades repeated Node downloads for isolation across concurrent runners and Node versions. pnpm keeps its existing private setup destination and persistent content-addressed store. Compile caches and node-gyp headers use runner temp before the first pnpm invocation. No global Node symlink or system package changes are introduced. Hosted jobs retain their tool and package caching. +The temporary tool cache trades repeated Node downloads for isolation across concurrent runners and Node versions. A setup-node-only [ESM preload](../../../../scripts/ci-compatible-toolcache.mjs) assigns the cache inside the action process: the Actions runner overwrites reserved environment variables after reading step configuration. An executed path check rejects installations outside runner temp; compatibility processes do not inherit the preload. pnpm keeps its existing private setup destination and persistent content-addressed store. Compile caches and node-gyp headers use runner temp before the first pnpm invocation. No global Node symlink or system package changes are introduced. Hosted jobs retain their tool and package caching. The [failover runbook](2026-07-26-ci-failover-runbook.md) remains the owner of repository trust and pool switching. The [serial reference decision](2026-07-21-serial-cross-platform-ci-reference.md) remains the owner of master scheduling. Neither decision is superseded beyond the compatibility jobs' runner selection; both remain active. diff --git a/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.zh.md b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.zh.md index ca8457d160..2794b47e02 100644 --- a/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.zh.md +++ b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.zh.md @@ -12,7 +12,7 @@ Status: implemented [CI](../../../../.github/workflows/ci.yml) 将 Linux 故障切换变量应用于这三个作业,要求作者不是 Dependabot,且非 fork 的头部仓库与当前仓库相同。标准托管回退仍然可用。[运行器文档](../../../../docs/ci-compatible-selfhosted.zh.md) 拥有安装与清理行为的说明。 -临时工具缓存以重复下载 Node 为代价,换取并发运行器与 Node 版本之间的隔离。pnpm 保留现有的私有安装目录和持久化内容寻址 store。编译缓存与 node-gyp 头文件在首次调用 pnpm 前就使用运行器临时目录。不引入全局 Node 符号链接或系统软件包变更。托管作业保留其工具与软件包缓存。 +临时工具缓存以重复下载 Node 为代价,换取并发运行器与 Node 版本之间的隔离。仅用于 setup-node 的 [ESM 预加载模块](../../../../scripts/ci-compatible-toolcache.mjs) 在 action 进程内指定缓存:Actions 运行器在读取步骤配置后会覆盖保留的环境变量。实际执行的路径检查拒绝运行器临时目录之外的安装;兼容性进程不继承预加载设置。pnpm 保留现有的私有安装目录和持久化内容寻址 store。编译缓存与 node-gyp 头文件在首次调用 pnpm 前就使用运行器临时目录。不引入全局 Node 符号链接或系统软件包变更。托管作业保留其工具与软件包缓存。 [故障切换手册](2026-07-26-ci-failover-runbook.zh.md) 仍拥有仓库信任与池切换规则。[串行参考决策](2026-07-21-serial-cross-platform-ci-reference.zh.md) 仍拥有 master 调度规则。除兼容性作业的运行器选择外,这两个决策都未被取代;两者均保持活跃。 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03a0bfbeee..944e36bdb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -344,12 +344,23 @@ jobs: - uses: actions/setup-node@v6 env: - RUNNER_TOOL_CACHE: ${{ runner.environment == 'self-hosted' && format('{0}/node-compat-toolcache', runner.temp) || runner.tool_cache }} + # The runner overwrites RUNNER_* step env before starting JavaScript actions. + NODE_OPTIONS: ${{ runner.environment == 'self-hosted' && '--import=./scripts/ci-compatible-toolcache.mjs' || '' }} with: node-version: ${{ matrix.node }} cache: ${{ runner.environment == 'github-hosted' && 'pnpm' || '' }} package-manager-cache: false + - name: Verify isolated Node installation + if: runner.environment == 'self-hosted' + run: | + node_path=$(node -p process.execPath) + echo "Node executable: $node_path" + case "$node_path" in + "$RUNNER_TEMP/node-compat-toolcache/"*) ;; + *) echo "::error::Node compatibility installation is outside runner temp"; exit 1 ;; + esac + - name: Install (immutable) run: pnpm install --frozen-lockfile diff --git a/docs/ci-compatible-selfhosted.i18n.yaml b/docs/ci-compatible-selfhosted.i18n.yaml index b127493d64..e29c41b681 100644 --- a/docs/ci-compatible-selfhosted.i18n.yaml +++ b/docs/ci-compatible-selfhosted.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/ci-compatible-selfhosted.md -ci-compatible-selfhosted.md: b8f4efd31449c83d11c1db9802dfe1255743edb1 -ci-compatible-selfhosted.zh.md: 592f9d6b467315b3ed88b81df9fbd90a206f7f8b +ci-compatible-selfhosted.md: 5cddb6fabf3b38f82463b7248683a22b7257930a +ci-compatible-selfhosted.zh.md: c93e8c2eb38bc7edfc35382b0a2dacb01d637d5c diff --git a/docs/ci-compatible-selfhosted.md b/docs/ci-compatible-selfhosted.md index b8f4efd314..5cddb6fabf 100644 --- a/docs/ci-compatible-selfhosted.md +++ b/docs/ci-compatible-selfhosted.md @@ -24,7 +24,7 @@ Each matrix entry runs one repository gate at a time. The matrix retains indepen ## Installation and cleanup -Self-hosted Node installations use a tool cache beneath `runner.temp`. pnpm setup uses its runner-and-run-private destination. Node compile caches and node-gyp headers also stay beneath runner temp; the pnpm content-addressed store remains persistent. Hosted jobs retain their normal tool cache and pnpm caching. Self-hosted jobs do not restore or upload hosted package caches. +Self-hosted Node installations use a tool cache beneath `runner.temp`. A setup-node-only ESM preload sets the path inside the action process because the Actions runner overwrites reserved `RUNNER_*` step variables. The following step rejects a Node executable outside that temporary installation; later compatibility processes do not inherit the preload. pnpm setup uses its runner-and-run-private destination. Node compile caches and node-gyp headers also stay beneath runner temp; the pnpm content-addressed store remains persistent. Hosted jobs retain their normal tool cache and pnpm caching. Self-hosted jobs do not restore or upload hosted package caches. The runner owns temporary-directory cleanup between jobs. These jobs do not install system packages or change global Node symlinks. The shared image must already provide the compiler and Python dependencies needed by native npm packages. A cold temporary Node cache requires downloading the selected runtime again. diff --git a/docs/ci-compatible-selfhosted.zh.md b/docs/ci-compatible-selfhosted.zh.md index 592f9d6b46..c93e8c2eb3 100644 --- a/docs/ci-compatible-selfhosted.zh.md +++ b/docs/ci-compatible-selfhosted.zh.md @@ -24,7 +24,7 @@ Node 22.19、24.9 和 26 仅在 `DSH_CI_FAILOVER_LINUX=selfhosted`,且 PR(Pu ## 安装与清理 -自托管 Node 安装使用 `runner.temp` 下的工具缓存。pnpm 设置使用运行器和运行私有的目标目录。Node 编译缓存和 node-gyp 头文件也保留在运行器临时目录下;pnpm 内容寻址 store 保持持久化。托管作业保留其常规工具缓存和 pnpm 缓存。自托管作业不恢复或上传托管软件包缓存。 +自托管 Node 安装使用 `runner.temp` 下的工具缓存。仅用于 setup-node 的 ESM 预加载模块在 action 进程内设置路径,因为 Actions 运行器会覆盖保留的 `RUNNER_*` 步骤变量。后续步骤拒绝位于该临时安装之外的 Node 可执行文件;之后的兼容性进程不继承预加载设置。pnpm 设置使用运行器和运行私有的目标目录。Node 编译缓存和 node-gyp 头文件也保留在运行器临时目录下;pnpm 内容寻址 store 保持持久化。托管作业保留其常规工具缓存和 pnpm 缓存。自托管作业不恢复或上传托管软件包缓存。 运行器负责作业之间的临时目录清理。这些作业不安装系统软件包,也不修改全局 Node 符号链接。共享镜像必须已提供原生 npm 软件包所需的编译器和 Python 依赖。冷的临时 Node 缓存需要重新下载所选运行时。 diff --git a/scripts/ci-compatible-selfhosted.spec.ts b/scripts/ci-compatible-selfhosted.spec.ts index 8c3ddd2165..2bde3889e1 100644 --- a/scripts/ci-compatible-selfhosted.spec.ts +++ b/scripts/ci-compatible-selfhosted.spec.ts @@ -34,7 +34,7 @@ const labels = ['self-hosted', 'linux', 'x64', 'vm-backup'] function evaluate(expression: string, context: Record): unknown { const body = expression.trim().slice(3, -2) return runInNewContext(body, { - ...context, fromJSON: JSON.parse, format: (template: string, value: string) => template.replace('{0}', value), + ...context, fromJSON: JSON.parse, }, { timeout: 1000 }) as unknown } @@ -81,21 +81,68 @@ describe('Node compatibility self-hosted routing', () => { it('isolates version installs and enables hosted package caching only on hosted runners', () => { const setup = job.steps.find(step => step.uses === 'actions/setup-node@v6')! expect(setup.env).toEqual({ - RUNNER_TOOL_CACHE: "${{ runner.environment == 'self-hosted' && format('{0}/node-compat-toolcache', runner.temp) || runner.tool_cache }}", + NODE_OPTIONS: "${{ runner.environment == 'self-hosted' && '--import=./scripts/ci-compatible-toolcache.mjs' || '' }}", }) expect(setup.with?.['node-version']).toBe('${{ matrix.node }}') expect(setup.with?.['package-manager-cache']).toBe(false) for (const [environment, cache] of [['github-hosted', 'pnpm'], ['self-hosted', '']]) { const context = { runner: { environment, temp: '/runner/temp', tool_cache: '/runner/toolcache' } } expect(evaluate(setup.with?.cache as string, context)).toBe(cache) - expect(evaluate(setup.env!.RUNNER_TOOL_CACHE!, context)).toBe( - environment === 'self-hosted' ? '/runner/temp/node-compat-toolcache' : '/runner/toolcache', + expect(evaluate(setup.env!.NODE_OPTIONS!, context)).toBe( + environment === 'self-hosted' ? '--import=./scripts/ci-compatible-toolcache.mjs' : '', ) } expect(job.steps[0]?.with).toEqual({ 'persist-credentials': false }) expect(job.steps.some(step => step.uses?.startsWith('actions/cache/'))).toBe(false) }) + it('overrides runner exports inside setup-node without affecting later Node processes', () => { + const setup = job.steps.find(step => step.uses === 'actions/setup-node@v6')! + const nodeOptions = evaluate(setup.env!.NODE_OPTIONS!, { runner: { environment: 'self-hosted' } }) as string + const root = mkdtempSync(join(tmpdir(), 'ci-compatible preload-')) + try { + const env = { PATH: process.env.PATH, RUNNER_TEMP: root, RUNNER_TOOL_CACHE: join(root, 'persistent') } + const probe = (options: Record) => { + const child = spawnSync(process.execPath, ['-p', 'process.env.RUNNER_TOOL_CACHE'], { + cwd: resolve(import.meta.dirname, '..'), env: options, encoding: 'utf8', timeout: 10_000, + }) + expect(child.error).toBeUndefined() + expect(child.signal).toBeNull() + return child + } + const setupChild = probe({ ...env, NODE_OPTIONS: nodeOptions }) + expect(setupChild.status, setupChild.stderr).toBe(0) + expect(setupChild.stdout.trim()).toBe(join(root, 'node-compat-toolcache')) + const normalChild = probe(env) + expect(normalChild.status, normalChild.stderr).toBe(0) + expect(normalChild.stdout.trim()).toBe(env.RUNNER_TOOL_CACHE) + const missingTemp = probe({ ...env, RUNNER_TEMP: undefined, NODE_OPTIONS: nodeOptions }) + expect(missingTemp.status).not.toBe(0) + expect(missingTemp.stderr).toContain('requires an absolute RUNNER_TEMP') + expect(job.env).not.toHaveProperty('NODE_OPTIONS') + expect(job.steps.filter(step => step.env?.NODE_OPTIONS)).toEqual([setup]) + } finally { + rmSync(root, { recursive: true, force: true }) + } + }) + + it.skipIf(process.platform === 'win32')('rejects a Node executable outside its runner temporary installation', () => { + const step = job.steps.find(candidate => candidate.name === 'Verify isolated Node installation')! + expect(step.if).toBe("runner.environment == 'self-hosted'") + for (const [executable, status] of [ + ['/runner temp/node-compat-toolcache/node/24.9.0/x64/bin/node', 0], + ['/shared/toolcache/node/24.9.0/x64/bin/node', 1], + ['/runner temp/node-compat-toolcache-other/node', 1], + ] as const) { + const child = spawnSync('bash', ['-e', '-u', '-o', 'pipefail', '-c', 'node() { printf "%s" "$TEST_EXECUTABLE"; }; ' + step.run!], { + env: { PATH: process.env.PATH, RUNNER_TEMP: '/runner temp', TEST_EXECUTABLE: executable }, encoding: 'utf8', timeout: 10_000, + }) + expect(child.error).toBeUndefined() + expect(child.signal).toBeNull() + expect(child.status, child.stderr).toBe(status) + } + }) + it.skipIf(process.platform === 'win32')('configures generated caches before pnpm without changing HOME or global links', () => { const index = job.steps.findIndex(step => step.name === 'Isolate compatibility caches') const step = job.steps[index]! diff --git a/scripts/ci-compatible-toolcache.mjs b/scripts/ci-compatible-toolcache.mjs new file mode 100644 index 0000000000..7da3accf64 --- /dev/null +++ b/scripts/ci-compatible-toolcache.mjs @@ -0,0 +1,8 @@ +import assert from 'node:assert/strict' +import { isAbsolute, join } from 'node:path' + +// The Actions runner exports RUNNER_TOOL_CACHE after step env. Run inside the +// setup-node process so version installs use runner temp rather than shared state. +const temp = process.env.RUNNER_TEMP +assert(temp && isAbsolute(temp), 'Node compatibility setup requires an absolute RUNNER_TEMP') +process.env.RUNNER_TOOL_CACHE = join(temp, 'node-compat-toolcache') From ac4fa3d6f5cdaaaaf6e9be92bcaac4f30bc152e4 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 6 Sep 2026 14:57:27 +0800 Subject: [PATCH 5/6] docs: clarify compatibility runner scope and verification --- .../2026-07-21-serial-cross-platform-ci-reference.i18n.yaml | 4 ++-- .../2026-07-21-serial-cross-platform-ci-reference.md | 2 +- .../2026-07-21-serial-cross-platform-ci-reference.zh.md | 2 +- .../process/2026-07-26-ci-failover-runbook.i18n.yaml | 4 ++-- .../implemented/process/2026-07-26-ci-failover-runbook.md | 6 +++--- .../process/2026-07-26-ci-failover-runbook.zh.md | 6 +++--- .../2026-09-06-node-compatibility-selfhosted.i18n.yaml | 4 ++-- .../process/2026-09-06-node-compatibility-selfhosted.md | 4 ++-- .../process/2026-09-06-node-compatibility-selfhosted.zh.md | 4 ++-- .github/workflows/ci.yml | 2 +- docs/ci-compatible-selfhosted.i18n.yaml | 2 +- docs/ci-compatible-selfhosted.zh.md | 2 +- scripts/ci-compatible-selfhosted.spec.ts | 4 ++-- 13 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml index 12c5d5d1cc..889010ca8d 100644 --- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md -2026-07-21-serial-cross-platform-ci-reference.md: fb44f1122eae8120615ab5963d40b8fa3fb6b3bb -2026-07-21-serial-cross-platform-ci-reference.zh.md: 9a316b13912347e2bfff0fbf866a8470c7070029 +2026-07-21-serial-cross-platform-ci-reference.md: edb81b643d0cef2e5bc807005a9016324b8430ab +2026-07-21-serial-cross-platform-ci-reference.zh.md: 41fd9c032038f2a312978acf995febfdab34aeaa diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md index fb44f1122e..edb81b643d 100644 --- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md +++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md @@ -16,7 +16,7 @@ Real-kernel sandbox proofs require specific hosted operating systems and archite ## Decision -[CI](../../../../.github/workflows/ci.yml) (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 isolated self-hosted routing](2026-09-06-node-compatibility-selfhosted.md), and Python checks; [platform scheduling](2026-09-06-master-only-platform-ci.md) 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) (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](2026-09-06-node-compatibility-selfhosted.md), and Python checks; [platform scheduling](2026-09-06-master-only-platform-ci.md) 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. diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md index 9a316b1391..41fd9c0320 100644 --- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md +++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md @@ -16,7 +16,7 @@ Status: implemented ## 决策 -[CI](../../../../.github/workflows/ci.yml)(仅 pull request)与 [CI master](../../../../.github/workflows/ci-master.yml)(master 推送 + `workflow_dispatch`)为拉取请求事件与 master 推送事件赋予互补的职责。拉取请求运行 Linux、原生 Windows、[使用隔离自托管路由的 Node 兼容性](2026-09-06-node-compatibility-selfhosted.zh.md)与 Python 检查;[平台调度](2026-09-06-master-only-platform-ci.zh.md)将 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)(仅 pull request)与 [CI master](../../../../.github/workflows/ci-master.yml)(master 推送 + `workflow_dispatch`)为拉取请求事件与 master 推送事件赋予互补的职责。拉取请求运行 Linux、原生 Windows、[仅在 Linux 变量与信任条件满足时使用自托管路由的 Node 兼容性](2026-09-06-node-compatibility-selfhosted.zh.md)与 Python 检查;[平台调度](2026-09-06-master-only-platform-ci.zh.md)将 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 则在安装采用符号链接的工作区前启用开发人员模式。 diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml index 012d82fc53..90fdf49b49 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md -2026-07-26-ci-failover-runbook.md: 68fcbf956410235bb245354477234fb921561d7c -2026-07-26-ci-failover-runbook.zh.md: 7f3cffe8b34250b32351cd890f7de1c129ca992b +2026-07-26-ci-failover-runbook.md: f24cb8b8239141cd1ccf468a566dba620dd3cfdd +2026-07-26-ci-failover-runbook.zh.md: 57c4a92a3af720d9b11b7a1ce7a1515b83c77339 diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md index 68fcbf9564..f24cb8b823 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md @@ -10,7 +10,7 @@ The three required Linux worker jobs in [CI](../../../../.github/workflows/ci.ym ## Decision -Each of the three required Linux worker jobs, the native Windows jobs, and the `all checks passed` verdict job — which would otherwise stay queued on the failed pool even after every worker passed — resolves its runner pool through a repository variable, and the switch is split by platform so an outage on one platform does not retarget the other. The three Linux workers and the `all checks passed` verdict (whose `needs` are the required Linux workers and which runs on the `vm-backup` pool) resolve through `DSH_CI_FAILOVER_LINUX`; the native Windows jobs resolve through `DSH_CI_FAILOVER_WINDOWS`. Unset, they default to their hosted pools; selecting `selfhosted` is an explicit operator choice. Set to `selfhosted` by any repository writer, the corresponding jobs retarget onto the in-house self-hosted pool: under `DSH_CI_FAILOVER_LINUX`, the Linux jobs and verdict move onto the `vm-backup` pool, snapshot concurrency drops to the shared-VM bound, and the hosted-path pnpm cache restores are skipped; under `DSH_CI_FAILOVER_WINDOWS`, the native Windows jobs move onto the `dsh-win-ci` pool. Each switch is writer-manageable repository state, not a merge, so it works while every check is red. The in-house pools' readiness is continuously re-proven by the `serial / linux (self-hosted standby)` and `serial / windows (self-hosted standby)` lanes, which run the complete unsharded aggregates on every master push. +The three primary Linux jobs (`node-24`, `node-24-coverage`, `node-24-consumers`), the three `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`; otherwise they retain their 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 `ubuntu-latest` fallback. 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. `ci-master.yml` exempts exactly one event from `cancel-in-progress` (`${{ github.event_name != 'push' }}`), so one master push does not cancel the drill still running from the previous one. Each drill runs its complete unsharded aggregate with one gate worker, which takes longer than the interval between master merges; under unconditional cancellation a drill is superseded before reaching a verdict and the lane yields no readiness evidence for a responder to check. @@ -44,7 +44,7 @@ The two switches are independent: flip only the one whose platform is degraded. ## Capacity during failover -Capacity includes the master standby, main-CI jobs, and three release-rehearsal jobs for each eligible PR or master push while the Linux switch is set. The release workflows do not cancel running rehearsals when another run arrives, so overlapping refs can add sustained build, pack, and install load. Check current CPU, memory, disk, and queue pressure before extending self-hosted operation; extra registrations on this VM add scheduling slots, not machine resources. Do not infer spare capacity from the standby alone. When host resources permit extra registrations, use an org registration token (org Settings → Actions → Runners → New runner). Clone an existing runner directory **excluding its identity files** — `rsync -a --exclude '.runner*' --exclude '.credentials*' --exclude '_diag' --exclude '_work' / /` (the globs also catch `.runner_migrated`/`.credentials_migrated`, which GitHub writes on migrated runners and which equally trigger the already-configured refusal) — then run `config.sh` (copying `.runner`/`.credentials` verbatim makes it refuse with "already configured"), and **start the listener**: `sudo ./svc.sh install ubuntu && sudo ./svc.sh start`. Registration alone leaves the runner offline; a started service adds a scheduling slot, not CPU or memory. +Capacity includes the master standby, main-CI jobs, and three release-rehearsal jobs for each eligible PR or master push while the Linux switch is set. Each trusted PR also adds three Node compatibility jobs at gate concurrency one, including the build-backed Node 22 leg and cold temporary runtime downloads. The release workflows do not cancel running rehearsals when another run arrives, so overlapping refs can add sustained build, pack, and install load. Check current CPU, memory, disk, and queue pressure before extending self-hosted operation; extra registrations on this VM add scheduling slots, not machine resources. Do not infer spare capacity from the standby alone. When host resources permit extra registrations, use an org registration token (org Settings → Actions → Runners → New runner). Clone an existing runner directory **excluding its identity files** — `rsync -a --exclude '.runner*' --exclude '.credentials*' --exclude '_diag' --exclude '_work' / /` (the globs also catch `.runner_migrated`/`.credentials_migrated`, which GitHub writes on migrated runners and which equally trigger the already-configured refusal) — then run `config.sh` (copying `.runner`/`.credentials` verbatim makes it refuse with "already configured"), and **start the listener**: `sudo ./svc.sh install ubuntu && sudo ./svc.sh start`. Registration alone leaves the runner offline; a started service adds a scheduling slot, not CPU or memory. ### Switch back @@ -53,7 +53,7 @@ Delete the `DSH_CI_FAILOVER_LINUX` or `DSH_CI_FAILOVER_WINDOWS` variable (or set ### Trust boundary -The variables are writer-manageable repository state; a pull request event itself can neither set them nor read a different value into effect, and the selector expressions live in workflow definitions. Note that under failover, `pull_request` runs execute the PR merge ref's own workflow definition — the boundary against untrusted code is repository membership (private, forking disabled, Dependabot excluded by the selectors), not the variable. Note on runner-group policy: pinning the runner group to the master-ref workflow is **incompatible** with this failover — the five failover jobs are `pull_request` runs evaluated from PR merge refs, and a master-pinned group leaves them queued (observed live on 2026-07-27; the group was widened to all workflows of this repository to unblock the switch). A stricter runner-side policy therefore costs PR failover; the shipped posture accepts repository-scoped, all-workflow group access. +The variables are writer-manageable repository state; a pull request event itself can neither set them nor read a different value into effect, and the selector expressions live in workflow definitions. Note that under failover, `pull_request` runs execute the PR merge ref's own workflow definition — the boundary against untrusted code is repository membership (private, forking disabled, Dependabot excluded by the selectors), not the variable. Note on runner-group policy: pinning the runner group to the master-ref workflow is **incompatible** with this failover — the failover jobs, including the Node compatibility matrix, are `pull_request` runs evaluated from PR merge refs, and a master-pinned group leaves them queued (observed live on 2026-07-27; the group was widened to all workflows of this repository to unblock the switch). A stricter runner-side policy therefore costs PR failover; the shipped posture accepts repository-scoped, all-workflow group access. ## Alternatives considered diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md index 7f3cffe8b3..57c4a92a3a 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md @@ -10,7 +10,7 @@ Status: implemented ## 决策 -三个必需的 Linux 工作作业、原生 Windows 作业,以及 `all checks passed` 判定作业(若不随切换,即使全部工作作业通过,它仍会滞留在故障池的队列中)——各自通过仓库变量解析运行器池,且开关按平台拆分,使一个平台的故障不会重定向另一个平台。三个 Linux 工作作业与 `all checks passed` 判定作业(其 `needs` 是必需的 Linux 工作作业,且运行在 `vm-backup` 池上)通过 `DSH_CI_FAILOVER_LINUX` 解析;原生 Windows 作业通过 `DSH_CI_FAILOVER_WINDOWS` 解析。未设置变量时默认使用各自的托管池;选择 `selfhosted` 是运维人员的明确操作;由任何具备写权限的协作者设为 `selfhosted` 时,对应作业切换到公司自有的自托管池:`DSH_CI_FAILOVER_LINUX` 下,Linux 作业与判定作业切到 `vm-backup` 池,快照并发降到共享虚拟机上限,并跳过托管路径的 pnpm 缓存恢复;`DSH_CI_FAILOVER_WINDOWS` 下,原生 Windows 作业切到 `dsh-win-ci` 池。每个开关都是写者可管理的仓库状态而非一次合并,因此在所有检查都是红色时仍然有效。自有池的就绪状态由 `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` 通过 `DSH_CI_FAILOVER_LINUX` 解析;原生 Windows 作业通过 `DSH_CI_FAILOVER_WINDOWS` 解析。一个平台的开关不会重定向另一个平台。仓库写者将变量设为 `selfhosted` 时,适用的可信作业选择 `vm-backup` 或 `dsh-win-ci`;否则保留工作流定义的托管回退。Node 兼容性作业要求同仓库且非 fork 的头部以及非 Dependabot 作者,使用隔离运行时设置,并保留 `ubuntu-latest` 回退。Linux 故障切换限制快照并发,并跳过托管软件包缓存恢复。判定作业跟随工作作业,避免继续在不可用的托管池排队。每个开关都是写者可管理的仓库状态而非一次合并,因此在检查失败时仍然有效。`serial / linux (self-hosted standby)` 与 `serial / windows (self-hosted standby)` 通道在 master 推送上重新验证完整的未分片聚合流程。 `ci-master.yml` 只豁免一个事件不做取消(`${{ github.event_name != 'push' }}`),因此一次 master 推送不会取消上一次推送留下的、仍在运行的演练。每次演练以单门禁工作进程执行完整的未分片聚合流程,耗时长于 master 合并的间隔;在无条件取消下,演练会在得出结论前被后续运行取代,该通道无法产出供响应者查看的就绪证据。 @@ -44,7 +44,7 @@ Status: implemented ## 切换期间的容量 -Linux 开关启用期间,容量需覆盖 master 热备、主 CI 作业,以及每个符合条件的 PR 或 master 推送的三个发布演练作业。发布工作流不会因为新运行到来而取消正在执行的演练,因此不同引用的重叠运行会增加持续的构建、打包和安装负载。延长自托管运行前,检查当前 CPU、内存、磁盘和队列压力;同一虚拟机上新增注册只增加调度槽位,不增加机器资源。不能只依据热备负载推断空闲容量。主机资源允许增加注册实例时,使用组织级注册 token(组织 Settings → Actions → Runners → New runner)。复制现有 runner 目录时**必须排除身份文件**——`rsync -a --exclude '.runner*' --exclude '.credentials*' --exclude '_diag' --exclude '_work' / /`(通配同时排除 `.runner_migrated`/`.credentials_migrated`——GitHub 会在迁移过的运行器上写入这些文件,它们同样会触发 already-configured 拒绝)——再跑 `config.sh`(原样拷贝 `.runner`/`.credentials` 会使其以 "already configured" 拒绝),然后**启动监听器**:`sudo ./svc.sh install ubuntu && sudo ./svc.sh start`。仅注册不会上线;启动服务增加的是调度槽位,而非 CPU 或内存。 +Linux 开关启用期间,容量需覆盖 master 热备、主 CI 作业,以及每个符合条件的 PR 或 master 推送的三个发布演练作业。每个可信 PR 还会增加三个门禁并发度为一的 Node 兼容性作业,包括需要构建的 Node 22 条目和冷临时运行时下载。发布工作流不会因为新运行到来而取消正在执行的演练,因此不同引用的重叠运行会增加持续的构建、打包和安装负载。延长自托管运行前,检查当前 CPU、内存、磁盘和队列压力;同一虚拟机上新增注册只增加调度槽位,不增加机器资源。不能只依据热备负载推断空闲容量。主机资源允许增加注册实例时,使用组织级注册 token(组织 Settings → Actions → Runners → New runner)。复制现有 runner 目录时**必须排除身份文件**——`rsync -a --exclude '.runner*' --exclude '.credentials*' --exclude '_diag' --exclude '_work' / /`(通配同时排除 `.runner_migrated`/`.credentials_migrated`——GitHub 会在迁移过的运行器上写入这些文件,它们同样会触发 already-configured 拒绝)——再跑 `config.sh`(原样拷贝 `.runner`/`.credentials` 会使其以 "already configured" 拒绝),然后**启动监听器**:`sudo ./svc.sh install ubuntu && sudo ./svc.sh start`。仅注册不会上线;启动服务增加的是调度槽位,而非 CPU 或内存。 ### 切回 @@ -53,7 +53,7 @@ Linux 开关启用期间,容量需覆盖 master 热备、主 CI 作业,以 ### 信任边界 -这些变量是写者可管理的仓库状态;`pull_request` 事件本身既不能设置它们,也不能让不同的值生效,选择器表达式存在于工作流定义中。需要注意:故障切换期间,`pull_request` 运行执行的是 PR merge 引用自带的工作流定义——抵御不可信代码的边界是仓库成员资格(私有、禁 fork、选择器排除 Dependabot),而非该变量。关于 runner group 策略的说明:把 runner group 绑定到 master 引用的工作流与本故障切换机制**不兼容**——五个故障切换作业是从 PR merge 引用求值的 `pull_request` 运行,master 绑定的组会让它们持续排队(2026-07-27 实际故障中亲历;当时将组放宽为本仓库全部工作流才疏通了切换)。更严格的运行器侧策略以牺牲 PR 故障切换为代价;当前采用的形态是仓库范围、全工作流的组访问。 +这些变量是写者可管理的仓库状态;`pull_request` 事件本身既不能设置它们,也不能让不同的值生效,选择器表达式存在于工作流定义中。需要注意:故障切换期间,`pull_request` 运行执行的是 PR merge 引用自带的工作流定义——抵御不可信代码的边界是仓库成员资格(私有、禁 fork、选择器排除 Dependabot),而非该变量。关于 runner group 策略的说明:把 runner group 绑定到 master 引用的工作流与本故障切换机制**不兼容**——包括 Node 兼容性矩阵在内的故障切换作业是从 PR merge 引用求值的 `pull_request` 运行,master 绑定的组会让它们持续排队(2026-07-27 实际故障中亲历;当时将组放宽为本仓库全部工作流才疏通了切换)。更严格的运行器侧策略以牺牲 PR 故障切换为代价;当前采用的形态是仓库范围、全工作流的组访问。 ## 曾考虑的替代方案 diff --git a/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.i18n.yaml b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.i18n.yaml index 32e9a52aa9..829d45266f 100644 --- a/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.i18n.yaml +++ b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md -2026-09-06-node-compatibility-selfhosted.md: 44015255a464e75b481ed41ac22f60421eed6059 -2026-09-06-node-compatibility-selfhosted.zh.md: 2794b47e02537341bae4d1fd2b434ebf3ba0ff3c +2026-09-06-node-compatibility-selfhosted.md: 6fc51b8082fa6aaeced2245f99fbc41cf993fc02 +2026-09-06-node-compatibility-selfhosted.zh.md: 3acd10c068ea5941d4eebbfdc80cc95ff6b44c8b diff --git a/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md index 44015255a4..6fc51b8082 100644 --- a/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md +++ b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md @@ -10,7 +10,7 @@ The Node 22.19, 24.9, and 26 compatibility jobs consume hosted Linux minutes eve ## Decision -[CI](../../../../.github/workflows/ci.yml) applies the Linux failover variable to these three jobs, requiring a non-Dependabot author and a non-fork head repository matching the current repository. The standard hosted fallback remains available. [Runner documentation](../../../../docs/ci-compatible-selfhosted.md) owns setup and cleanup behavior. +[CI](../../../../.github/workflows/ci.yml) applies the Linux failover variable to these three jobs, requiring a non-Dependabot author and a non-fork head repository matching the current repository. The standard hosted fallback remains available. These predicates constrain this job, not every workflow admitted to the pool. Both repository identity and fork status remain explicit to preserve its trust restriction if repository settings change; existing sibling selectors are outside this migration. [Runner documentation](../../../../docs/ci-compatible-selfhosted.md) owns setup and cleanup behavior. The temporary tool cache trades repeated Node downloads for isolation across concurrent runners and Node versions. A setup-node-only [ESM preload](../../../../scripts/ci-compatible-toolcache.mjs) assigns the cache inside the action process: the Actions runner overwrites reserved environment variables after reading step configuration. An executed path check rejects installations outside runner temp; compatibility processes do not inherit the preload. pnpm keeps its existing private setup destination and persistent content-addressed store. Compile caches and node-gyp headers use runner temp before the first pnpm invocation. No global Node symlink or system package changes are introduced. Hosted jobs retain their tool and package caching. @@ -32,4 +32,4 @@ The pool receives three additional jobs per trusted PR; each retains gate concur The focused [workflow regression](../../../../scripts/ci-compatible-selfhosted.spec.ts) executes the actual routing expressions and environment setup. A negative control removing the fork condition fails the hosted-fallback assertion. It checks Dependabot reruns by a maintainer, repository mismatch, fork flags, disabled variables, and runner-scoped cache paths. -[Successful standby run 33984559660](https://github.com/deepseek-harness/deepseek-harness/actions/runs/33984559660) at the implementation base supplies Linux Node 24.19.0 and Windows Node 24.20.0 baseline evidence. Linux job 101359402557 uses runner-specific temporary and tool directories on the data volume. [Read-only capability probe 34012679056](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34012679056/job/101431064925) reports Linux x64, 192 online logical CPUs, GCC/G++ 13.3, Make 4.3, and Python 3.12.3. Python 3.10 is absent, reinforcing the separate SDK provisioning requirement. That baseline does not prove these three exact Node versions on self-hosted Linux; their PR matrix execution is the platform verification owner. +[Successful standby run 33984559660](https://github.com/deepseek-harness/deepseek-harness/actions/runs/33984559660) at the implementation base supplies Linux Node 24.19.0 and Windows Node 24.20.0 baseline evidence. Linux job 101359402557 uses runner-specific temporary and tool directories on the data volume. [Read-only capability probe 34012679056](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34012679056/job/101431064925) reports Linux x64, 192 online logical CPUs, GCC/G++ 13.3, Make 4.3, and Python 3.12.3. Python 3.10 is absent, reinforcing the separate SDK provisioning requirement. [PR run 34013779750](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34013779750) at `282519d2` verifies Node 22.19.0, 24.9.0, and 26.8.1 on self-hosted Linux, including setup, executable-path checks, compatibility tests, and post actions. The executables reside under each runner’s `_temp/node-compat-toolcache/node//x64/bin`; the completed jobs take 228s, 94s, and 101s respectively. These observations establish version and path compatibility, not an exclusive-host capacity guarantee. diff --git a/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.zh.md b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.zh.md index 2794b47e02..3acd10c068 100644 --- a/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.zh.md +++ b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.zh.md @@ -10,7 +10,7 @@ Status: implemented ## 决策 -[CI](../../../../.github/workflows/ci.yml) 将 Linux 故障切换变量应用于这三个作业,要求作者不是 Dependabot,且非 fork 的头部仓库与当前仓库相同。标准托管回退仍然可用。[运行器文档](../../../../docs/ci-compatible-selfhosted.zh.md) 拥有安装与清理行为的说明。 +[CI](../../../../.github/workflows/ci.yml) 将 Linux 故障切换变量应用于这三个作业,要求作者不是 Dependabot,且非 fork 的头部仓库与当前仓库相同。标准托管回退仍然可用。这些条件约束本作业,而非所有可进入该池的工作流。仓库身份和 fork 状态均显式保留,以便在仓库设置改变时保持本作业的信任限制;现有兄弟选择器不属于本次迁移范围。[运行器文档](../../../../docs/ci-compatible-selfhosted.zh.md) 拥有安装与清理行为的说明。 临时工具缓存以重复下载 Node 为代价,换取并发运行器与 Node 版本之间的隔离。仅用于 setup-node 的 [ESM 预加载模块](../../../../scripts/ci-compatible-toolcache.mjs) 在 action 进程内指定缓存:Actions 运行器在读取步骤配置后会覆盖保留的环境变量。实际执行的路径检查拒绝运行器临时目录之外的安装;兼容性进程不继承预加载设置。pnpm 保留现有的私有安装目录和持久化内容寻址 store。编译缓存与 node-gyp 头文件在首次调用 pnpm 前就使用运行器临时目录。不引入全局 Node 符号链接或系统软件包变更。托管作业保留其工具与软件包缓存。 @@ -32,4 +32,4 @@ Status: implemented 聚焦的[工作流回归测试](../../../../scripts/ci-compatible-selfhosted.spec.ts) 执行真实的路由表达式和环境设置。移除 fork 条件的负对照使托管回退断言失败。它检查维护者重跑 Dependabot PR、仓库不匹配、fork 标志、禁用变量以及运行器范围内的缓存路径。 -实施基线上的[成功热备运行 33984559660](https://github.com/deepseek-harness/deepseek-harness/actions/runs/33984559660) 提供 Linux Node 24.19.0 和 Windows Node 24.20.0 基线证据。Linux 作业 101359402557 使用数据卷上运行器专属的临时目录和工具目录。[只读能力探测 34012679056](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34012679056/job/101431064925) 报告 Linux x64、192 个在线逻辑 CPU、GCC/G++ 13.3、Make 4.3 和 Python 3.12.3。Python 3.10 缺失,进一步说明 SDK 需要单独配置。该基线不能证明自托管 Linux 上这三个精确 Node 版本的行为;其 PR 矩阵执行拥有平台验证责任。 +实施基线上的[成功热备运行 33984559660](https://github.com/deepseek-harness/deepseek-harness/actions/runs/33984559660) 提供 Linux Node 24.19.0 和 Windows Node 24.20.0 基线证据。Linux 作业 101359402557 使用数据卷上运行器专属的临时目录和工具目录。[只读能力探测 34012679056](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34012679056/job/101431064925) 报告 Linux x64、192 个在线逻辑 CPU、GCC/G++ 13.3、Make 4.3 和 Python 3.12.3。Python 3.10 缺失,进一步说明 SDK 需要单独配置。`282519d2` 上的 [PR 运行 34013779750](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34013779750) 验证了自托管 Linux 上的 Node 22.19.0、24.9.0 和 26.8.1,包括设置、可执行文件路径检查、兼容性测试和 post actions。可执行文件位于各运行器的 `_temp/node-compat-toolcache/node//x64/bin` 下;完成的作业分别耗时 228s、94s 和 101s。这些观测证明版本与路径兼容性,而非独占主机的容量保证。 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 944e36bdb1..9b09e46671 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -291,7 +291,7 @@ jobs: node-compat: if: github.event_name == 'pull_request' - # Only repository-owned PR code may reach the persistent shared VM. + # This job admits only repository-owned PR code to the persistent shared VM. runs-on: >- ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.head.repo.full_name == github.repository diff --git a/docs/ci-compatible-selfhosted.i18n.yaml b/docs/ci-compatible-selfhosted.i18n.yaml index e29c41b681..0b80282ee1 100644 --- a/docs/ci-compatible-selfhosted.i18n.yaml +++ b/docs/ci-compatible-selfhosted.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/ci-compatible-selfhosted.md ci-compatible-selfhosted.md: 5cddb6fabf3b38f82463b7248683a22b7257930a -ci-compatible-selfhosted.zh.md: c93e8c2eb38bc7edfc35382b0a2dacb01d637d5c +ci-compatible-selfhosted.zh.md: ebd8c4aa9bd03e650e1b7423331a17d93e7bb0a4 diff --git a/docs/ci-compatible-selfhosted.zh.md b/docs/ci-compatible-selfhosted.zh.md index c93e8c2eb3..ebd8c4aa9b 100644 --- a/docs/ci-compatible-selfhosted.zh.md +++ b/docs/ci-compatible-selfhosted.zh.md @@ -2,7 +2,7 @@ [English](ci-compatible-selfhosted.md) | 中文 -## 摘要 +## 概述 三个 Node 兼容性作业可以使用现有的 Linux 自托管池,而不改变其版本、必需检查或 master 调度。[CI](../.github/workflows/ci.yml) 拥有运行器选择逻辑;[决策记录](../.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.zh.md) 解释隔离和取舍。 diff --git a/scripts/ci-compatible-selfhosted.spec.ts b/scripts/ci-compatible-selfhosted.spec.ts index 2bde3889e1..3ef7a8b813 100644 --- a/scripts/ci-compatible-selfhosted.spec.ts +++ b/scripts/ci-compatible-selfhosted.spec.ts @@ -29,8 +29,8 @@ const workflow = yaml.load(readFileSync(resolve(import.meta.dirname, '../.github const job = workflow.jobs['node-compat'] const labels = ['self-hosted', 'linux', 'x64', 'vm-backup'] -// These expressions use only boolean operators and equal-typed string/boolean -// comparisons, whose results agree in Actions and JavaScript. +// This wiring check uses equal-typed, canonical-case fixtures. Actions compares +// strings case-insensitively; JavaScript does not. This is not an Actions evaluator. function evaluate(expression: string, context: Record): unknown { const body = expression.trim().slice(3, -2) return runInNewContext(body, { From a7ea2d74de85f61a70ac050ab77aead7dfb2797f Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:37:26 +0800 Subject: [PATCH 6/6] docs: remove redundant compatibility runner guide --- ...06-node-compatibility-selfhosted.i18n.yaml | 4 +- ...026-09-06-node-compatibility-selfhosted.md | 4 +- ...-09-06-node-compatibility-selfhosted.zh.md | 4 +- docs/ci-compatible-selfhosted.i18n.yaml | 6 --- docs/ci-compatible-selfhosted.md | 39 ------------------- docs/ci-compatible-selfhosted.zh.md | 39 ------------------- 6 files changed, 6 insertions(+), 90 deletions(-) delete mode 100644 docs/ci-compatible-selfhosted.i18n.yaml delete mode 100644 docs/ci-compatible-selfhosted.md delete mode 100644 docs/ci-compatible-selfhosted.zh.md diff --git a/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.i18n.yaml b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.i18n.yaml index 829d45266f..1cde51d814 100644 --- a/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.i18n.yaml +++ b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md -2026-09-06-node-compatibility-selfhosted.md: 6fc51b8082fa6aaeced2245f99fbc41cf993fc02 -2026-09-06-node-compatibility-selfhosted.zh.md: 3acd10c068ea5941d4eebbfdc80cc95ff6b44c8b +2026-09-06-node-compatibility-selfhosted.md: c78092834123b837d100814be9beba52c1a41397 +2026-09-06-node-compatibility-selfhosted.zh.md: 6dcff8aa197c0995e4e90d2d56179340a41bc783 diff --git a/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md index 6fc51b8082..c780928341 100644 --- a/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md +++ b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md @@ -10,9 +10,9 @@ The Node 22.19, 24.9, and 26 compatibility jobs consume hosted Linux minutes eve ## Decision -[CI](../../../../.github/workflows/ci.yml) applies the Linux failover variable to these three jobs, requiring a non-Dependabot author and a non-fork head repository matching the current repository. The standard hosted fallback remains available. These predicates constrain this job, not every workflow admitted to the pool. Both repository identity and fork status remain explicit to preserve its trust restriction if repository settings change; existing sibling selectors are outside this migration. [Runner documentation](../../../../docs/ci-compatible-selfhosted.md) owns setup and cleanup behavior. +[CI](../../../../.github/workflows/ci.yml) applies the Linux failover variable to these three jobs, requiring a non-Dependabot author and a non-fork head repository matching the current repository. The standard hosted fallback remains available. These predicates constrain this job, not every workflow admitted to the pool. Both repository identity and fork status remain explicit to preserve its trust restriction if repository settings change; existing sibling selectors are outside this migration. -The temporary tool cache trades repeated Node downloads for isolation across concurrent runners and Node versions. A setup-node-only [ESM preload](../../../../scripts/ci-compatible-toolcache.mjs) assigns the cache inside the action process: the Actions runner overwrites reserved environment variables after reading step configuration. An executed path check rejects installations outside runner temp; compatibility processes do not inherit the preload. pnpm keeps its existing private setup destination and persistent content-addressed store. Compile caches and node-gyp headers use runner temp before the first pnpm invocation. No global Node symlink or system package changes are introduced. Hosted jobs retain their tool and package caching. +The temporary tool cache trades repeated Node downloads for isolation across concurrent runners and Node versions. A setup-node-only [ESM preload](../../../../scripts/ci-compatible-toolcache.mjs) assigns the cache inside the action process: the Actions runner overwrites reserved environment variables after reading step configuration. An executed path check rejects installations outside runner temp; compatibility processes do not inherit the preload. pnpm keeps its existing private setup destination and persistent content-addressed store. Compile caches and node-gyp headers use runner temp before the first pnpm invocation. No global Node symlink or system package changes are introduced. Hosted jobs retain their tool and package caching; self-hosted jobs do not restore or upload hosted package caches. The runner owns temporary-directory cleanup between jobs, and the shared image supplies native npm packages’ compiler and Python prerequisites. The [failover runbook](2026-07-26-ci-failover-runbook.md) remains the owner of repository trust and pool switching. The [serial reference decision](2026-07-21-serial-cross-platform-ci-reference.md) remains the owner of master scheduling. Neither decision is superseded beyond the compatibility jobs' runner selection; both remain active. diff --git a/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.zh.md b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.zh.md index 3acd10c068..6dcff8aa19 100644 --- a/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.zh.md +++ b/.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.zh.md @@ -10,9 +10,9 @@ Status: implemented ## 决策 -[CI](../../../../.github/workflows/ci.yml) 将 Linux 故障切换变量应用于这三个作业,要求作者不是 Dependabot,且非 fork 的头部仓库与当前仓库相同。标准托管回退仍然可用。这些条件约束本作业,而非所有可进入该池的工作流。仓库身份和 fork 状态均显式保留,以便在仓库设置改变时保持本作业的信任限制;现有兄弟选择器不属于本次迁移范围。[运行器文档](../../../../docs/ci-compatible-selfhosted.zh.md) 拥有安装与清理行为的说明。 +[CI](../../../../.github/workflows/ci.yml) 将 Linux 故障切换变量应用于这三个作业,要求作者不是 Dependabot,且非 fork 的头部仓库与当前仓库相同。标准托管回退仍然可用。这些条件约束本作业,而非所有可进入该池的工作流。仓库身份和 fork 状态均显式保留,以便在仓库设置改变时保持本作业的信任限制;现有兄弟选择器不属于本次迁移范围。 -临时工具缓存以重复下载 Node 为代价,换取并发运行器与 Node 版本之间的隔离。仅用于 setup-node 的 [ESM 预加载模块](../../../../scripts/ci-compatible-toolcache.mjs) 在 action 进程内指定缓存:Actions 运行器在读取步骤配置后会覆盖保留的环境变量。实际执行的路径检查拒绝运行器临时目录之外的安装;兼容性进程不继承预加载设置。pnpm 保留现有的私有安装目录和持久化内容寻址 store。编译缓存与 node-gyp 头文件在首次调用 pnpm 前就使用运行器临时目录。不引入全局 Node 符号链接或系统软件包变更。托管作业保留其工具与软件包缓存。 +临时工具缓存以重复下载 Node 为代价,换取并发运行器与 Node 版本之间的隔离。仅用于 setup-node 的 [ESM 预加载模块](../../../../scripts/ci-compatible-toolcache.mjs) 在 action 进程内指定缓存:Actions 运行器在读取步骤配置后会覆盖保留的环境变量。实际执行的路径检查拒绝运行器临时目录之外的安装;兼容性进程不继承预加载设置。pnpm 保留现有的私有安装目录和持久化内容寻址 store。编译缓存与 node-gyp 头文件在首次调用 pnpm 前就使用运行器临时目录。不引入全局 Node 符号链接或系统软件包变更。托管作业保留其工具与软件包缓存;自托管作业不恢复或上传托管软件包缓存。运行器负责作业之间的临时目录清理,共享镜像提供原生 npm 软件包所需的编译器和 Python 前置依赖。 [故障切换手册](2026-07-26-ci-failover-runbook.zh.md) 仍拥有仓库信任与池切换规则。[串行参考决策](2026-07-21-serial-cross-platform-ci-reference.zh.md) 仍拥有 master 调度规则。除兼容性作业的运行器选择外,这两个决策都未被取代;两者均保持活跃。 diff --git a/docs/ci-compatible-selfhosted.i18n.yaml b/docs/ci-compatible-selfhosted.i18n.yaml deleted file mode 100644 index 0b80282ee1..0000000000 --- a/docs/ci-compatible-selfhosted.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# side as of the last confirmed-consistent state. Both languages carry equal authority; -# after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write docs/ci-compatible-selfhosted.md -ci-compatible-selfhosted.md: 5cddb6fabf3b38f82463b7248683a22b7257930a -ci-compatible-selfhosted.zh.md: ebd8c4aa9bd03e650e1b7423331a17d93e7bb0a4 diff --git a/docs/ci-compatible-selfhosted.md b/docs/ci-compatible-selfhosted.md deleted file mode 100644 index 5cddb6fabf..0000000000 --- a/docs/ci-compatible-selfhosted.md +++ /dev/null @@ -1,39 +0,0 @@ -# Node compatibility CI runners - -English | [中文](ci-compatible-selfhosted.zh.md) - -## Summary - -The three Node compatibility jobs can use the existing Linux self-hosted pool without changing their versions, required checks, or master scheduling. [CI](../.github/workflows/ci.yml) owns the runner selection; the [decision record](../.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md) explains isolation and trade-offs. - -## Table of Contents - -- [Runner selection](#runner-selection) -- [Installation and cleanup](#installation-and-cleanup) -- [Verification](#verification) - - - -## Runner selection - -Node 22.19, 24.9, and 26 select `[self-hosted, linux, x64, vm-backup]` only when `DSH_CI_FAILOVER_LINUX=selfhosted` and the PR author is not Dependabot, the head repository matches the current repository, and the head repository is not a fork. All other cases select `ubuntu-latest`. The Python SDK job remains hosted. - -Each matrix entry runs one repository gate at a time. The matrix retains independent jobs and does not cancel sibling versions on failure. Runner registrations share host resources; their count is not a count of independent machines. - - - -## Installation and cleanup - -Self-hosted Node installations use a tool cache beneath `runner.temp`. A setup-node-only ESM preload sets the path inside the action process because the Actions runner overwrites reserved `RUNNER_*` step variables. The following step rejects a Node executable outside that temporary installation; later compatibility processes do not inherit the preload. pnpm setup uses its runner-and-run-private destination. Node compile caches and node-gyp headers also stay beneath runner temp; the pnpm content-addressed store remains persistent. Hosted jobs retain their normal tool cache and pnpm caching. Self-hosted jobs do not restore or upload hosted package caches. - -The runner owns temporary-directory cleanup between jobs. These jobs do not install system packages or change global Node symlinks. The shared image must already provide the compiler and Python dependencies needed by native npm packages. A cold temporary Node cache requires downloading the selected runtime again. - - - -## Verification - -`pnpm exec vitest run scripts/ci-compatible-selfhosted.spec.ts scripts/ci-workflow.spec.ts` checks routing, hosted fallback, matrix preservation, cache paths, and the executed environment setup. The actual Node matrix on the self-hosted host remains the required platform verification; local workflow tests do not prove native runtime compatibility or capacity under concurrent PR load. - -## Dev Note - -None. diff --git a/docs/ci-compatible-selfhosted.zh.md b/docs/ci-compatible-selfhosted.zh.md deleted file mode 100644 index ebd8c4aa9b..0000000000 --- a/docs/ci-compatible-selfhosted.zh.md +++ /dev/null @@ -1,39 +0,0 @@ -# Node 兼容性 CI 运行器 - -[English](ci-compatible-selfhosted.md) | 中文 - -## 概述 - -三个 Node 兼容性作业可以使用现有的 Linux 自托管池,而不改变其版本、必需检查或 master 调度。[CI](../.github/workflows/ci.yml) 拥有运行器选择逻辑;[决策记录](../.agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.zh.md) 解释隔离和取舍。 - -## 目录 - -- [运行器选择](#runner-selection) -- [安装与清理](#installation-and-cleanup) -- [验证](#verification) - - - -## 运行器选择 - -Node 22.19、24.9 和 26 仅在 `DSH_CI_FAILOVER_LINUX=selfhosted`,且 PR(Pull Request)作者不是 Dependabot、头部仓库与当前仓库相同、头部仓库不是 fork 时选择 `[self-hosted, linux, x64, vm-backup]`。其余情况均选择 `ubuntu-latest`。Python SDK 作业仍使用托管运行器。 - -每个矩阵条目一次运行一个仓库门禁。矩阵保留独立作业,不会因某个版本失败而取消其他版本。运行器注册实例共享主机资源;注册数量不等于独立机器数量。 - - - -## 安装与清理 - -自托管 Node 安装使用 `runner.temp` 下的工具缓存。仅用于 setup-node 的 ESM 预加载模块在 action 进程内设置路径,因为 Actions 运行器会覆盖保留的 `RUNNER_*` 步骤变量。后续步骤拒绝位于该临时安装之外的 Node 可执行文件;之后的兼容性进程不继承预加载设置。pnpm 设置使用运行器和运行私有的目标目录。Node 编译缓存和 node-gyp 头文件也保留在运行器临时目录下;pnpm 内容寻址 store 保持持久化。托管作业保留其常规工具缓存和 pnpm 缓存。自托管作业不恢复或上传托管软件包缓存。 - -运行器负责作业之间的临时目录清理。这些作业不安装系统软件包,也不修改全局 Node 符号链接。共享镜像必须已提供原生 npm 软件包所需的编译器和 Python 依赖。冷的临时 Node 缓存需要重新下载所选运行时。 - - - -## 验证 - -`pnpm exec vitest run scripts/ci-compatible-selfhosted.spec.ts scripts/ci-workflow.spec.ts` 检查路由、托管回退、矩阵保留、缓存路径和实际执行的环境设置。自托管主机上的真实 Node 矩阵仍是必需的平台验证;本地工作流测试不能证明原生运行时兼容性或并发 PR 负载下的容量。 - -## 开发备注 - -无。