Files
nodeagent/.github/workflows/windows-e2e.yml
T
jkooe f558e3e7df chore(release): 交付物架构断言 + CHANGELOG 归置 v2.0.2
已公开发布的 nodeagent-win-arm64.zip 内装的其实是 x64 的 node.exe
(两包内 node.exe 字节完全相同,PE Machine 均为 0x8664),该包在 ARM Windows 上
能启动、能 listen,却 TLS 握手不完成 —— 被控端实际不可用,而 macOS 侧 100% 复现不出。

- 新增 scripts/lib/zip-pe-arch.mjs:纯内置 zlib 手解 zip、读 PE 头判架构。
  不依赖 unzip / Expand-Archive(Windows runner 上没有 unzip)。
  含 CLI 自检入口,已注入故障实测会红(arm64 包去要 x64 期望 → 退出码 1)。
- release.mjs:生成资产前调用之,不符即中止发版 —— 拦在 tag/push 之前,
  是「交付物本身」的断言,与 pack.mjs 的「流程内部」断言分两层。
- windows-e2e.yml:把原先内联的 PowerShell 校验实现改为复用同一模块,
  消除两套实现各自漂移的可能。
- CHANGELOG:[未发布] 归入 v2.0.2。
2026-10-11 10:17:30 +08:00

213 lines
10 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Windows E2E
on:
push:
branches: [main]
paths:
- 'apps/**'
- 'packages/**'
- 'tests/**'
- 'scripts/**'
- 'package.json'
- 'pnpm-lock.yaml'
- '.github/workflows/windows-e2e.yml'
pull_request:
branches: [main]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
windows-e2e:
name: Windows 真机验证
runs-on: windows-latest
timeout-minutes: 30
steps:
- name: 检出代码
uses: actions/checkout@v5
- name: 安装 pnpm
uses: pnpm/action-setup@v4
with:
version: 12
- name: 安装 Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: 安装依赖
run: pnpm install --frozen-lockfile
- name: 构建
run: pnpm -r build
- name: 类型检查
run: pnpm typecheck
# 控制台「随包分发的 Node 运行时」在 Windows 上的分支此前零覆盖
# (准备脚本本机只在 macOS 上跑过)。此处真跑:复制 node.exe(无 strip 分支)→
# 自检可执行 → 再验证它与 sidecar 能否走通一次 JSON-RPC 自举。
# ⚠️ 必须先 sidecar:build —— prepare-console.mjs 的前置检查要求 sidecar.mjs 存在
# (首次推送时漏了这步,CI 在 Windows 上当场报「未找到 sidecar/dist/sidecar.mjs」)。
- name: 控制台 Node 运行时(Windows 分支)与自举验证
run: |
pnpm --filter nodeagent-console sidecar:build
pnpm --filter nodeagent-console prepare:node
node apps/console/scripts/verify-bundle.mjs --runtime
- name: 单元测试 — 协议纯函数 / 清单守护 / 脚本生成
# 来自 ci/workflow-unit-tests 分支(4d73d6e)的实质贡献;该分支基于老 main,
# 直接 merge 会回退上万行,故只取其提交内容手工并入。
run: node --test "tests/unit/**/*.test.mjs"
- name: E2E — 协议与跨平台能力
run: node tests/e2e/run.mjs
- name: E2E — Windows 专属能力(服务 / 软件 / winget)
run: node tests/e2e/windows.mjs
# ⚠️ 这一步刻意用 **powershell(Windows PowerShell 5.1)** 而非 pwsh。
# 5.1 读无 BOM 的 .ps1 会按 ANSI/GBK 解析,中文注释会破坏字符串边界 →
# 整份脚本 ParseError(真机 2026-10-04 踩过:install.ps1 报「","后面缺少表达式」)。
# 之前的冒烟全用 pwsh(7+ 认无 BOM UTF-8),所以 CI 绿、真机炸。
# 这里只做「解析 + 首行 help」检查:5.1 能否读懂,就是真机的核心约束。
# ⚠️ 必须用 shell: powershell(runner 上的 powershell 即 Windows PowerShell 5.1)。
# 不要用 shell: cmd 再套 powershell -Command "..." —— 多行带引号经 cmd 转义会全乱
# (实测:Missing closing '}' / '$errs' is not recognized,是步骤自身坏,不是脚本坏)。
- name: 冒烟 — 脚本可被 Windows PowerShell 5.1 解析(BOM 守卫)
shell: powershell
run: |
$ErrorActionPreference = 'Stop'
foreach ($f in @('scripts\install.ps1', 'scripts\uninstall.ps1', 'scripts\rescue-network.ps1')) {
$errs = $null
[void][System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $f).Path, [ref]$null, [ref]$errs)
if ($errs -and $errs.Count -gt 0) { throw ('5.1 parse FAILED: ' + $f + ' -> ' + $errs[0].Message) }
Write-Host ('OK 5.1 parse: ' + $f)
}
- name: 冒烟 — install.ps1 安装链路
shell: powershell
run: |
$ErrorActionPreference = "Stop"
./scripts/install.ps1 -Port 18769 -NodeId ci_win
$task = Get-ScheduledTask -TaskName "nodeagent" -ErrorAction Stop
Write-Host "Scheduled task state: $($task.State)"
$cfg = Get-Content "$env:USERPROFILE\.nodeagent\agent.json" -Raw | ConvertFrom-Json
if ($cfg.port -ne 18769) { throw "port mismatch: $($cfg.port)" }
if (-not $cfg.key) { throw "PSK missing in agent.json" }
if (-not $cfg.tls) { throw "TLS should be on by default" }
$rule = Get-NetFirewallRule -DisplayName "nodeagent (TCP 18769)" -ErrorAction Stop
if (-not $rule) { throw "Firewall rule" }
Write-Host "OK scheduled task / config / firewallall in place"
# 一键入口冒烟:真跑 install.cmd / control.cmd。
# 目的不是「再装一遍」,而是证明双击路径可用 —— 这些脚本是用户唯一的
# 上手入口,语法或编码一旦坏了,只有真机 cmd.exe 才测得出来。
# 端口用 18770 避开前一步的 18770 之外的安装,便于分辨是哪一步装的。
- name: 冒烟 — install.cmd 双击链路(含自提权与 PSK 读取)
shell: cmd
run: call scripts\install.cmd 18770
env:
CI: "true"
- name: 冒烟 — control.cmd 状态查询
shell: cmd
run: scripts\control.cmd 18770 < nul
env:
CI: "true"
- name: 冒烟 — 产物包结构与编码
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
pnpm pack:win
$zip = "release/nodeagent-win-x64.zip"
if (-not (Test-Path $zip)) { throw "zip not produced: $zip" }
Add-Type -AssemblyName System.IO.Compression.FileSystem
$z = [IO.Compression.ZipFile]::OpenRead((Resolve-Path $zip))
try {
$names = $z.Entries.Name
# 必须随包分发
foreach ($need in @('install.cmd','control.cmd','install.ps1','control.ps1','README.txt','agent.mjs','node.exe')) {
if ($names -notcontains $need) { throw "missing in package: $need" }
}
# 必须**不**在包内:PSK.txt 是现生成给本机的,打进公开资产等于泄露密钥
# (eaca9e0 起已从 zip 剔除;此处反向断言守住该性质)
foreach ($forbidden in @('PSK.txt','*.key','*.pem','certs')) {
if ($names -contains $forbidden) { throw "package must NOT contain: $forbidden" }
}
if ($names -match 'PSK\.txt') { throw "package leaks PSK.txt" }
foreach ($e in $z.Entries) {
$s = $e.Open(); $buf = New-Object byte[] 3
[void]$s.Read($buf, 0, 3); $s.Close()
$hasBom = ($buf[0] -eq 0xEF -and $buf[1] -eq 0xBB -and $buf[2] -eq 0xBF)
if ($e.Name -like '*.ps1' -and -not $hasBom) { throw "$($e.Name) lost its BOM in package" }
if ($e.Name -like '*.cmd' -and $hasBom) { throw "$($e.Name) must not have a BOM" }
}
Write-Host "OK - package complete; .ps1 has BOM / .cmd has none; no key shipped"
} finally { $z.Dispose() }
- name: 冒烟 — 双架构包(node.exe PE 架构校验)
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
# 真机教训(Parallels 27 + Win11 ARM):曾出现 arm64 包里装的其实是 x64 的 node.exe
# —— 两份包的 node.exe 字节完全相同。x64 node 在 ARM Windows 上**能 listen,
# 但 TLS 握手不完成**(客户端只见超时)→ 被控端实际不可用,且 macOS 侧 100% 测不出。
# 根因是 pack.mjs 的运行时缓存文件名不含架构。这里直接验**最终产物**的 PE 架构,
# 比只验脚本内部逻辑更靠近交付物(缓存串包 / 手工塞错文件都会被这一步拦下)。
# 校验实现与发版脚本共用(scripts/lib/zip-pe-arch.mjs)—— 避免两套实现各自漂移。
foreach ($arch in @('x64','arm64')) {
node scripts/pack.mjs --arch $arch
}
foreach ($arch in @('x64','arm64')) {
node scripts/lib/zip-pe-arch.mjs "release/nodeagent-win-$arch.zip" $arch
if ($LASTEXITCODE -ne 0) { throw "node.exe 架构校验失败($arch)—— 严禁发布该包" }
}
- name: 冒烟 — 被控端监听
shell: pwsh
run: |
Start-Process -FilePath "node" -ArgumentList "apps/agent/dist/index.js" -NoNewWindow
Start-Sleep -Seconds 4
$ok = (Test-NetConnection 127.0.0.1 -Port 18769 -WarningAction SilentlyContinue).TcpTestSucceeded
if (-not $ok) { throw "agent 未监听 18769" }
Write-Host "agent 已在 18769 监听"
- name: 冒烟 — CLI 连入(wss + 自签证书 + HMAC 鉴权)
shell: pwsh
run: |
$cfg = Get-Content "$env:USERPROFILE\.nodeagent\agent.json" -Raw | ConvertFrom-Json
node apps/cli/dist/index.js connect 127.0.0.1 --port 18769 --key $cfg.key --insecure
Write-Host "--- status ---"
node apps/cli/dist/index.js status
Write-Host "--- services ---"
node apps/cli/dist/index.js services --limit 5
- name: 冒烟 — MCP 工具列表
shell: pwsh
run: |
$cfg = Get-Content "$env:USERPROFILE\.nodeagent\agent.json" -Raw | ConvertFrom-Json
@{ client_id="ci"; host="127.0.0.1"; port=18769; tls=$true; insecure=$true; key=$cfg.key } |
ConvertTo-Json | Set-Content "$env:USERPROFILE\.nodeagent\config.json" -Encoding UTF8
$req = @(
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"ci","version":"1.0"}}}',
'{"jsonrpc":"2.0","method":"notifications/initialized"}',
'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
) -join "`n"
($req + "`n") | node apps/mcp/dist/index.js 2>$null | Select-String "na_status"
- name: 清理
if: always()
# 清理失败不掩盖真实结论(前置挂了会连带这里报错)
continue-on-error: true
shell: powershell
run: |
Get-Process node -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
./scripts/uninstall.ps1 -Port 18769 -Purge