ci(windows): use ReFS block-clone installs on the self-hosted VM (#3342)
pnpm hardlinks node_modules files to the store on the same volume, and TypeScript's native realpath resolves those links back to store paths (F:/.pnpm-store/v11/files/...), producing TS6231 during tsc -b and vite resolution. ReFS block cloning (package-import-method=clone) gives each file an independent path while sharing physical blocks, avoiding the leak without the copy cost. Clone mode needs the @reflink/reflink native module, which the system corepack pnpm carries but pnpm/action-setup's dest build omits, so installs run through corepack pnpm. The install steps branch on the workspace filesystem: clone only on ReFS, plain install on hosted NTFS (which rejects copy-on-write). The serial-windows store points at F:\.pnpm-store to share the ReFS volume. Agent Note 2026-08-30-windows-refs-store-block-clone-install records the rationale; ci-workflow.spec asserts the branch.
This commit is contained in:
parent
0a53fb55be
commit
4032a0a428
12 changed files with 217 additions and 16 deletions
|
|
@ -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: bfed4e6e15311d0191c1379a5822b0daf46f4ed3
|
||||
2026-07-26-ci-failover-runbook.zh.md: 86007d5b189ccc883dc96d68bc9e54f38bb09e2a
|
||||
2026-07-26-ci-failover-runbook.md: 7579e6ca4da5207f3d308c7606edc6d885ab25c7
|
||||
2026-07-26-ci-failover-runbook.zh.md: eba74831572252ecbbde388e83458161cad0f696
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ The decision belongs at workflow level because cancellation applies to the whole
|
|||
|
||||
#### Windows pool
|
||||
|
||||
`dsh-win-ci`: 32 always-on runner instances (scheduled tasks `GH-Runner-01`…`GH-Runner-32`) on the in-house Windows CI server (one 96-core / 580 GB machine). Labels: `[self-hosted, dsh-win-ci, windows]`. The image must preinstall Node 24, pnpm, Git (with Git Bash on `PATH`, i.e. `C:\Program Files\Git\bin` — the `bash` tool spawns `bash` by name), PowerShell 7, and enable Developer Mode for symlink support. Check the latest `serial / windows (self-hosted standby)` run before switching: a green standby verifies the pool can execute `check:ci:windows-complete` end-to-end.
|
||||
`dsh-win-ci`: 32 always-on runner instances (scheduled tasks `GH-Runner-01`…`GH-Runner-32`) on the in-house Windows CI server (one 96-core / 580 GB machine). Labels: `[self-hosted, dsh-win-ci, windows]`. The image must preinstall Node 24, pnpm, Git (with Git Bash on `PATH`, i.e. `C:\Program Files\Git\bin` — the `bash` tool spawns `bash` by name), PowerShell 7, and enable Developer Mode for symlink support. The workspaces and the pnpm store must both live on a ReFS volume (`F:`): the Windows installs pass `--package-import-method=clone` on ReFS, which needs that volume layout and the `@reflink/reflink` native module that the system corepack pnpm carries (see [the Windows ReFS store note](2026-08-30-windows-refs-store-block-clone-install.md)); a rebuilt runner without this layout fails the Windows build gates with TS6231. Check the latest `serial / windows (self-hosted standby)` run before switching: a green standby verifies the pool can execute `check:ci:windows-complete` end-to-end.
|
||||
|
||||
### Switch (any repository writer, ~1 minute, no merge)
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ Status: implemented
|
|||
|
||||
#### Windows 池
|
||||
|
||||
`dsh-win-ci`:公司内部 Windows CI 服务器(一台 96 核 / 580 GB 机器)上 32 个常驻运行器实例(计划任务 `GH-Runner-01`…`GH-Runner-32`)。标签:`[self-hosted, dsh-win-ci, windows]`。镜像必须预装 Node 24、pnpm、Git(Git Bash 在 `PATH` 上,即 `C:\Program Files\Git\bin`——`bash` 工具按名称 spawn `bash`)、PowerShell 7,并为符号链接支持启用开发人员模式。切换前先看 `serial / windows (self-hosted standby)` 最近一次运行:绿色热备验证该池能端到端执行 `check:ci:windows-complete`。
|
||||
`dsh-win-ci`:公司内部 Windows CI 服务器(一台 96 核 / 580 GB 机器)上 32 个常驻运行器实例(计划任务 `GH-Runner-01`…`GH-Runner-32`)。标签:`[self-hosted, dsh-win-ci, windows]`。镜像必须预装 Node 24、pnpm、Git(Git Bash 在 `PATH` 上,即 `C:\Program Files\Git\bin`——`bash` 工具按名称 spawn `bash`)、PowerShell 7,并为符号链接支持启用开发人员模式。工作区与 pnpm store 必须都位于 ReFS 卷(`F:`)上:Windows 安装步骤在 ReFS 上传递 `--package-import-method=clone`,这需要该卷布局以及系统 corepack pnpm 携带的 `@reflink/reflink` 原生模块(见 [Windows ReFS store note](2026-08-30-windows-refs-store-block-clone-install.zh.md));没有此布局的重建运行器会在 Windows 构建门禁阶段以 TS6231 失败。切换前先看 `serial / windows (self-hosted standby)` 最近一次运行:绿色热备验证该池能端到端执行 `check:ci:windows-complete`。
|
||||
|
||||
### 切换步骤(任何具备写权限的协作者,约 1 分钟,无需合并)
|
||||
|
||||
|
|
|
|||
|
|
@ -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-pnpm-action-setup-for-symmetric-ci-caching.md
|
||||
2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.md: d485098f7ee04596e77322089fa0f6f45020024a
|
||||
2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.zh.md: 47bd525377d6c358891b238373410049987f5ee1
|
||||
2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.md: 2519b9e8bc565790acbd02f0a01491fcc136bbe6
|
||||
2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.zh.md: 2157541c4b503d4acd38073263b6b69050a239bc
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ Outside `landlock-run.yml`, each workflow that installed pnpm hand-provisioned i
|
|||
|
||||
## Decision
|
||||
|
||||
`pnpm/action-setup@v4` is the only pnpm provisioning mechanism in CI: no workflow runs `corepack enable`. The root dev dependency on `@yarnpkg/cli-dist` separately supplies the modern Yarn CLI exercised by the generated-project e2e; package-manager coverage therefore does not inherit the runner image's Yarn Classic. Caching remains per-job policy on top of pnpm provisioning, in three deliberate shapes:
|
||||
`pnpm/action-setup@v4` is the pnpm provisioning mechanism across CI: no workflow runs `corepack enable`. The self-hosted Windows install steps are the deliberate exception — they invoke `corepack pnpm` because clone-mode installs need the `@reflink/reflink` native module that the system corepack pnpm carries but `pnpm/action-setup`'s dest build omits (see [the Windows ReFS store note](2026-08-30-windows-refs-store-block-clone-install.md)). The root dev dependency on `@yarnpkg/cli-dist` separately supplies the modern Yarn CLI exercised by the generated-project e2e; package-manager coverage therefore does not inherit the runner image's Yarn Classic. Caching remains per-job policy on top of pnpm provisioning, in three deliberate shapes:
|
||||
|
||||
- **Symmetric cache** (restore and save): `actions/setup-node` with `cache: pnpm` — `e2e.yml`, `docs-pages.yml`, `pi-ai-provider-e2e.yml`, `build-exe-for-python-sdk.yml`, the node-compat job of `ci.yml`, and the two benchmark jobs of `ci-master.yml`. The larger-runner benchmark keeps its store cache Linux-only through a conditional `cache:` input; the consolidated benchmark caches on both platforms.
|
||||
- **Restore-only caching** (hand-rolled `actions/cache` steps): the three enterprise-runner PR jobs and the Wine-based required Windows job restore without saving, keeping cache compression/upload off their latency-sensitive paths — an asymmetry `setup-node`'s cache cannot express. Each configures a store outside the action's replaceable install directory and resolves that path. No master job produces these hosted caches, so these restores hit matching archived entries until they evict. The enterprise jobs skip restore during self-hosted failover because that VM's persistent store is already warm.
|
||||
|
|
@ -27,7 +27,7 @@ Outside `landlock-run.yml`, each workflow that installed pnpm hand-provisioned i
|
|||
|
||||
## Consequences
|
||||
|
||||
- The corepack dependency is gone from CI entirely; pnpm arrives via the pnpm team's official action everywhere, and the version pin stays single-sourced in `package.json`'s `packageManager` field.
|
||||
- The corepack dependency is gone from CI except the self-hosted Windows install steps, which invoke `corepack pnpm` for the ReFS block-clone native module; pnpm otherwise arrives via the pnpm team's official action, and the version pin stays single-sourced in `package.json`'s `packageManager` field.
|
||||
- The generated-project e2e runs the root-pinned Yarn 4 CLI instead of inheriting or silently skipping the runner image's Yarn version.
|
||||
- The cache-key format changed once for converted lanes; one cold run repopulated it, after which hit rates match the old steps. The built-in key spans platform, arch, and the lockfile hash but not the Node version, so the node-compat matrix legs share one store entry — safe, because the pnpm store is Node-version-independent.
|
||||
- `setup-node`'s built-in pnpm cache restores by exact key only, with no `restore-keys` prefix fallback: a `pnpm-lock.yaml` change starts a converted lane from a cold store instead of seeding from the previous entry.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ Status: implemented
|
|||
|
||||
## 决策
|
||||
|
||||
`pnpm/action-setup@v4` 是 CI 中提供 pnpm 的唯一机制:没有任何工作流运行 `corepack enable`。根目录的 `@yarnpkg/cli-dist` 开发依赖另行提供 generated-project e2e 所运行的现代 Yarn CLI(命令行界面);因此,用于包管理器覆盖率的 Yarn 不会沿用 runner 镜像里的 Yarn Classic。缓存仍是叠加在 pnpm 提供机制上的按作业策略,保留三种有意采用的形态:
|
||||
`pnpm/action-setup@v4` 是 CI 中提供 pnpm 的机制:没有任何工作流运行 `corepack enable`。自托管 Windows 安装步骤是刻意的例外——它们调用 `corepack pnpm`,因为 clone 模式安装需要系统 corepack pnpm 携带、而 `pnpm/action-setup` 的 dest 构建缺少的 `@reflink/reflink` 原生模块(见 [Windows ReFS store note](2026-08-30-windows-refs-store-block-clone-install.zh.md))。根目录的 `@yarnpkg/cli-dist` 开发依赖另行提供 generated-project e2e 所运行的现代 Yarn CLI(命令行界面);因此,用于包管理器覆盖率的 Yarn 不会沿用 runner 镜像里的 Yarn Classic。缓存仍是叠加在 pnpm 提供机制上的按作业策略,保留三种有意采用的形态:
|
||||
|
||||
- **对称缓存**(既恢复也保存):带 `cache: pnpm` 的 `actions/setup-node`——`e2e.yml`、`docs-pages.yml`、`pi-ai-provider-e2e.yml`、`build-exe-for-python-sdk.yml`、`ci.yml` 的 node-compat 作业,以及 `ci-master.yml` 的两个 benchmark 作业。larger-runner benchmark 通过条件化的 `cache:` 输入让 store 缓存仅限 Linux;consolidated benchmark 在两个平台上都启用缓存。
|
||||
- **只恢复不上传**(手写的 `actions/cache` 步骤):企业 runner 上的三个 PR(Pull Request)作业和基于 Wine 的必需 Windows 作业只恢复不保存,把缓存压缩/上传挡在它们的延迟敏感路径之外——这种不对称是 `setup-node` 的缓存无法表达的。每个作业都在 action 可替换的安装目录之外配置 store,并解析该路径。没有任何 master 作业生产这些 hosted 缓存,这些恢复步骤只能命中仍有归档的旧条目,直至其被逐出;企业作业在自托管故障切换期间跳过恢复,因为该 VM 的持久 store 已经预热。
|
||||
|
|
@ -27,7 +27,7 @@ Status: implemented
|
|||
|
||||
## 后果
|
||||
|
||||
- corepack 依赖已从 CI 中彻底消失;pnpm 在所有工作流中都经由 pnpm 团队的官方 action 提供,版本锁定继续单一来源于 `package.json` 的 `packageManager` 字段。
|
||||
- corepack 依赖已从 CI 中消失,唯独自托管 Windows 安装步骤例外——它们为 ReFS 块克隆原生模块调用 `corepack pnpm`;pnpm 在其他工作流中都经由 pnpm 团队的官方 action 提供,版本锁定继续单一来源于 `package.json` 的 `packageManager` 字段。
|
||||
- generated-project e2e 运行根目录锁定的 Yarn 4 CLI,既不再沿用 runner 镜像中的 Yarn 版本,也不会因此悄然跳过。
|
||||
- 已转换泳道的缓存键格式变更了一次;各跑一次冷运行重建缓存后,命中率与旧步骤持平。内建缓存键涵盖平台、架构与锁文件哈希,但不含 Node 版本,因此 node-compat 的各个矩阵任务共享同一条 store 缓存记录——这是安全的,因为 pnpm store 与 Node 版本无关。
|
||||
- `setup-node` 内建的 pnpm 缓存只按精确键恢复,没有 `restore-keys` 前缀回退:`pnpm-lock.yaml` 一旦变更,已转换泳道会从冷 store 起步,而不是利用上一条缓存记录预填充。
|
||||
|
|
|
|||
|
|
@ -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-08-30-windows-refs-store-block-clone-install.md
|
||||
2026-08-30-windows-refs-store-block-clone-install.md: 086c00453fffada7faef1631e7c270f3270b82d6
|
||||
2026-08-30-windows-refs-store-block-clone-install.zh.md: f813ae15a16c9f5cd61b1f7b98a66d74af2115e9
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
# Agent Note: Windows self-hosted ReFS store and block-clone installs
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-30-windows-refs-store-block-clone-install.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The self-hosted Windows VM's workspaces moved from the NTFS `E:` volume to the ReFS `F:` volume. `git clean -ffdx` on the NTFS volume deleted the ~70k-file node_modules tree in tens of minutes and forced a full reinstall on every run, driving disk writes past the volume's sustained bandwidth. ReFS metadata operations are orders of magnitude faster, so the workspace move restored fast checkout, but it exposed a second failure.
|
||||
|
||||
The pnpm store also lives on `F:` (`F:\.pnpm-store`), so pnpm links node_modules files to the store with hardlinks (its default `package-import-method=auto` on a same-volume layout). TypeScript resolves module files with the native realpath (`fs.realpathSync.native`), which on Windows resolves a hardlink to the store's content-addressed path (`F:/.pnpm-store/v11/files/<xx>/<sha256>`). The compiler then resolves bare imports from that store path, where no `node_modules` exists, and fails with TS6231 (`Could not resolve the path 'F:/.pnpm-store/...'`) during `tsc -b` and vite's module resolution. The JS `realpathSync` does not leak the store path; only the native variant does, so this only appears in compiler tooling.
|
||||
|
||||
A related install failure appears when `package-import-method=clone` runs on a volume that does not support copy-on-write: pnpm reports `ERR_PNPM_LINKING_FAILED ... Source volume does not support copy-on-write` on NTFS volumes (hosted runners).
|
||||
|
||||
The pnpm build that `pnpm/action-setup` installs into its `dest` omits the `@reflink/reflink` native module that clone mode requires, so even on ReFS, clone fails with `Cannot find module './reflink.win32-x64-msvc-*.node'`. The system corepack pnpm carries the complete `@reflink` platform set, including `reflink.win32-x64-msvc.node`.
|
||||
|
||||
## Decision
|
||||
|
||||
The Windows install steps in [ci.yml](../../../../.github/workflows/ci.yml) (the four pull-request native jobs) and [ci-master.yml](../../../../.github/workflows/ci-master.yml) (`serial-windows`) branch on the workspace filesystem, using clone only on ReFS:
|
||||
|
||||
```pwsh
|
||||
$drive = (Split-Path -Qualifier $env:GITHUB_WORKSPACE).TrimEnd(':')
|
||||
$fs = (Get-Volume -DriveLetter $drive).FileSystem
|
||||
if ($fs -eq 'ReFS') {
|
||||
corepack pnpm install --frozen-lockfile --package-import-method=clone
|
||||
} else {
|
||||
pnpm install --frozen-lockfile
|
||||
}
|
||||
```
|
||||
|
||||
- `--package-import-method=clone` on ReFS uses block cloning: each node_modules file gets an independent path (so native realpath cannot resolve it back to a store path, eliminating TS6231) while sharing physical blocks with the store (no copy cost). ReFS supports block cloning and hardlinks (verified with `fsutil fsinfo volumeinfo` and hardlink listing).
|
||||
- The flag is passed only when the workspace volume is ReFS. Hosted runners (NTFS, fresh VM per job) keep the default import method, because NTFS rejects block clone.
|
||||
- `corepack pnpm` is used because clone mode needs the `@reflink/reflink` native module, which the system corepack pnpm carries but `pnpm/action-setup`'s dest build omits.
|
||||
- `.npmrc` and `npm_config_*` environment variables do not drive `package-import-method` in pnpm 11.7.0 on Windows; only the CLI flag is honored, so the flag is explicit in the command.
|
||||
|
||||
The self-hosted VM's store lives on `F:\.pnpm-store` (ReFS, machine-level `PNPM_CONFIG_STORE_DIR`), and the workspaces live on `F:\ci\_work-NN`. The F: volume is 200 GB ReFS after rebuild. `DSH_CI_FAILOVER_WINDOWS=selfhosted` routes the four pull-request native jobs to the self-hosted pool.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Keep workspaces on NTFS `E:`** - rejected because `git clean -ffdx` deleted the node_modules tree in tens of minutes on NTFS, the original write-storm cause; ReFS reduced it to ~23 seconds.
|
||||
- **`--package-import-method=copy`** - avoids the store-path leak (files are independent copies) and needs no native module, but copies every file from the store on every install, restoring most of the write cost the workspace move removed.
|
||||
- **Fix the action-setup pnpm's reflink** - rejected because `pnpm/action-setup` installs a fresh pnpm into a per-job `dest` directory; adding the native module there is fragile and per-job.
|
||||
- **`.npmrc` `package-import-method=clone`** - rejected because pnpm 11.7.0 on Windows ignores it (verified: files remain hardlinks with `nlink=2` and native realpath still leaks the store path).
|
||||
|
||||
## Consequences
|
||||
|
||||
The self-hosted Windows installs use block cloning, giving independent file paths (no TS6231) with shared physical blocks (no copy). Hosted runners keep the default import method. The `serial-windows` standby drill and the pull-request native jobs on the self-hosted pool depend on the ReFS volume layout; a runner rebuilt from the [failover runbook](2026-07-26-ci-failover-runbook.md) without the ReFS store-and-workspace layout would fail the Windows build gates with TS6231 (or the install with reflink errors).
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
# Agent Note:Windows 自托管 ReFS store 与块克隆安装
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-30-windows-refs-store-block-clone-install.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
自托管 Windows 虚拟机的工作区从 NTFS 的 `E:` 卷迁到了 ReFS 的 `F:` 卷。在 NTFS 卷上,`git clean -ffdx` 删除约 7 万个文件的 node_modules 树需要几十分钟,并迫使每次运行全量重装,把磁盘写入推到该卷持续带宽以上。ReFS 的元数据操作快几个数量级,因此工作区迁移恢复了快速 checkout,但暴露了第二个失败。
|
||||
|
||||
pnpm store 也在 `F:` 上(`F:\.pnpm-store`),因此 pnpm 用硬链接把 node_modules 文件链接到 store(同卷布局下的默认 `package-import-method=auto`)。TypeScript 用原生 realpath(`fs.realpathSync.native`)解析模块文件,在 Windows 上会把硬链接解析到 store 的内容寻址路径(`F:/.pnpm-store/v11/files/<xx>/<sha256>`)。编译器随后从那个 store 路径解析裸导入,而那里没有 `node_modules`,于是在 `tsc -b` 和 vite 的模块解析期间以 TS6231(`Could not resolve the path 'F:/.pnpm-store/...'`)失败。JS 的 `realpathSync` 不泄漏 store 路径;只有原生变体会泄漏,所以这只出现在编译器工具链里。
|
||||
|
||||
当 `package-import-method=clone` 运行在不支持 copy-on-write 的卷上时,会出现相关的安装失败:pnpm 在 NTFS 卷(托管 runner)上报告 `ERR_PNPM_LINKING_FAILED ... Source volume does not support copy-on-write`。
|
||||
|
||||
`pnpm/action-setup` 装到其 `dest` 的 pnpm 构建缺少 clone 模式所需的 `@reflink/reflink` 原生模块,所以即使在 ReFS 上,clone 也会以 `Cannot find module './reflink.win32-x64-msvc-*.node'` 失败。系统 corepack pnpm 带有完整的 `@reflink` 平台集合,包括 `reflink.win32-x64-msvc.node`。
|
||||
|
||||
## Decision
|
||||
|
||||
[ci.yml](../../../../.github/workflows/ci.yml)(四个 pull-request 原生作业)和 [ci-master.yml](../../../../.github/workflows/ci-master.yml)(`serial-windows`)中的 Windows 安装步骤按工作区文件系统分支,仅在 ReFS 上使用 clone:
|
||||
|
||||
```pwsh
|
||||
$drive = (Split-Path -Qualifier $env:GITHUB_WORKSPACE).TrimEnd(':')
|
||||
$fs = (Get-Volume -DriveLetter $drive).FileSystem
|
||||
if ($fs -eq 'ReFS') {
|
||||
corepack pnpm install --frozen-lockfile --package-import-method=clone
|
||||
} else {
|
||||
pnpm install --frozen-lockfile
|
||||
}
|
||||
```
|
||||
|
||||
- ReFS 上的 `--package-import-method=clone` 使用块克隆:每个 node_modules 文件获得独立路径(因此原生 realpath 无法把它解析回 store 路径,消除了 TS6231),同时与 store 共享物理块(无复制代价)。ReFS 支持块克隆和硬链接(已用 `fsutil fsinfo volumeinfo` 和硬链接列表验证)。
|
||||
- 仅当工作区卷是 ReFS 时才传该 flag。托管 runner(NTFS,每个 job 全新 VM)保留默认导入方式,因为 NTFS 拒绝块克隆。
|
||||
- 使用 `corepack pnpm` 是因为 clone 模式需要 `@reflink/reflink` 原生模块,系统 corepack pnpm 带有它,而 `pnpm/action-setup` 的 dest 构建缺少。
|
||||
- `.npmrc` 与 `npm_config_*` 环境变量在 Windows 的 pnpm 11.7.0 上不驱动 `package-import-method`;只有 CLI flag 生效,因此命令中显式传 flag。
|
||||
|
||||
自托管虚拟机的 store 位于 `F:\.pnpm-store`(ReFS,机器级 `PNPM_CONFIG_STORE_DIR`),工作区位于 `F:\ci\_work-NN`。重建后 F: 卷为 200 GB ReFS。`DSH_CI_FAILOVER_WINDOWS=selfhosted` 把四个 pull-request 原生作业路由到自托管池。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **把工作区留在 NTFS 的 `E:`** - 不采纳,因为 NTFS 上 `git clean -ffdx` 删除 node_modules 树需要几十分钟,即最初的写风暴根因;ReFS 把它降到约 23 秒。
|
||||
- **`--package-import-method=copy`** - 避免 store 路径泄漏(文件是独立副本)且不需要原生模块,但每次安装都从 store 复制每个文件,恢复了工作区迁移移除的大部分写代价。
|
||||
- **修复 action-setup 的 pnpm 的 reflink** - 不采纳,因为 `pnpm/action-setup` 把全新 pnpm 装进每 job 的 `dest` 目录;在那里补原生模块脆弱且按 job 生效。
|
||||
- **`.npmrc` 的 `package-import-method=clone`** - 不采纳,因为 Windows 的 pnpm 11.7.0 忽略它(已验证:文件保持 `nlink=2` 的硬链接,原生 realpath 仍泄漏 store 路径)。
|
||||
|
||||
## Consequences
|
||||
|
||||
自托管 Windows 安装使用块克隆,既得到独立文件路径(无 TS6231),又共享物理块(无复制)。托管 runner 保留默认导入方式。`serial-windows` standby drill 与自托管池上的 pull-request 原生作业依赖 ReFS 卷布局;若按 [failover runbook](2026-07-26-ci-failover-runbook.zh.md) 重建 runner 而没有 ReFS store 与工作区布局,Windows 构建门禁会以 TS6231 失败(或安装阶段以 reflink 错误失败)。
|
||||
16
.github/workflows/ci-master.yml
vendored
16
.github/workflows/ci-master.yml
vendored
|
|
@ -184,13 +184,25 @@ jobs:
|
|||
|
||||
- name: Configure persistent pnpm store
|
||||
shell: pwsh
|
||||
# The store must share the ReFS workspace volume for the clone
|
||||
# import method below; LOCALAPPDATA (C:) would cross volumes and
|
||||
# break block clone. See 2026-08-30-windows-refs-store-block-clone-install.
|
||||
run: |
|
||||
$storeRoot = "$env:LOCALAPPDATA\pnpm\store"
|
||||
$storeRoot = "F:\.pnpm-store"
|
||||
echo "PNPM_CONFIG_STORE_DIR=$storeRoot" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Install (immutable)
|
||||
shell: pwsh
|
||||
run: pnpm install --frozen-lockfile
|
||||
# See 2026-08-30-windows-refs-store-block-clone-install for the
|
||||
# ReFS block-clone rationale; use clone only on ReFS.
|
||||
run: >-
|
||||
$drive = (Split-Path -Qualifier $env:GITHUB_WORKSPACE).TrimEnd(':');
|
||||
$fs = (Get-Volume -DriveLetter $drive).FileSystem;
|
||||
if ($fs -eq 'ReFS') {
|
||||
corepack pnpm install --frozen-lockfile --package-import-method=clone
|
||||
} else {
|
||||
pnpm install --frozen-lockfile
|
||||
}
|
||||
|
||||
- name: Run complete unsharded Windows gate inventory serially
|
||||
shell: pwsh
|
||||
|
|
|
|||
48
.github/workflows/ci.yml
vendored
48
.github/workflows/ci.yml
vendored
|
|
@ -442,7 +442,17 @@ jobs:
|
|||
node-version: ${{ env.PRIMARY_NODE_VERSION }}
|
||||
- name: Install (immutable)
|
||||
shell: pwsh
|
||||
run: pnpm install --frozen-lockfile
|
||||
# See 2026-08-30-windows-refs-store-block-clone-install for the
|
||||
# ReFS block-clone rationale; detect the workspace filesystem and
|
||||
# pass --package-import-method=clone only on ReFS.
|
||||
run: >-
|
||||
$drive = (Split-Path -Qualifier $env:GITHUB_WORKSPACE).TrimEnd(':');
|
||||
$fs = (Get-Volume -DriveLetter $drive).FileSystem;
|
||||
if ($fs -eq 'ReFS') {
|
||||
corepack pnpm install --frozen-lockfile --package-import-method=clone
|
||||
} else {
|
||||
pnpm install --frozen-lockfile
|
||||
}
|
||||
- name: Run blocking Windows builds
|
||||
shell: pwsh
|
||||
run: pnpm run check:ci:windows-blocking
|
||||
|
|
@ -491,7 +501,17 @@ jobs:
|
|||
node-version: ${{ env.PRIMARY_NODE_VERSION }}
|
||||
- name: Install (immutable)
|
||||
shell: pwsh
|
||||
run: pnpm install --frozen-lockfile
|
||||
# See 2026-08-30-windows-refs-store-block-clone-install for the
|
||||
# ReFS block-clone rationale; detect the workspace filesystem and
|
||||
# pass --package-import-method=clone only on ReFS.
|
||||
run: >-
|
||||
$drive = (Split-Path -Qualifier $env:GITHUB_WORKSPACE).TrimEnd(':');
|
||||
$fs = (Get-Volume -DriveLetter $drive).FileSystem;
|
||||
if ($fs -eq 'ReFS') {
|
||||
corepack pnpm install --frozen-lockfile --package-import-method=clone
|
||||
} else {
|
||||
pnpm install --frozen-lockfile
|
||||
}
|
||||
- name: Build before coverage
|
||||
shell: pwsh
|
||||
run: pnpm run build
|
||||
|
|
@ -534,7 +554,17 @@ jobs:
|
|||
node-version: ${{ env.PRIMARY_NODE_VERSION }}
|
||||
- name: Install (immutable)
|
||||
shell: pwsh
|
||||
run: pnpm install --frozen-lockfile
|
||||
# See 2026-08-30-windows-refs-store-block-clone-install for the
|
||||
# ReFS block-clone rationale; detect the workspace filesystem and
|
||||
# pass --package-import-method=clone only on ReFS.
|
||||
run: >-
|
||||
$drive = (Split-Path -Qualifier $env:GITHUB_WORKSPACE).TrimEnd(':');
|
||||
$fs = (Get-Volume -DriveLetter $drive).FileSystem;
|
||||
if ($fs -eq 'ReFS') {
|
||||
corepack pnpm install --frozen-lockfile --package-import-method=clone
|
||||
} else {
|
||||
pnpm install --frozen-lockfile
|
||||
}
|
||||
- name: Run Windows-specific native tests
|
||||
shell: pwsh
|
||||
run: >-
|
||||
|
|
@ -576,7 +606,17 @@ jobs:
|
|||
node-version: ${{ env.PRIMARY_NODE_VERSION }}
|
||||
- name: Install (immutable)
|
||||
shell: pwsh
|
||||
run: pnpm install --frozen-lockfile
|
||||
# See 2026-08-30-windows-refs-store-block-clone-install for the
|
||||
# ReFS block-clone rationale; detect the workspace filesystem and
|
||||
# pass --package-import-method=clone only on ReFS.
|
||||
run: >-
|
||||
$drive = (Split-Path -Qualifier $env:GITHUB_WORKSPACE).TrimEnd(':');
|
||||
$fs = (Get-Volume -DriveLetter $drive).FileSystem;
|
||||
if ($fs -eq 'ReFS') {
|
||||
corepack pnpm install --frozen-lockfile --package-import-method=clone
|
||||
} else {
|
||||
pnpm install --frozen-lockfile
|
||||
}
|
||||
- name: Run Windows observational gates
|
||||
shell: pwsh
|
||||
run: pnpm run check:ci:windows-observational
|
||||
|
|
|
|||
|
|
@ -117,6 +117,35 @@ describe('CI workflow', () => {
|
|||
))
|
||||
expect(buildCommands.map(step => step.run)).toContain('pnpm run check:ci:windows-blocking')
|
||||
|
||||
// The four native Windows installs branch on the workspace filesystem:
|
||||
// clone (ReFS block clone) only on ReFS, plain install elsewhere. This
|
||||
// keeps the TS6231 store-path leak (see the Windows ReFS store note) out
|
||||
// of the self-hosted pool without forcing clone onto hosted NTFS, which
|
||||
// rejects copy-on-write. The branch must stay, or a hosted fallback would
|
||||
// fail installs with ERR_PNPM_LINKING_FAILED.
|
||||
for (const [jobName, job] of [['windows-build', windowsBuild], ['windows-coverage', windowsCoverage], ['windows-native-tests', windowsNativeTests], ['windows-observational', windowsObservational]] as const) {
|
||||
const steps = job.steps as unknown[]
|
||||
const install = steps.find((step): step is Record<string, unknown> & { run: string } => (
|
||||
isRecord(step) && step.name === 'Install (immutable)' && typeof step.run === 'string'
|
||||
))
|
||||
expect(install, `${jobName} must define the filesystem-branched install`).toBeDefined()
|
||||
expect(install!.run).toContain("$fs -eq 'ReFS'")
|
||||
expect(install!.run).toContain('--package-import-method=clone')
|
||||
expect(install!.run).toContain('corepack pnpm install')
|
||||
// The else branch must keep the plain hosted install as a distinct line
|
||||
// (not the corepack clone line, which contains the same substring);
|
||||
// dropping it or making both branches clone would force clone onto
|
||||
// NTFS, which rejects copy-on-write (ERR_PNPM_LINKING_FAILED). The
|
||||
// YAML folded block keeps the first statement on line 1 and folds the
|
||||
// rest with leading two-space indents.
|
||||
const installLines = install!.run.split('\n').map(line => line.trim())
|
||||
expect(installLines).toContain('} else {')
|
||||
expect(installLines.some(line => line === 'pnpm install --frozen-lockfile'), `${jobName} else branch must keep the plain hosted install`).toBe(true)
|
||||
// The ReFS branch must not use the interpolated empty-flag form, which
|
||||
// passes a stray "" positional argument to pnpm.
|
||||
expect(install!.run).not.toContain('$cloneFlag')
|
||||
}
|
||||
|
||||
// windows-coverage uses the lower 4-partition profile.
|
||||
expect(windowsCoverage.name).toBe('windows node 24 / coverage')
|
||||
expect(windowsCoverage.env).toMatchObject({ DSH_COVERAGE_PARTITIONS: '4' })
|
||||
|
|
@ -150,6 +179,26 @@ describe('CI workflow', () => {
|
|||
expect(serialWindows.if).toBe("github.event_name == 'push' && github.ref == 'refs/heads/master'")
|
||||
expect(serialWindows['runs-on']).toEqual(['self-hosted', 'dsh-win-ci', 'windows'])
|
||||
expect(serialWindows.name).toBe('serial / windows (self-hosted standby)')
|
||||
// Its store must share the ReFS workspace volume for clone; the install
|
||||
// must carry the same filesystem branch as the PR jobs.
|
||||
const serialSteps = serialWindows.steps as unknown[]
|
||||
const serialStore = serialSteps.find((step): step is Record<string, unknown> & { run: string } => (
|
||||
isRecord(step) && step.name === 'Configure persistent pnpm store' && typeof step.run === 'string'
|
||||
))
|
||||
expect(serialStore).toBeDefined()
|
||||
expect(serialStore!.run).toContain('F:\\.pnpm-store')
|
||||
const serialInstall = serialSteps.find((step): step is Record<string, unknown> & { run: string } => (
|
||||
isRecord(step) && step.name === 'Install (immutable)' && typeof step.run === 'string'
|
||||
))
|
||||
expect(serialInstall).toBeDefined()
|
||||
expect(serialInstall!.run).toContain("$fs -eq 'ReFS'")
|
||||
expect(serialInstall!.run).toContain('--package-import-method=clone')
|
||||
expect(serialInstall!.run).toContain('corepack pnpm install')
|
||||
// Distinct else-branch line, as for the PR jobs: the corepack clone line
|
||||
// contains the plain-install substring too.
|
||||
expect(serialInstall!.run.split('\n').map(line => line.trim())).toContain('} else {')
|
||||
expect(serialInstall!.run.split('\n').map(line => line.trim())).toContain('pnpm install --frozen-lockfile')
|
||||
expect(serialInstall!.run).not.toContain('$cloneFlag')
|
||||
|
||||
// Aggregate: Wine and the required split native jobs are needed;
|
||||
// windows-coverage is temporarily non-blocking while Windows ACP
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue