已公开发布的 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。
213 lines
10 KiB
YAML
213 lines
10 KiB
YAML
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
|