ci: experiment — Wine-run Windows blocking gates on a Linux runner

This commit is contained in:
Tianyi Cui
2026-07-27 04:37:00 +08:00
parent f3a4833dbf
commit c115357737
4 changed files with 217 additions and 0 deletions

View File

@@ -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
2026-07-27-wine-windows-gates-experiment.md: 9f7856dfef229f8f02c85f5968082a0c857bbc94
2026-07-27-wine-windows-gates-experiment.zh.md: cb185293d7f22723a96448a774bd27dd31e1bc28

View File

@@ -0,0 +1,44 @@
# 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 downloaded 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.
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.
**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 gate (tsc, tsdown, production site) and a recorded wall-clock comparison against the Windows benchmark lanes.
- 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.

View File

@@ -0,0 +1,44 @@
# 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` 最终派生的那些进程。
这刻意是一次保真度探针而非直接替换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` 探索;两个实验共同为保真度与延迟定价。
**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` 上完成对每个阻断门禁tsc、tsdown、生产站点给出独立的通过/失败裁决,并记录与 Windows 基准通道的墙钟对比。
- 在此记录一项决定:晋升该通道、保留为非阻断金丝雀、或以观察到的失败类别否决。
## 风险
- 假绿Wine 的大小写敏感文件系统与宽松路径处理可能放过在真实 NTFS 上会坏的代码,因此该通道可以补充、但永远无法完全替代发布资格所需的真实内核检查。
- 假红Wine 下缺失或桩化的 Win32 API 会因非产品原因让门禁失败,每次此类失败都要花分诊时间归类。
- 吞吐Wine 的系统调用翻译在 2 核标准 runner 上可能让阻断门禁的墙钟超过付费 Windows 通道,抹掉成本论点;无论结果如何,运行都会记录数字。

123
.github/workflows/exp-wine-windows.yml vendored Normal file
View File

@@ -0,0 +1,123 @@
# EXPERIMENT: run the blocking Windows CI gates on a Linux runner through
# Wine, and execute the gate commands with a real Windows Node.js binary.
# Dependency provisioning happens natively on Linux with
# `supportedArchitectures` extended to win32-x64 so the Windows
# esbuild/rolldown/rollup binaries are present in the store. The pnpm-run/cmd
# shim layer is deliberately bypassed (a Linux install writes POSIX shims
# only), so 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
# Deliberately the cheapest hosted substrate: if Wine holds up here, the
# lane needs no special pool at all.
runs-on: ubuntu-latest
timeout-minutes: 120
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 }}
- name: Enable corepack and install with win32-x64 artifacts
run: |
corepack enable
# Experiment-only install-time override: also materialize the
# win32-x64 platform packages (@esbuild/win32-x64, rolldown and
# rollup MSVC bindings) that the Windows toolchain resolves at
# runtime. supportedArchitectures is not recorded in the lockfile,
# so --frozen-lockfile stays valid.
cat >> pnpm-workspace.yaml <<'EOF'
supportedArchitectures:
os: [current, win32]
cpu: [current, x64]
EOF
pnpm install --frozen-lockfile
- name: Install Wine (64-bit)
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends wine64
WINE_BIN=$(command -v wine || command -v wine64)
echo "WINE_BIN=$WINE_BIN" >> "$GITHUB_ENV"
"$WINE_BIN" --version
- name: Fetch Windows Node.js
run: |
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"
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"
- name: Boot Wine prefix and smoke Windows Node
run: |
"$WINE_BIN" wineboot --init || true
wineserver -w || true
"$WINE_BIN" "$NODE_WIN" -p "'smoke: ' + process.platform + ' ' + process.arch + ' ' + process.version"
# The continue-on-error gates below mirror ci-windows-blocking
# (scripts/run-gates.ts): `build` = tsc -b + tsdown, `production site` =
# vitepress build. Each reports independently so one failure does not
# hide the others' results; the summary step at the end owns the job
# conclusion.
- name: 'Gate: tsc -b (Windows node under Wine)'
id: tsc
continue-on-error: true
timeout-minutes: 45
run: '"$WINE_BIN" "$NODE_WIN" node_modules/typescript/bin/tsc -b --pretty false'
- name: 'Gate: tsdown (Windows node under Wine)'
id: tsdown
continue-on-error: true
timeout-minutes: 30
run: '"$WINE_BIN" "$NODE_WIN" node_modules/tsdown/dist/run.mjs'
- name: 'Gate: production site (Windows node under Wine)'
id: site
continue-on-error: true
timeout-minutes: 30
working-directory: website
run: '"$WINE_BIN" "$NODE_WIN" node_modules/vitepress/bin/vitepress.js build .'
- name: Report gate outcomes
env:
TSC: ${{ steps.tsc.outcome }}
TSDOWN: ${{ steps.tsdown.outcome }}
SITE: ${{ steps.site.outcome }}
run: |
echo "tsc: $TSC"
echo "tsdown: $TSDOWN"
echo "production site: $SITE"
[ "$TSC" = success ] && [ "$TSDOWN" = success ] && [ "$SITE" = success ]