ci: run the pull-request Windows blocking gates under Wine on hosted Linux
The required windows job moves from windows-2025 to ubuntu-latest, running checksum-verified Windows Node under Wine at Linux-job wall clock (2m46s warm vs 7-9min); master's serial-windows native-kernel reference is untouched, and a new master-only wine-apt-cache job seeds the apt cache every pull request restores. The experiment workflow folds into ci.yml, the Agent Note moves to implemented with measured results, and the two CI topology notes update to the shipped facts.
This commit is contained in:
+3
-3
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-21-serial-cross-platform-ci-reference.md: 5433d2c51831ce61d06a16ee0b0ed982911f9218
|
||||
2026-07-21-serial-cross-platform-ci-reference.zh.md: 041d53d13e14354c995e4b65defce94a97646b0a
|
||||
# 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: 5eac1bc1c47c7309942b5615bc98a7fed893f346
|
||||
2026-07-21-serial-cross-platform-ci-reference.zh.md: 35fb761023fe7be081bf7d9591a53ed98b6e3abc
|
||||
@@ -24,7 +24,7 @@ The macOS reference runs the ordinary Vitest project in forked processes. Node 2
|
||||
|
||||
Master reference jobs are diagnostic and do not participate in the pull request's required `all checks passed` result. A pull request runs only its required jobs; a master push runs only the three serial references. Performance is evaluated from completed hosted-job timestamps and reported as a measurement; it is not encoded as a `timeout-minutes` value.
|
||||
|
||||
The portable reference uses GitHub's standard `ubuntu-latest`, `macos-latest`, and `windows-2025` labels. Required pull-request jobs use the same portable Linux and Windows capacity under the [required-CI decision](2026-07-23-portable-required-pull-request-ci.md). Higher-core hosted runners remain manual benchmarks because a correctness path must remain runnable without repository-external runner configuration.
|
||||
The portable reference uses GitHub's standard `ubuntu-latest`, `macos-latest`, and `windows-2025` labels; `serial / windows` is the one remaining native-Windows job, the complete-kernel oracle behind the Wine-hosted pull-request lane ([Wine lane decision](2026-07-27-wine-windows-gates-experiment.md)). Required pull-request jobs use portable standard capacity under the [required-CI decision](2026-07-23-portable-required-pull-request-ci.md). Higher-core hosted runners remain manual benchmarks because a correctness path must remain runnable without repository-external runner configuration.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ macOS 参考流程使用 fork 进程运行常规 Vitest 项目。macOS arm64 上
|
||||
|
||||
master 分支的参考作业仅用于诊断,不参与拉取请求所要求的 `all checks passed` 结果。拉取请求只运行其必需作业;向 master 推送时只运行三个串行参考作业。系统根据已完成托管作业的时间戳评估性能,并将其报告为测量结果,而不是写成 `timeout-minutes` 值。
|
||||
|
||||
可移植的参考流程使用 GitHub 标准的 `ubuntu-latest`、`macos-latest` 和 `windows-2025` 标签。依据[必需 CI 决策](2026-07-23-portable-required-pull-request-ci.md),拉取请求必需作业使用相同的可移植 Linux 和 Windows 容量。更高核心数的托管运行器仍仅用于手动基准测试,因为正确性路径必须无需仓库外部的运行器配置即可运行。
|
||||
可移植的参考流程使用 GitHub 标准的 `ubuntu-latest`、`macos-latest` 和 `windows-2025` 标签;`serial / windows` 是仅存的原生 Windows 作业,是 Wine 托管拉取请求通道背后的完整内核标尺([Wine 通道决策](2026-07-27-wine-windows-gates-experiment.md))。依据[必需 CI 决策](2026-07-23-portable-required-pull-request-ci.md),拉取请求必需作业使用可移植的标准容量。更高核心数的托管运行器仍仅用于手动基准测试,因为正确性路径必须无需仓库外部的运行器配置即可运行。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-23-portable-required-pull-request-ci.md: d1002c7d9db7cd8bbed3bdfda8a773a4b124bf16
|
||||
2026-07-23-portable-required-pull-request-ci.zh.md: fedfc6b9c982ace5ece430c52db23c22ec5119d4
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md
|
||||
2026-07-23-portable-required-pull-request-ci.md: 1a6939e8386e381cba114a7be71993a644457a45
|
||||
2026-07-23-portable-required-pull-request-ci.zh.md: cf0af769f9e740a2c9285caf4be05023371578d9
|
||||
@@ -12,9 +12,9 @@ Billing health, a runner definition's `Ready` state, and a large autoscaling cei
|
||||
|
||||
## Decision
|
||||
|
||||
[CI](../../../../.github/workflows/ci.yml) runs the required primary Node 24 jobs, plus the stable `all checks passed` aggregate, on repo-restricted enterprise 32-core pools. The aggregate performs no checkout or repository gate, but sharing the enterprise pool prevents the required verdict from introducing a separate standard-hosted billing dependency after its substantive jobs have already succeeded. The required Windows job runs on standard `windows-2025` with single-worker bounds, keeping the complete Windows contract independent of enterprise Windows allocation. Standard `ubuntu-latest` jobs retain Node 22.19, Node 26, and Python SDK compatibility, and `master` runs complete serial Linux, macOS, and Windows references. Those standard-hosted jobs keep the portable execution boundary observable without duplicating the primary inventory on every pull request.
|
||||
[CI](../../../../.github/workflows/ci.yml) runs the required primary Node 24 jobs, plus the stable `all checks passed` aggregate, on repo-restricted enterprise 32-core pools. The aggregate performs no checkout or repository gate, but sharing the enterprise pool prevents the required verdict from introducing a separate standard-hosted billing dependency after its substantive jobs have already succeeded. The required Windows job runs Windows Node under Wine on standard `ubuntu-latest` for the blocking surfaces ([Wine lane decision](2026-07-27-wine-windows-gates-experiment.md)), keeping the pull-request Windows contract independent of any Windows runner allocation; the complete native-kernel Windows inventory lives in the master serial reference. Standard `ubuntu-latest` jobs retain Node 22.19, Node 26, and Python SDK compatibility, and `master` runs complete serial Linux, macOS, and Windows references. Those standard-hosted jobs keep the portable execution boundary observable without duplicating the primary inventory on every pull request.
|
||||
|
||||
The two Linux primary jobs, Node compatibility, Python SDK, and `windows node 24 / complete` remain dependencies of `all checks passed`; branch protection continues to require `e2e` and `all checks passed`. There is no automatic fallback when a remaining enterprise Linux label cannot allocate: the standard jobs continue to report their own contracts, but they cannot manufacture the missing required result.
|
||||
The two Linux primary jobs, Node compatibility, Python SDK, and `windows node 24 / wine blocking` remain dependencies of `all checks passed`; branch protection continues to require `e2e` and `all checks passed`. There is no automatic fallback when a remaining enterprise Linux label cannot allocate: the standard jobs continue to report their own contracts, but they cannot manufacture the missing required result.
|
||||
|
||||
The [larger-runner decision](2026-07-22-evidence-based-larger-hosted-runners.md) owns the current primary topology and its measurements. The [serial cross-platform reference](2026-07-21-serial-cross-platform-ci-reference.md) remains the independent standard-hosted completeness check, and the manual larger-runner suites retain size comparisons without expanding the ordinary required matrix.
|
||||
|
||||
@@ -30,6 +30,6 @@ The [larger-runner decision](2026-07-22-evidence-based-larger-hosted-runners.md)
|
||||
|
||||
## Consequences
|
||||
|
||||
Ordinary pull requests spend enterprise capacity on the Linux critical path while standard Windows trades longer runtime for independent allocation. A live exact-head run proves the same commands that branch protection consumes; queue delay is reported separately from each job's `startedAt` to `completedAt` execution interval.
|
||||
Ordinary pull requests spend enterprise capacity on the Linux critical path while the Wine-hosted Windows job keeps its verdict on standard Linux allocation. A live exact-head run proves the same commands that branch protection consumes; queue delay is reported separately from each job's `startedAt` to `completedAt` execution interval.
|
||||
|
||||
Standard compatibility and required Windows jobs remain useful when enterprise allocation is degraded, but they do not make a blocked required Linux job or aggregate green. Recovering Linux availability may require restoring the complete standard-hosted topology; changing a pool definition's status alone is insufficient evidence that it can receive work.
|
||||
+3
-3
@@ -12,9 +12,9 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
[CI](../../../../.github/workflows/ci.yml) 在仅限本仓库使用的企业级 32 核运行器池上运行必需的主 Node 24 作业,以及稳定的 `all checks passed` 聚合流程。该聚合流程不执行代码检出或仓库门禁;但让它与所依赖的实质性作业共用企业级运行器池,可以避免这些作业已经成功后,必需判定结果又引入一项单独的标准托管计费依赖。必需的 Windows 作业在标准 `windows-2025` 上运行,并采用单工作线程上限,使完整的 Windows 契约不依赖企业级 Windows 运行器分配。标准 `ubuntu-latest` 作业保留 Node 22.19、Node 26 和 Python SDK 兼容性,`master` 则运行完整的 Linux、macOS 和 Windows 串行参考流程。这些标准托管作业让可移植执行边界保持可观测,而不必在每个拉取请求中重复主清单。
|
||||
[CI](../../../../.github/workflows/ci.yml) 在仅限本仓库使用的企业级 32 核运行器池上运行必需的主 Node 24 作业,以及稳定的 `all checks passed` 聚合流程。该聚合流程不执行代码检出或仓库门禁;但让它与所依赖的实质性作业共用企业级运行器池,可以避免这些作业已经成功后,必需判定结果又引入一项单独的标准托管计费依赖。必需的 Windows 作业在标准 `ubuntu-latest` 上通过 Wine 运行 Windows Node 以覆盖阻断表面([Wine 通道决策](2026-07-27-wine-windows-gates-experiment.md)),使拉取请求的 Windows 契约不依赖任何 Windows 运行器分配;完整的原生内核 Windows 清单归 master 串行参考流程所有。标准 `ubuntu-latest` 作业保留 Node 22.19、Node 26 和 Python SDK 兼容性,`master` 则运行完整的 Linux、macOS 和 Windows 串行参考流程。这些标准托管作业让可移植执行边界保持可观测,而不必在每个拉取请求中重复主清单。
|
||||
|
||||
两项 Linux 主作业、Node 兼容性、Python SDK 和 `windows node 24 / complete` 继续作为 `all checks passed` 的依赖项;分支保护继续要求 `e2e` 和 `all checks passed`。剩余的企业级 Linux 运行器标签无法分配运行器时没有自动后备机制:标准作业会继续报告各自的契约,但无法产出缺失的必需结果。
|
||||
两项 Linux 主作业、Node 兼容性、Python SDK 和 `windows node 24 / wine blocking` 继续作为 `all checks passed` 的依赖项;分支保护继续要求 `e2e` 和 `all checks passed`。剩余的企业级 Linux 运行器标签无法分配运行器时没有自动后备机制:标准作业会继续报告各自的契约,但无法产出缺失的必需结果。
|
||||
|
||||
当前主拓扑及其测量结果由[大型运行器决策](2026-07-22-evidence-based-larger-hosted-runners.md)记录。[跨平台串行参考流程](2026-07-21-serial-cross-platform-ci-reference.md)继续作为独立的标准托管完整性检查,手动大型运行器套件则保留规格比较,同时不扩大普通必需矩阵。
|
||||
|
||||
@@ -30,6 +30,6 @@ Status: implemented
|
||||
|
||||
## 后果
|
||||
|
||||
普通拉取请求会将企业级运行器容量用于 Linux 关键路径,而标准托管 Windows 作业则以更长的运行时间换取不依赖企业池的运行器分配。一次实际的分支头精确运行能够证明分支保护使用的同一组命令;排队延迟与每个作业从 `startedAt` 到 `completedAt` 的执行区间分开报告。
|
||||
普通拉取请求会将企业级运行器容量用于 Linux 关键路径,而 Wine 托管的 Windows 作业让其判定保持在标准 Linux 运行器分配上。一次实际的分支头精确运行能够证明分支保护使用的同一组命令;排队延迟与每个作业从 `startedAt` 到 `completedAt` 的执行区间分开报告。
|
||||
|
||||
企业级运行器分配能力下降时,标准兼容性作业和必需的 Windows 作业仍能提供有用证据,但无法让受阻的必需 Linux 作业或聚合流程变绿。恢复 Linux 可用性时,可能需要恢复完整的标准托管拓扑;仅改变运行器池定义的状态,不足以证明它可以接收作业。
|
||||
@@ -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-07-27-wine-windows-gates-experiment.md
|
||||
2026-07-27-wine-windows-gates-experiment.md: aab8aecdfca06c1f15641044a071015f543a84b6
|
||||
2026-07-27-wine-windows-gates-experiment.zh.md: 5239b185e1e0c63aa626ee3f20f3f298c0c8579d
|
||||
@@ -0,0 +1,45 @@
|
||||
# Agent Note: Wine-run Windows blocking gates on Linux runners
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-27-wine-windows-gates-experiment.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The pull-request Windows lane exists to prove the two blocking win32 surfaces — the workspace build and the production site — and it ran on hosted `windows-2025`, the slowest job in the required matrix: 7–9 minutes against 1.5–2.5 for the Linux jobs, so the Windows VM's boot, setup, and filesystem costs dominated every pull request's critical path.
|
||||
|
||||
The question the experiment answered: can a plain Linux runner produce an equivalent win32 signal for the blocking surfaces at Linux wall clock, so no Windows VM sits on the pull-request path at all?
|
||||
|
||||
## Decision
|
||||
|
||||
The required pull-request `windows` job in [ci.yml](../../../../.github/workflows/ci.yml) (`windows node 24 / wine blocking`) runs the blocking gate commands on `ubuntu-latest` under Wine with real Windows binaries: a checksum-verified win-x64 Node.js executes `tsc -b`, `tsdown`, and the VitePress production build, so the win32 branches of the toolchain — backslash path handling, `CreateProcess` spawn semantics, PE loading of `@esbuild/win32-x64`, and the rolldown/rollup MSVC `.node` addons — actually execute. The master `serial-windows` job is untouched: the complete native-kernel inventory, including the observational portability gates this lane does not run, still executes on real `windows-2025` on every master push.
|
||||
|
||||
Dependencies install natively on Linux with `supportedArchitectures` extended to win32-x64, which materializes the Windows platform packages in the same store; the cmd-shim layer is bypassed by invoking each tool's JavaScript entrypoint directly, the same processes `run-gates` ultimately spawns. `nodeLinker: hoisted` is load-bearing, not stylistic: the independent prototype in [PR #689](https://github.com/deepseek-harness/deepseek-harness/pull/689) kept pnpm's default isolated layout — including a faithful offline Windows-pnpm re-install over a Linux-prefetched store — and Windows Node under Wine still could not resolve `@esbuild/win32-x64` or load the koffi prebuild through the isolated symlink chain, failing before any repository gate ran. A flat layout with real files is what makes the gates reachable at all; #689's checksum pinning is adopted, while its Windows-pnpm-installs-the-tree goal is explicitly given up (the install contract stays Linux-tested here).
|
||||
|
||||
The lane holds the wall clock of the Linux CI jobs through four levers: the master-refreshed pnpm store cache (restore-only, same key as the Linux jobs), Wine provisioning (apt install, Windows Node download, `wineboot`) running concurrently with `pnpm install`, the two blocking surfaces running concurrently — the same shape `run-gates` gives them on native Windows — and an apt-archive cache keyed on the runner image, seeded from master by the `wine apt cache` job so every pull request restores from the default-branch scope.
|
||||
|
||||
Four environment constraints shape the job, each found as a red run: Ubuntu's `wine64` package alone puts nothing on PATH (install `wine`, the dispatcher); Node under Wine cannot attach stdio to the Actions runner's pipes (`Socket open EBADF` at bootstrap — every invocation routes stdio through a file); Wine does not realpath pnpm's isolated-layout Unix symlinks (the hoisted layout above); and Wine cannot create Windows symlinks (`ENOTSUP` from VitePress's `linkVue` — the `vue` link is laid down host-side before the gate).
|
||||
|
||||
## Measured results
|
||||
|
||||
Measured on 2026-07-27, warm caches, pull-request trigger, standard 2-core `ubuntu-latest`: 2m46s end-to-end — setup and cache restores ~17s, concurrent install+provision 33s, concurrent gates 110s — against 1.5–2.5 minutes for the Linux CI jobs and 7–9 minutes for the replaced `windows-2025` job. A cold-cache run pays roughly one extra minute. An 8-core benchmark leg was defined during the experiment but never left the restricted `dsh-ubuntu-*` pool's queue; the standard-runner number met the target, so no larger box is used.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep the hosted `windows-2025` pull-request job (status quo).** Nothing wrong with its signal, only its latency: 7–9 minutes for two build commands, the slowest required job in the matrix. It survives as the master serial reference, where completeness matters more than latency.
|
||||
|
||||
**A full Windows guest under QEMU/KVM inside the Linux runner.** Real NT kernel, so full fidelity including case-insensitive NTFS and ConPTY — but tens of minutes of image download and unattended install before the first gate runs (40m19s measured end-to-end on the sibling experiment branch `exp/kvm-windows-ci`). Promotable only with disk-image caching that pressures the Actions cache budget.
|
||||
|
||||
**Windows pnpm performing the install under Wine ([PR #689](https://github.com/deepseek-harness/deepseek-harness/pull/689)).** The higher-fidelity variant of this same idea: MinGit and pnpm staged into the prefix, a Linux prefetch filling the store, then `pnpm install --offline` run by Windows Node so the install contract itself executes as win32. It reached the install but not the gates — Wine's networking could not reach the registry directly, and the isolated `node_modules` layout defeated resolution of the Windows platform packages even after a clean offline install. This lane trades that fidelity away (hoisted layout, Linux-side install) to reach the gates; the two records are complementary halves of the same verdict.
|
||||
|
||||
**Filesystem-semantics lanes on Linux (casefolded ext4, filename lint).** Catches the highest-frequency Windows breakage class for near-zero cost but proves nothing about win32 binaries. Explored as the sibling experiment branch `exp/casefold-windows-ci`; complementary to, not competitive with, this lane.
|
||||
|
||||
**Windows containers.** Not possible: Windows containers require a Windows host kernel; a hosted Linux runner cannot run them.
|
||||
|
||||
**Dropping the Windows lane.** Rejected — win32 is a first-class product target: the koffi-backed DACL and durable-namespace modules, ConPTY-based PTY sessions, and Windows path policy all ship in `packages/`.
|
||||
|
||||
## Consequences
|
||||
|
||||
Every pull request's Windows verdict now arrives in Linux-job time on free standard capacity, and no Windows VM allocation sits on the pull-request critical path; `all checks passed` consumes the same `windows` job id it always did.
|
||||
|
||||
What the trade costs: Wine reimplements Win32 over a case-sensitive ext4 — NTFS case-insensitivity, real DACLs, ConPTY, and crash-durability semantics are not proved here, and the observational portability inventory (duplication, publint, node-next types, built-package invariants on win32) no longer runs on pull requests at all. The master `serial-windows` reference owns all of that: a Wine-green pull request can still fail the native-kernel master run, and that failure mode is accepted as post-merge. The lane also inherits Wine-specific divergences as permanent job structure — file-routed stdio, the host-side `vue` link, the hoisted layout — so a future toolchain change that depends on isolated-layout semantics or in-process symlink creation will surface here first as a Wine failure rather than a product failure, and triage must classify it as such. If Wine reds ever recur without product cause, the recorded fallback is reverting the `windows` job to the pre-Wine `windows-2025` definition preserved in git history.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Agent Note: 在 Linux runner 上用 Wine 运行 Windows 阻断门禁
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-27-wine-windows-gates-experiment.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Pull request 的 Windows 通道存在的意义是证明两个阻断性 win32 表面——workspace 构建与生产站点——它此前运行在托管 `windows-2025` 上,是必需矩阵中最慢的作业:7–9 分钟,对照 Linux 作业的 1.5–2.5 分钟,因此 Windows VM 的启动、准备与文件系统开销主导了每个 pull request 的关键路径。
|
||||
|
||||
实验回答的问题是:一台普通 Linux runner 能否以 Linux 墙钟为阻断表面产出等效的 win32 信号,让 pull request 路径上完全没有 Windows VM?
|
||||
|
||||
## 决策
|
||||
|
||||
[ci.yml](../../../../.github/workflows/ci.yml) 中必需的 pull request `windows` 作业(`windows node 24 / wine blocking`)在 `ubuntu-latest` 上通过 Wine 用真实 Windows 二进制运行阻断门禁命令:校验和验证过的 win-x64 Node.js 执行 `tsc -b`、`tsdown` 与 VitePress 生产构建,因此工具链的 win32 分支——反斜杠路径处理、`CreateProcess` 派生语义、`@esbuild/win32-x64` 的 PE 加载、以及 rolldown/rollup 的 MSVC `.node` 插件——都真正执行。master 的 `serial-windows` 作业原封不动:完整的原生内核清单,包括本通道不运行的观察性可移植性门禁,仍在每次 master push 时于真实 `windows-2025` 上执行。
|
||||
|
||||
依赖在 Linux 上原生安装,`supportedArchitectures` 扩展到 win32-x64,使 Windows 平台包物化进同一个 store;通过直接调用各工具的 JavaScript 入口绕开 cmd-shim 层,这正是 `run-gates` 最终派生的那些进程。`nodeLinker: hoisted` 是承重的,不是风格问题:[PR #689](https://github.com/deepseek-harness/deepseek-harness/pull/689) 的独立原型保留了 pnpm 默认的 isolated 布局——包括在 Linux 预取的 store 上忠实地用 Windows pnpm 离线重装——而 Wine 下的 Windows Node 依然无法穿过 isolated 符号链接链解析 `@esbuild/win32-x64` 或加载 koffi 预编译产物,在任何仓库门禁运行前就失败了。扁平的真实文件布局才让门禁变得可达;本通道采纳了 #689 的校验和固定,同时明确放弃其"Windows pnpm 安装依赖树"的目标(安装契约在此仍由 Linux 侧验证)。
|
||||
|
||||
该通道靠四个杠杆保持 Linux CI 作业的墙钟:master 刷新的 pnpm store 缓存(只恢复,与 Linux 作业同键)、Wine 供给(apt 安装、Windows Node 下载、`wineboot`)与 `pnpm install` 并发运行、两个阻断表面并发运行——与 `run-gates` 在原生 Windows 上给它们的形状相同——以及按 runner 镜像为键的 apt 归档缓存,由 master 的 `wine apt cache` 作业播种,使每个 pull request 都能从默认分支作用域恢复。
|
||||
|
||||
四条环境约束塑造了该作业,每条都以一次红色运行被发现:Ubuntu 的 `wine64` 包本身不往 PATH 放任何东西(要装 `wine` 调度器);Wine 下的 Node 无法把 stdio 接到 Actions runner 的管道上(引导期 `Socket open EBADF`——所有调用都经文件中转 stdio);Wine 不对 pnpm isolated 布局的 Unix 符号链接做 realpath(即上文的 hoisted 布局);Wine 无法创建 Windows 符号链接(VitePress 的 `linkVue` 报 `ENOTSUP`——`vue` 链接在门禁前由宿主侧铺好)。
|
||||
|
||||
## 实测结果
|
||||
|
||||
2026-07-27 实测,热缓存,pull request 触发,标准 2 核 `ubuntu-latest`:端到端 2 分 46 秒——准备与缓存恢复约 17 秒,并发安装+供给 33 秒,并发门禁 110 秒——对照 Linux CI 作业的 1.5–2.5 分钟与被替换的 `windows-2025` 作业的 7–9 分钟。冷缓存约多付一分钟。实验期间定义过 8 核基准腿,但它从未离开受限 `dsh-ubuntu-*` 池的队列;标准 runner 的数字已达标,故不使用更大的机器。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**保留托管 `windows-2025` 的 pull request 作业(现状)。** 其信号没有问题,问题只在延迟:为两条构建命令花 7–9 分钟,是必需矩阵中最慢的作业。它作为 master 串行参照存续——在那里完整性比延迟更重要。
|
||||
|
||||
**在 Linux runner 内用 QEMU/KVM 跑完整 Windows 客户机。** 真实 NT 内核,保真度完整,包括大小写不敏感的 NTFS 与 ConPTY——但首个门禁运行前要花数十分钟下载镜像并做无人值守安装(兄弟实验分支 `exp/kvm-windows-ci` 实测端到端 40 分 19 秒)。只有配上会挤压 Actions 缓存预算的磁盘镜像缓存才可晋升。
|
||||
|
||||
**在 Wine 下由 Windows pnpm 执行安装([PR #689](https://github.com/deepseek-harness/deepseek-harness/pull/689))。** 同一想法的更高保真度变体:把 MinGit 与 pnpm 放进 prefix,用 Linux 预取填充 store,再由 Windows Node 运行 `pnpm install --offline`,让安装契约本身以 win32 身份执行。它到达了安装但没到达门禁——Wine 的网络无法直接访问 registry,且 isolated 的 `node_modules` 布局即便在干净的离线安装后也挫败了 Windows 平台包的解析。本通道用掉这份保真度(hoisted 布局、Linux 侧安装)来换取门禁可达;两份记录是同一裁决互补的两半。
|
||||
|
||||
**Linux 上的文件系统语义通道(casefold ext4、文件名 lint)。** 以近零成本捕获最高频的 Windows 破坏类别,但对 win32 二进制什么也证明不了。作为兄弟实验分支 `exp/casefold-windows-ci` 探索;与本通道互补而非竞争。
|
||||
|
||||
**Windows 容器。** 不可行:Windows 容器要求 Windows 宿主内核;托管 Linux runner 无法运行。
|
||||
|
||||
**砍掉 Windows 通道。** 已否决——win32 是一等产品目标:基于 koffi 的 DACL 与持久命名空间模块、基于 ConPTY 的 PTY 会话、以及 Windows 路径策略都随 `packages/` 交付。
|
||||
|
||||
## 结果
|
||||
|
||||
每个 pull request 的 Windows 裁决现在以 Linux 作业的时间在免费标准容量上到达,pull request 关键路径上不再有任何 Windows VM 分配;`all checks passed` 消费的仍是原来的 `windows` 作业 id。
|
||||
|
||||
这笔交易的代价:Wine 在大小写敏感的 ext4 之上重实现 Win32——NTFS 大小写不敏感、真实 DACL、ConPTY 与崩溃持久性语义在此都未被证明,且观察性可移植性清单(duplication、publint、node-next 类型、win32 上的构建包不变量)完全不再于 pull request 上运行。master 的 `serial-windows` 参照拥有这一切:Wine 绿灯的 pull request 仍可能在原生内核的 master 运行上失败,该失败模式被接受为合并后处理。该通道还把 Wine 特有的分歧继承为永久的作业结构——文件中转的 stdio、宿主侧的 `vue` 链接、hoisted 布局——因此未来依赖 isolated 布局语义或进程内符号链接创建的工具链变更会先在这里以 Wine 失败而非产品失败的形式浮现,分诊必须如此归类。若 Wine 红灯在无产品原因的情况下反复出现,记录在案的退路是把 `windows` 作业还原为 git 历史中保存的 Wine 之前的 `windows-2025` 定义。
|
||||
@@ -1,6 +0,0 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/proposed/process/2026-07-27-wine-windows-gates-experiment.md
|
||||
2026-07-27-wine-windows-gates-experiment.md: 47a37ddb48f4321f916c7f7a0cb96ae80b133103
|
||||
2026-07-27-wine-windows-gates-experiment.zh.md: 3a912861110a06b39bfb2c37395fc6a061bdfbe6
|
||||
@@ -1,51 +0,0 @@
|
||||
# Agent Note: Wine-run Windows blocking gates on Linux runners
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-07-27-wine-windows-gates-experiment.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The pull-request Windows lane exists to prove the two blocking win32 surfaces — the workspace build and the production site — plus an observational portability inventory, and it runs on a dedicated paid Windows larger-runner pool; the master serial reference adds a second hosted Windows job. That pool is the only reason a Windows VM exists anywhere in this pipeline, and its provisioning, pricing, and slow setup dominate the lane's cost.
|
||||
|
||||
The open question: can a plain Linux runner produce an equivalent win32 signal for the blocking surfaces, so the dedicated Windows pool can shrink to a master-only reference or disappear from the pull-request path entirely?
|
||||
|
||||
## Proposal
|
||||
|
||||
[exp-wine-windows.yml](../../../../.github/workflows/exp-wine-windows.yml) (self-path-filtered, plus manual dispatch) runs the blocking gate commands on `ubuntu-latest` under Wine with real Windows binaries: a checksum-verified win-x64 Node.js executes `tsc -b`, `tsdown`, and the VitePress production build, so the win32 branches of the toolchain — backslash path handling, `CreateProcess` spawn semantics, PE loading of `@esbuild/win32-x64`, and the rolldown/rollup MSVC `.node` addons — actually execute.
|
||||
|
||||
Dependencies install natively on Linux with `supportedArchitectures` extended to win32-x64, which materializes the Windows platform packages in the same store; the cmd-shim layer is bypassed by invoking each tool's JavaScript entrypoint directly, the same processes `run-gates` ultimately spawns. `nodeLinker: hoisted` is load-bearing, not stylistic: the independent prototype in [PR #689](https://github.com/deepseek-harness/deepseek-harness/pull/689) kept pnpm's default isolated layout — including a faithful offline Windows-pnpm re-install over a Linux-prefetched store — and Windows Node under Wine still could not resolve `@esbuild/win32-x64` or load the koffi prebuild through the isolated symlink chain, failing before any repository gate ran. A flat layout with real files is what makes the gates reachable at all; #689's checksum pinning is adopted, while its Windows-pnpm-installs-the-tree goal is explicitly given up (the install contract stays Linux-tested here).
|
||||
|
||||
The lane targets the wall clock of the Linux CI jobs (about two minutes), from four levers: the master-refreshed pnpm store cache (restore-only, same key as ci.yml), Wine provisioning (apt install, Windows Node download, `wineboot`) running concurrently with `pnpm install`, the two blocking surfaces running concurrently — the same shape `run-gates` gives them on native Windows — and an apt-archive cache keyed on the runner image so Wine's package downloads are paid once per image version.
|
||||
|
||||
Measured on 2026-07-27: 2m46s end-to-end on a warm-cache pull-request run (setup and cache restores ~17s, concurrent install+provision 33s, concurrent gates 110s), against 1.5–2.5 minutes for the Linux CI jobs and 7–9 minutes for the paid Windows lane; a cold-cache run pays roughly one extra minute. The 8-core benchmark leg never left the queue — the restricted `dsh-ubuntu-*` pools were also observed queueing indefinitely from the sibling KVM experiment — so the standard-runner number stands as the result, and no larger box is needed to hit the target.
|
||||
|
||||
This is deliberately a fidelity probe, not a drop-in replacement: Wine reimplements the Win32 API over a case-sensitive ext4 (NTFS case-insensitivity is not emulated by default), provides no ConPTY, and substitutes its own security-descriptor and `MoveFileExW` semantics — exactly the surfaces the repo's `win32.ts` modules and PTY backend care about. The experiment measures which blocking gates pass, which fail for Wine reasons rather than product reasons, and the wall-clock cost relative to the recorded Windows benchmark lanes.
|
||||
|
||||
Promotion, if the verdict is positive: fold the Wine lane in as the pull-request Windows signal for blocking gates and demote the real-Windows pool to the master serial reference; otherwise record the failure class here and keep the pool.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep the dedicated Windows pool (status quo).** It is the baseline being priced; nothing is wrong with its signal, only with paying for a Windows VM pool whose blocking surface is two build commands.
|
||||
|
||||
**A full Windows guest under QEMU/KVM inside the Linux runner.** Real NT kernel, so full fidelity including case-insensitive NTFS and ConPTY — but tens of minutes of image download and unattended install before the first gate runs. Explored as the sibling experiment branch `exp/kvm-windows-ci`; the two experiments price fidelity against latency.
|
||||
|
||||
**Windows pnpm performing the install under Wine ([PR #689](https://github.com/deepseek-harness/deepseek-harness/pull/689)).** The higher-fidelity variant of this same idea: MinGit and pnpm staged into the prefix, a Linux prefetch filling the store, then `pnpm install --offline` run by Windows Node so the install contract itself executes as win32. It reached the install but not the gates — Wine's networking could not reach the registry directly, and the isolated `node_modules` layout defeated resolution of the Windows platform packages even after a clean offline install. This lane trades that fidelity away (hoisted layout, Linux-side install) to reach the gates; the two records are complementary halves of the same verdict.
|
||||
|
||||
**Filesystem-semantics lanes on Linux (casefolded ext4, filename lint).** Catches the highest-frequency Windows breakage class for near-zero cost but proves nothing about win32 binaries. Explored as the sibling experiment branch `exp/casefold-windows-ci`.
|
||||
|
||||
**Windows containers.** Not possible: Windows containers require a Windows host kernel; a hosted Linux runner cannot run them.
|
||||
|
||||
**Dropping the Windows lane.** Rejected — win32 is a first-class product target: the koffi-backed DACL and durable-namespace modules, ConPTY-based PTY sessions, and Windows path policy all ship in `packages/`.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- The workflow completes on `ubuntu-latest` with an independent pass/fail verdict per blocking surface (build, production site) and a recorded wall-clock comparison against both the paid Windows lane and the Linux CI jobs.
|
||||
- End-to-end wall clock lands in the same band as the Linux CI jobs (minutes, not tens of minutes), demonstrating the pool-replacement case on cost as well as signal.
|
||||
- A decision is recorded here: promote the lane, keep it as a non-blocking canary, or reject it with the observed failure class.
|
||||
|
||||
## Risks
|
||||
|
||||
- False greens: Wine's case-sensitive filesystem and permissive path handling can pass code that breaks on real NTFS, so this lane can complement but never fully replace a real-kernel check for release qualification.
|
||||
- False reds: missing or stubbed Win32 APIs under Wine fail gates for non-product reasons, and each such failure costs triage time to classify.
|
||||
- Throughput: Wine's syscall translation on the 2-core standard runner may push the blocking gates past the paid Windows lane's wall clock, erasing the cost argument; the run records the numbers either way.
|
||||
@@ -1,51 +0,0 @@
|
||||
# Agent Note: 在 Linux runner 上用 Wine 运行 Windows 阻断门禁
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-07-27-wine-windows-gates-experiment.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Pull request 的 Windows 通道存在的意义是证明两个阻断性 win32 表面——workspace 构建与生产站点——外加一份观察性可移植性清单,它运行在一个专用的付费 Windows larger-runner 池上;master 串行参照又增加一个托管 Windows 作业。该池是这条流水线中唯一需要 Windows VM 的理由,而其供给、计价与缓慢的准备阶段主导了该通道的成本。
|
||||
|
||||
悬而未决的问题是:一台普通 Linux runner 能否为阻断表面产出等效的 win32 信号,让专用 Windows 池收缩为仅 master 的参照、甚至完全退出 pull request 路径?
|
||||
|
||||
## 提案
|
||||
|
||||
[exp-wine-windows.yml](../../../../.github/workflows/exp-wine-windows.yml)(自身路径过滤,外加手动触发)在 `ubuntu-latest` 上通过 Wine 用真实 Windows 二进制运行阻断门禁命令:校验和验证过的 win-x64 Node.js 执行 `tsc -b`、`tsdown` 与 VitePress 生产构建,因此工具链的 win32 分支——反斜杠路径处理、`CreateProcess` 派生语义、`@esbuild/win32-x64` 的 PE 加载、以及 rolldown/rollup 的 MSVC `.node` 插件——都真正执行。
|
||||
|
||||
依赖在 Linux 上原生安装,`supportedArchitectures` 扩展到 win32-x64,使 Windows 平台包物化进同一个 store;通过直接调用各工具的 JavaScript 入口绕开 cmd-shim 层,这正是 `run-gates` 最终派生的那些进程。`nodeLinker: hoisted` 是承重的,不是风格问题:[PR #689](https://github.com/deepseek-harness/deepseek-harness/pull/689) 的独立原型保留了 pnpm 默认的 isolated 布局——包括在 Linux 预取的 store 上忠实地用 Windows pnpm 离线重装——而 Wine 下的 Windows Node 依然无法穿过 isolated 符号链接链解析 `@esbuild/win32-x64` 或加载 koffi 预编译产物,在任何仓库门禁运行前就失败了。扁平的真实文件布局才让门禁变得可达;本通道采纳了 #689 的校验和固定,同时明确放弃其"Windows pnpm 安装依赖树"的目标(安装契约在此仍由 Linux 侧验证)。
|
||||
|
||||
该通道以 Linux CI 作业的墙钟(约两分钟)为目标,靠四个杠杆:master 刷新的 pnpm store 缓存(只恢复,与 ci.yml 同键)、Wine 供给(apt 安装、Windows Node 下载、`wineboot`)与 `pnpm install` 并发运行、两个阻断表面并发运行——与 `run-gates` 在原生 Windows 上给它们的形状相同——以及按 runner 镜像为键的 apt 归档缓存,使 Wine 的包下载每个镜像版本只付一次。
|
||||
|
||||
2026-07-27 实测:热缓存 pull request 运行端到端 2 分 46 秒(准备与缓存恢复约 17 秒,并发安装+供给 33 秒,并发门禁 110 秒),对照 Linux CI 作业的 1.5–2.5 分钟与付费 Windows 通道的 7–9 分钟;冷缓存约多付一分钟。8 核基准腿从未离开队列——受限的 `dsh-ubuntu-*` 池在兄弟 KVM 实验中也被观察到无限排队——因此标准 runner 的数字即为结果,达标不需要更大的机器。
|
||||
|
||||
这刻意是一次保真度探针,而非直接替换:Wine 在大小写敏感的 ext4 之上重实现 Win32 API(默认不模拟 NTFS 的大小写不敏感)、不提供 ConPTY、并用自己的安全描述符与 `MoveFileExW` 语义替代——恰是本仓库 `win32.ts` 模块与 PTY 后端关心的表面。实验度量哪些阻断门禁通过、哪些因 Wine 原因而非产品原因失败,以及相对已记录 Windows 基准通道的墙钟成本。
|
||||
|
||||
若结论为正则晋升:把 Wine 通道并入为 pull request 的阻断门禁 Windows 信号,将真实 Windows 池降级为 master 串行参照;否则在此记录失败类别并保留该池。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**保留专用 Windows 池(现状)。** 它正是被计价的基线;其信号没有问题,问题只在于为一个阻断表面仅是两条构建命令的 Windows VM 池付费。
|
||||
|
||||
**在 Linux runner 内用 QEMU/KVM 跑完整 Windows 客户机。** 真实 NT 内核,保真度完整,包括大小写不敏感的 NTFS 与 ConPTY——但首个门禁运行前要花数十分钟下载镜像并做无人值守安装。作为兄弟实验分支 `exp/kvm-windows-ci` 探索;两个实验共同为保真度与延迟定价。
|
||||
|
||||
**在 Wine 下由 Windows pnpm 执行安装([PR #689](https://github.com/deepseek-harness/deepseek-harness/pull/689))。** 同一想法的更高保真度变体:把 MinGit 与 pnpm 放进 prefix,用 Linux 预取填充 store,再由 Windows Node 运行 `pnpm install --offline`,让安装契约本身以 win32 身份执行。它到达了安装但没到达门禁——Wine 的网络无法直接访问 registry,且 isolated 的 `node_modules` 布局即便在干净的离线安装后也挫败了 Windows 平台包的解析。本通道用掉这份保真度(hoisted 布局、Linux 侧安装)来换取门禁可达;两份记录是同一裁决互补的两半。
|
||||
|
||||
**Linux 上的文件系统语义通道(casefold ext4、文件名 lint)。** 以近零成本捕获最高频的 Windows 破坏类别,但对 win32 二进制什么也证明不了。作为兄弟实验分支 `exp/casefold-windows-ci` 探索。
|
||||
|
||||
**Windows 容器。** 不可行:Windows 容器要求 Windows 宿主内核;托管 Linux runner 无法运行。
|
||||
|
||||
**砍掉 Windows 通道。** 已否决——win32 是一等产品目标:基于 koffi 的 DACL 与持久命名空间模块、基于 ConPTY 的 PTY 会话、以及 Windows 路径策略都随 `packages/` 交付。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 该 workflow 在 `ubuntu-latest` 上完成,对每个阻断表面(构建、生产站点)给出独立的通过/失败裁决,并记录与付费 Windows 通道及 Linux CI 作业两者的墙钟对比。
|
||||
- 端到端墙钟落在 Linux CI 作业的同一档位(分钟级,而非数十分钟),从成本与信号两方面共同论证替换池的理由。
|
||||
- 在此记录一项决定:晋升该通道、保留为非阻断金丝雀、或以观察到的失败类别否决。
|
||||
|
||||
## 风险
|
||||
|
||||
- 假绿:Wine 的大小写敏感文件系统与宽松路径处理可能放过在真实 NTFS 上会坏的代码,因此该通道可以补充、但永远无法完全替代发布资格所需的真实内核检查。
|
||||
- 假红:Wine 下缺失或桩化的 Win32 API 会因非产品原因让门禁失败,每次此类失败都要花分诊时间归类。
|
||||
- 吞吐:Wine 的系统调用翻译在 2 核标准 runner 上可能让阻断门禁的墙钟超过付费 Windows 通道,抹掉成本论点;无论结果如何,运行都会记录数字。
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
# AGENTS.md — GitHub Actions
|
||||
|
||||
Run Windows jobs under native `pwsh`.
|
||||
Run jobs on Windows runners (`windows-*` labels) under native `pwsh`. The pull-request `windows` job is not one of them: it runs Windows Node under Wine on hosted Linux, so its steps are bash — see the [Wine lane Agent Note](../.agents/notes/implemented/process/2026-07-27-wine-windows-gates-experiment.md).
|
||||
+205
-22
@@ -281,41 +281,224 @@ jobs:
|
||||
- name: Run complete keyless Python suite
|
||||
run: uv run --python 3.10 --group test --project python/sdk pytest
|
||||
|
||||
# One standard Windows box shares setup across the required build/site checks
|
||||
# and the observational portability inventory. Serial worker bounds keep this
|
||||
# recovery path portable; Linux owns duplicate lint, coverage, and snapshots.
|
||||
# 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 master
|
||||
# serial-windows job below keeps the complete native-kernel inventory —
|
||||
# including the observational portability gates this lane does not run —
|
||||
# on real windows-2025. Direct tool entrypoints stand in for pnpm's cmd
|
||||
# shims, which a Linux-side install does not create; layout, fidelity
|
||||
# limits, and measured timings live in
|
||||
# .agents/notes/implemented/process/2026-07-27-wine-windows-gates-experiment.md
|
||||
windows:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: windows-2025
|
||||
name: windows node 24 / complete
|
||||
runs-on: ubuntu-latest
|
||||
name: windows node 24 / wine blocking
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
DSH_COVERAGE_MAX_WORKERS: '1'
|
||||
DSH_GATE_CONCURRENCY: '1'
|
||||
DSH_PUBLINT_CONCURRENCY: '1'
|
||||
WINEDEBUG: '-all'
|
||||
WINEARCH: win64
|
||||
# Skip Wine Mono / Gecko installers: Node needs neither.
|
||||
WINEDLLOVERRIDES: 'mscoree,mshtml='
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Enable Developer Mode (symlink support)
|
||||
shell: pwsh
|
||||
run: >-
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
|
||||
/t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.PRIMARY_NODE_VERSION }}
|
||||
|
||||
# Extracting the many-file pnpm store cache is slower than a clean install,
|
||||
# and saving it adds more latency after gates.
|
||||
- name: Enable corepack and install (immutable)
|
||||
shell: pwsh
|
||||
- uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: /home/runner/.local/share/pnpm/store/v11
|
||||
key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-
|
||||
|
||||
# Master's wine-apt-cache job seeds the default-branch scope every pull
|
||||
# request can read; a save from this job only reaches reruns of the
|
||||
# same merge ref.
|
||||
- name: Compose Wine apt cache key
|
||||
id: wine-cache-key
|
||||
run: echo "key=wine-debs-${ImageOS:-linux}-${ImageVersion:-v0}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/wine-debs
|
||||
key: ${{ steps.wine-cache-key.outputs.key }}
|
||||
|
||||
- name: Install dependencies and provision Wine concurrently
|
||||
run: |
|
||||
corepack enable
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run blocking and observational Windows gates concurrently
|
||||
shell: pwsh
|
||||
run: pnpm run check:ci:windows-complete
|
||||
# Windows-lane install-time overrides. supportedArchitectures
|
||||
# additionally materializes the win32-x64 platform packages
|
||||
# (@esbuild/win32-x64, rolldown and rollup MSVC bindings) the
|
||||
# Windows toolchain resolves at runtime; nodeLinker: hoisted lays
|
||||
# node_modules out flat with real files because Windows Node under
|
||||
# Wine does not realpath pnpm's isolated-layout symlinks. Neither
|
||||
# override is recorded in the lockfile, so --frozen-lockfile stays
|
||||
# valid. --ignore-scripts skips Linux lifecycle scripts no gate in
|
||||
# this lane loads; the win32 binaries ship prebuilt.
|
||||
cat >> pnpm-workspace.yaml <<'EOF'
|
||||
|
||||
nodeLinker: hoisted
|
||||
supportedArchitectures:
|
||||
os: [current, win32]
|
||||
cpu: [current, x64]
|
||||
EOF
|
||||
|
||||
pnpm install --frozen-lockfile --ignore-scripts &
|
||||
install_pid=$!
|
||||
|
||||
provision_wine() {
|
||||
set -euo pipefail
|
||||
# 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.
|
||||
if compgen -G "$HOME/wine-debs/*.deb" > /dev/null; then
|
||||
sudo apt-get install -y --no-install-recommends "$HOME"/wine-debs/*.deb
|
||||
else
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends --download-only wine
|
||||
mkdir -p "$HOME/wine-debs"
|
||||
cp /var/cache/apt/archives/*.deb "$HOME/wine-debs/" 2>/dev/null || true
|
||||
sudo apt-get install -y --no-install-recommends wine
|
||||
fi
|
||||
WINE_BIN=''
|
||||
for candidate in "$(command -v wine || true)" "$(command -v wine64 || true)" /usr/lib/wine/wine64; do
|
||||
if [ -n "$candidate" ] && [ -x "$candidate" ]; then WINE_BIN="$candidate"; break; fi
|
||||
done
|
||||
[ -n "$WINE_BIN" ] || { echo '::error::no wine binary found after install'; exit 1; }
|
||||
echo "WINE_BIN=$WINE_BIN" >> "$GITHUB_ENV"
|
||||
|
||||
# Windows Node for the repo's primary line, checksum-verified
|
||||
# against the same dist directory.
|
||||
version=$(curl -fsSL https://nodejs.org/dist/index.json \
|
||||
| jq -r --arg p "v${PRIMARY_NODE_VERSION}." '[.[] | select(.version | startswith($p))][0].version')
|
||||
echo "Windows Node: $version"
|
||||
curl -fsSL -o "$RUNNER_TEMP/node-win.zip" \
|
||||
"https://nodejs.org/dist/${version}/node-${version}-win-x64.zip"
|
||||
curl -fsSL "https://nodejs.org/dist/${version}/SHASUMS256.txt" \
|
||||
| awk -v a="node-${version}-win-x64.zip" '$2 == a { print $1 " '"$RUNNER_TEMP"'/node-win.zip" }' \
|
||||
| sha256sum --check -
|
||||
unzip -q "$RUNNER_TEMP/node-win.zip" -d "$RUNNER_TEMP/node-win"
|
||||
echo "NODE_WIN=$RUNNER_TEMP/node-win/node-${version}-win-x64/node.exe" >> "$GITHUB_ENV"
|
||||
|
||||
"$WINE_BIN" wineboot --init || true
|
||||
wineserver -w || true
|
||||
}
|
||||
provision_wine &
|
||||
wine_pid=$!
|
||||
|
||||
install_status=0
|
||||
wait "$install_pid" || install_status=$?
|
||||
wine_status=0
|
||||
wait "$wine_pid" || wine_status=$?
|
||||
if (( install_status != 0 )); then exit "$install_status"; fi
|
||||
exit "$wine_status"
|
||||
|
||||
- name: Resolve entrypoints, link vue, smoke Windows Node
|
||||
run: |
|
||||
# Node under Wine cannot attach stdio to the Actions runner's pipes
|
||||
# (Socket open EBADF at bootstrap), so every invocation runs through
|
||||
# this wrapper: stdio to a regular file, replayed after exit.
|
||||
cat > "$RUNNER_TEMP/wine-node.sh" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
set -u
|
||||
log="$1"; shift
|
||||
"$WINE_BIN" "$NODE_WIN" "$@" < /dev/null > "$log" 2>&1
|
||||
status=$?
|
||||
tail -n 300 "$log"
|
||||
exit "$status"
|
||||
SH
|
||||
chmod +x "$RUNNER_TEMP/wine-node.sh"
|
||||
|
||||
resolve() {
|
||||
local name="$1"; shift
|
||||
for p in "$@"; do
|
||||
if [ -f "$p" ]; then echo "$name=$PWD/$p" >> "$GITHUB_ENV"; return 0; fi
|
||||
done
|
||||
echo "::error::$name not found at any of: $*"; return 1
|
||||
}
|
||||
resolve TSC_JS node_modules/typescript/bin/tsc
|
||||
resolve TSDOWN_JS node_modules/tsdown/dist/run.mjs
|
||||
resolve VITEPRESS_JS website/node_modules/vitepress/bin/vitepress.js node_modules/vitepress/bin/vitepress.js
|
||||
|
||||
# VitePress links vue into the site's node_modules at build time;
|
||||
# Wine cannot CREATE Windows symlinks (ENOTSUP) but follows
|
||||
# pre-existing Unix ones, so lay the link down host-side.
|
||||
if [ -d node_modules/vue ] && [ ! -e website/node_modules/vue ]; then
|
||||
mkdir -p website/node_modules
|
||||
ln -s ../../node_modules/vue website/node_modules/vue
|
||||
fi
|
||||
|
||||
"$RUNNER_TEMP/wine-node.sh" "$RUNNER_TEMP/smoke.log" -p "'smoke: ' + process.platform + ' ' + process.arch + ' ' + process.version"
|
||||
|
||||
# The two blocking surfaces run concurrently, the same shape run-gates
|
||||
# gives ci-windows-blocking on native Windows: `build` = tsc -b then
|
||||
# tsdown, `production site` = the VitePress build. Both statuses are
|
||||
# captured so one failure cannot hide the other's result.
|
||||
- name: Run blocking Windows gates concurrently under Wine
|
||||
run: |
|
||||
build_gate() {
|
||||
"$RUNNER_TEMP/wine-node.sh" "$RUNNER_TEMP/tsc.log" "$TSC_JS" -b --pretty false || return $?
|
||||
"$RUNNER_TEMP/wine-node.sh" "$RUNNER_TEMP/tsdown.log" "$TSDOWN_JS"
|
||||
}
|
||||
site_gate() {
|
||||
cd website
|
||||
"$RUNNER_TEMP/wine-node.sh" "$RUNNER_TEMP/site.log" "$VITEPRESS_JS" build .
|
||||
}
|
||||
start=$SECONDS
|
||||
build_gate > "$RUNNER_TEMP/build-gate.out" 2>&1 &
|
||||
build_pid=$!
|
||||
site_gate > "$RUNNER_TEMP/site-gate.out" 2>&1 &
|
||||
site_pid=$!
|
||||
build_status=0
|
||||
wait "$build_pid" || build_status=$?
|
||||
site_status=0
|
||||
wait "$site_pid" || site_status=$?
|
||||
echo "== build gate (exit $build_status, $((SECONDS - start))s elapsed) =="
|
||||
tail -n 120 "$RUNNER_TEMP/build-gate.out"
|
||||
echo "== production site gate (exit $site_status, $((SECONDS - start))s elapsed) =="
|
||||
tail -n 120 "$RUNNER_TEMP/site-gate.out"
|
||||
if (( build_status != 0 )); then exit "$build_status"; fi
|
||||
exit "$site_status"
|
||||
|
||||
- name: Shut down wineserver
|
||||
if: always()
|
||||
run: wineserver -k 2>/dev/null || true
|
||||
|
||||
# Master seeds the Wine apt-archive cache in the default-branch scope,
|
||||
# which every pull request's windows job can restore; saves from
|
||||
# pull-request runs are scoped to their own merge ref and help nobody
|
||||
# else. Runs in seconds when the image version already has a cache.
|
||||
wine-apt-cache:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
name: wine apt cache
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Compose Wine apt cache key
|
||||
id: wine-cache-key
|
||||
run: echo "key=wine-debs-${ImageOS:-linux}-${ImageVersion:-v0}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: wine-cache
|
||||
with:
|
||||
path: ~/wine-debs
|
||||
key: ${{ steps.wine-cache-key.outputs.key }}
|
||||
|
||||
- name: Download the Wine dependency closure
|
||||
if: steps.wine-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends --download-only wine
|
||||
mkdir -p "$HOME/wine-debs"
|
||||
cp /var/cache/apt/archives/*.deb "$HOME/wine-debs/"
|
||||
du -sh "$HOME/wine-debs"
|
||||
|
||||
# Master pushes run only the serial reference jobs below.
|
||||
# Each host executes the complete, unsharded primary Node aggregate with one
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
# EXPERIMENT: run the blocking Windows CI gates on a Linux runner through
|
||||
# Wine with a real Windows Node.js binary, at roughly the wall clock of the
|
||||
# Linux CI jobs (~2 min). Speed comes from four levers: the master-refreshed
|
||||
# pnpm store cache, provisioning Wine concurrently with the dependency
|
||||
# install, running the two blocking surfaces concurrently (the same shape
|
||||
# run-gates gives them on native Windows), and an apt package cache for Wine
|
||||
# itself. Dependency provisioning happens natively on Linux with
|
||||
# `supportedArchitectures` extended to win32-x64 so the Windows
|
||||
# esbuild/rolldown/rollup binaries are present, and `nodeLinker: hoisted`
|
||||
# because Windows Node under Wine does not realpath pnpm's isolated-layout
|
||||
# Unix symlinks — the sibling prototype in PR #689 kept the isolated layout
|
||||
# and failed on exactly that. The pnpm-run/cmd shim layer is deliberately
|
||||
# bypassed; each gate invokes its tool's JavaScript entrypoint directly — the
|
||||
# same commands run-gates ultimately spawns. Owning rationale and promotion
|
||||
# criteria:
|
||||
# .agents/notes/proposed/process/2026-07-27-wine-windows-gates-experiment.md
|
||||
name: Experiment Wine Windows gates
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/exp-wine-windows.yml
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PRIMARY_NODE_VERSION: '24'
|
||||
|
||||
jobs:
|
||||
wine-blocking-gates:
|
||||
name: wine / blocking windows gates (${{ matrix.runner }})
|
||||
# Pull requests run the free standard runner only; a manual dispatch adds
|
||||
# the 8-core benchmark pool for a like-for-like core-count comparison.
|
||||
# The larger leg stays dispatch-only because those restricted pools can
|
||||
# queue indefinitely (observed on the sibling KVM experiment).
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
runner: ${{ fromJSON(github.event_name == 'workflow_dispatch' && '["ubuntu-latest", "dsh-ubuntu-24-04-8core"]' || '["ubuntu-latest"]') }}
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
WINEDEBUG: '-all'
|
||||
WINEARCH: win64
|
||||
# Skip Wine Mono / Gecko installers: Node needs neither.
|
||||
WINEDLLOVERRIDES: 'mscoree,mshtml='
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.PRIMARY_NODE_VERSION }}
|
||||
|
||||
# The default-branch pnpm store cache ci.yml maintains; restore-only,
|
||||
# same key, so this lane rides the cache master already refreshes.
|
||||
- uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: /home/runner/.local/share/pnpm/store/v11
|
||||
key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-
|
||||
|
||||
# Keyed on the runner image so a new image version re-downloads once.
|
||||
# Cache scoping: each trigger seeds its own scope (pull_request → the
|
||||
# PR merge ref, dispatch → the branch); only same-scope reruns hit.
|
||||
# Promotion to ci.yml would let master seed the shared default-branch
|
||||
# scope every trigger reads, as the pnpm store cache already does.
|
||||
- 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 }}
|
||||
|
||||
- name: Install dependencies and provision Wine concurrently
|
||||
run: |
|
||||
corepack enable
|
||||
|
||||
# Experiment-only install-time overrides. supportedArchitectures
|
||||
# additionally materializes the win32-x64 platform packages
|
||||
# (@esbuild/win32-x64, rolldown and rollup MSVC bindings) the
|
||||
# Windows toolchain resolves at runtime; nodeLinker: hoisted lays
|
||||
# node_modules out flat with real files because Windows Node under
|
||||
# Wine does not realpath pnpm's isolated-layout symlinks (PR #689's
|
||||
# failure mode). Neither override is recorded in the lockfile, so
|
||||
# --frozen-lockfile stays valid. --ignore-scripts skips the Linux
|
||||
# esbuild/node-pty/lefthook lifecycle scripts: no gate in this lane
|
||||
# loads them, and the win32 binaries ship prebuilt in their
|
||||
# packages.
|
||||
cat >> pnpm-workspace.yaml <<'EOF'
|
||||
|
||||
nodeLinker: hoisted
|
||||
supportedArchitectures:
|
||||
os: [current, win32]
|
||||
cpu: [current, x64]
|
||||
EOF
|
||||
|
||||
pnpm install --frozen-lockfile --ignore-scripts &
|
||||
install_pid=$!
|
||||
|
||||
provision_wine() {
|
||||
set -euo pipefail
|
||||
# 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.
|
||||
if compgen -G "$HOME/wine-debs/*.deb" > /dev/null; then
|
||||
sudo apt-get install -y --no-install-recommends "$HOME"/wine-debs/*.deb
|
||||
else
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends --download-only wine
|
||||
mkdir -p "$HOME/wine-debs"
|
||||
cp /var/cache/apt/archives/*.deb "$HOME/wine-debs/" 2>/dev/null || true
|
||||
sudo apt-get install -y --no-install-recommends wine
|
||||
fi
|
||||
WINE_BIN=''
|
||||
for candidate in "$(command -v wine || true)" "$(command -v wine64 || true)" /usr/lib/wine/wine64; do
|
||||
if [ -n "$candidate" ] && [ -x "$candidate" ]; then WINE_BIN="$candidate"; break; fi
|
||||
done
|
||||
[ -n "$WINE_BIN" ] || { echo '::error::no wine binary found after install'; exit 1; }
|
||||
echo "WINE_BIN=$WINE_BIN" >> "$GITHUB_ENV"
|
||||
|
||||
# Windows Node for the repo's primary line, checksum-verified
|
||||
# against the same dist directory (adopted from PR #689).
|
||||
version=$(curl -fsSL https://nodejs.org/dist/index.json \
|
||||
| jq -r --arg p "v${PRIMARY_NODE_VERSION}." '[.[] | select(.version | startswith($p))][0].version')
|
||||
echo "Windows Node: $version"
|
||||
curl -fsSL -o "$RUNNER_TEMP/node-win.zip" \
|
||||
"https://nodejs.org/dist/${version}/node-${version}-win-x64.zip"
|
||||
curl -fsSL "https://nodejs.org/dist/${version}/SHASUMS256.txt" \
|
||||
| awk -v a="node-${version}-win-x64.zip" '$2 == a { print $1 " '"$RUNNER_TEMP"'/node-win.zip" }' \
|
||||
| sha256sum --check -
|
||||
unzip -q "$RUNNER_TEMP/node-win.zip" -d "$RUNNER_TEMP/node-win"
|
||||
echo "NODE_WIN=$RUNNER_TEMP/node-win/node-${version}-win-x64/node.exe" >> "$GITHUB_ENV"
|
||||
|
||||
"$WINE_BIN" wineboot --init || true
|
||||
wineserver -w || true
|
||||
}
|
||||
provision_wine &
|
||||
wine_pid=$!
|
||||
|
||||
install_status=0
|
||||
wait "$install_pid" || install_status=$?
|
||||
wine_status=0
|
||||
wait "$wine_pid" || wine_status=$?
|
||||
if (( install_status != 0 )); then exit "$install_status"; fi
|
||||
exit "$wine_status"
|
||||
|
||||
- name: Resolve entrypoints, link vue, smoke Windows Node
|
||||
run: |
|
||||
# Node under Wine cannot attach stdio to the Actions runner's pipes
|
||||
# (Socket open EBADF at bootstrap), so every invocation runs through
|
||||
# this wrapper: stdio to a regular file, replayed after exit.
|
||||
cat > "$RUNNER_TEMP/wine-node.sh" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
set -u
|
||||
log="$1"; shift
|
||||
"$WINE_BIN" "$NODE_WIN" "$@" < /dev/null > "$log" 2>&1
|
||||
status=$?
|
||||
tail -n 300 "$log"
|
||||
exit "$status"
|
||||
SH
|
||||
chmod +x "$RUNNER_TEMP/wine-node.sh"
|
||||
|
||||
resolve() {
|
||||
local name="$1"; shift
|
||||
for p in "$@"; do
|
||||
if [ -f "$p" ]; then echo "$name=$PWD/$p" >> "$GITHUB_ENV"; return 0; fi
|
||||
done
|
||||
echo "::error::$name not found at any of: $*"; return 1
|
||||
}
|
||||
resolve TSC_JS node_modules/typescript/bin/tsc
|
||||
resolve TSDOWN_JS node_modules/tsdown/dist/run.mjs
|
||||
resolve VITEPRESS_JS website/node_modules/vitepress/bin/vitepress.js node_modules/vitepress/bin/vitepress.js
|
||||
|
||||
# VitePress links vue into the site's node_modules at build time;
|
||||
# Wine cannot CREATE Windows symlinks (ENOTSUP) but follows
|
||||
# pre-existing Unix ones, so lay the link down host-side.
|
||||
if [ -d node_modules/vue ] && [ ! -e website/node_modules/vue ]; then
|
||||
mkdir -p website/node_modules
|
||||
ln -s ../../node_modules/vue website/node_modules/vue
|
||||
fi
|
||||
|
||||
"$RUNNER_TEMP/wine-node.sh" "$RUNNER_TEMP/smoke.log" -p "'smoke: ' + process.platform + ' ' + process.arch + ' ' + process.version"
|
||||
|
||||
# The two blocking surfaces run concurrently, the same shape run-gates
|
||||
# gives ci-windows-blocking on native Windows (DSH_GATE_CONCURRENCY):
|
||||
# `build` = tsc -b then tsdown, `production site` = the VitePress
|
||||
# build. Both statuses are captured so one failure cannot hide the
|
||||
# other's result.
|
||||
- name: Run blocking Windows gates concurrently under Wine
|
||||
timeout-minutes: 20
|
||||
run: |
|
||||
build_gate() {
|
||||
"$RUNNER_TEMP/wine-node.sh" "$RUNNER_TEMP/tsc.log" "$TSC_JS" -b --pretty false || return $?
|
||||
"$RUNNER_TEMP/wine-node.sh" "$RUNNER_TEMP/tsdown.log" "$TSDOWN_JS"
|
||||
}
|
||||
site_gate() {
|
||||
cd website
|
||||
"$RUNNER_TEMP/wine-node.sh" "$RUNNER_TEMP/site.log" "$VITEPRESS_JS" build .
|
||||
}
|
||||
start=$SECONDS
|
||||
build_gate > "$RUNNER_TEMP/build-gate.out" 2>&1 &
|
||||
build_pid=$!
|
||||
site_gate > "$RUNNER_TEMP/site-gate.out" 2>&1 &
|
||||
site_pid=$!
|
||||
build_status=0
|
||||
wait "$build_pid" || build_status=$?
|
||||
site_status=0
|
||||
wait "$site_pid" || site_status=$?
|
||||
echo "== build gate (exit $build_status, $((SECONDS - start))s elapsed) =="
|
||||
tail -n 120 "$RUNNER_TEMP/build-gate.out"
|
||||
echo "== production site gate (exit $site_status, $((SECONDS - start))s elapsed) =="
|
||||
tail -n 120 "$RUNNER_TEMP/site-gate.out"
|
||||
if (( build_status != 0 )); then exit "$build_status"; fi
|
||||
exit "$site_status"
|
||||
|
||||
- name: Shut down wineserver
|
||||
if: always()
|
||||
run: wineserver -k 2>/dev/null || true
|
||||
Reference in New Issue
Block a user