From c1153577378f271c1145f12f07185be591193fa2 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 27 Jul 2026 04:37:00 +0800 Subject: [PATCH] =?UTF-8?q?ci:=20experiment=20=E2=80=94=20Wine-run=20Windo?= =?UTF-8?q?ws=20blocking=20gates=20on=20a=20Linux=20runner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...27-wine-windows-gates-experiment.i18n.yaml | 6 + ...026-07-27-wine-windows-gates-experiment.md | 44 +++++++ ...-07-27-wine-windows-gates-experiment.zh.md | 44 +++++++ .github/workflows/exp-wine-windows.yml | 123 ++++++++++++++++++ 4 files changed, 217 insertions(+) create mode 100644 .agents/notes/proposed/process/2026-07-27-wine-windows-gates-experiment.i18n.yaml create mode 100644 .agents/notes/proposed/process/2026-07-27-wine-windows-gates-experiment.md create mode 100644 .agents/notes/proposed/process/2026-07-27-wine-windows-gates-experiment.zh.md create mode 100644 .github/workflows/exp-wine-windows.yml diff --git a/.agents/notes/proposed/process/2026-07-27-wine-windows-gates-experiment.i18n.yaml b/.agents/notes/proposed/process/2026-07-27-wine-windows-gates-experiment.i18n.yaml new file mode 100644 index 0000000000..eb3909cc4e --- /dev/null +++ b/.agents/notes/proposed/process/2026-07-27-wine-windows-gates-experiment.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 +2026-07-27-wine-windows-gates-experiment.md: 9f7856dfef229f8f02c85f5968082a0c857bbc94 +2026-07-27-wine-windows-gates-experiment.zh.md: cb185293d7f22723a96448a774bd27dd31e1bc28 diff --git a/.agents/notes/proposed/process/2026-07-27-wine-windows-gates-experiment.md b/.agents/notes/proposed/process/2026-07-27-wine-windows-gates-experiment.md new file mode 100644 index 0000000000..9f7856dfef --- /dev/null +++ b/.agents/notes/proposed/process/2026-07-27-wine-windows-gates-experiment.md @@ -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. diff --git a/.agents/notes/proposed/process/2026-07-27-wine-windows-gates-experiment.zh.md b/.agents/notes/proposed/process/2026-07-27-wine-windows-gates-experiment.zh.md new file mode 100644 index 0000000000..cb185293d7 --- /dev/null +++ b/.agents/notes/proposed/process/2026-07-27-wine-windows-gates-experiment.zh.md @@ -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 通道,抹掉成本论点;无论结果如何,运行都会记录数字。 diff --git a/.github/workflows/exp-wine-windows.yml b/.github/workflows/exp-wine-windows.yml new file mode 100644 index 0000000000..499429f807 --- /dev/null +++ b/.github/workflows/exp-wine-windows.yml @@ -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 ]