build(scripts): 发布护栏 fail-closed 与 flag 语义;文档对齐

- release.cjs:assertNoDowngrade 的通道探测原先对网络错误/5xx/解析失败
  一律放行(fail-open),旧分支发旧版本号撞上探测失败会覆盖 latest.yml
  并让 pruneChannel 删掉线上更新版本的 exe。现仅通道真空(404)放行,
  其余抛错中止发布;prune 前的探测保持 best-effort
- 新增 --skip-gitea-release(只跳 Gitea release、通道照常上传);
  --skip-gitea 保持原行为但打印醒目警告;usage 补全 flag 语义
- 文档对齐:STATE.md(v1.0.23、19→22 离线测试、清除 MSI 描述、bundle
  清单补全)、ROADMAP 补 M12/M13、ci.yml 与 AGENTS.md 测试计数、
  AGENTS.md 补录本轮关键不变式(legacy 模板门控、SESSION_STATE/
  pendingOpens、zmodem 背压钩子、主机密钥 TTL);.gitignore 清重复与
  死条目
This commit is contained in:
Bill committed 2026-10-09 11:31:25 +08:00
1 parent 6a218ec6c0
commit 64982993f3
6 files changed
+82 -28

No files matched your search

+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
- name: Install dependencies
run: npm ci
# npm test 会先自动跑 pretest(typecheck),再重建 esbuild bundle 并跑 13 个离线测试
# npm test 会先自动跑 pretest(typecheck),再重建 esbuild bundle 并跑 19 个离线测试
- name: Test
run: npm test
-3
View File
@@ -4,9 +4,7 @@ node_modules/
# build output
out/
dist/
tests/.session-e2e.cjs
tests/.sftp-svc.*
tests/.hl-smoke.*
# research artifacts (XTerminal reverse-engineering notes, not part of the app)
research/
@@ -52,7 +50,6 @@ release/
# stray local test artifacts
.min-test.bin
.min2-test.bin
.exact-test.bin
tests/.zm-src-*/
tests/.zm-out-*/
+8 -3
View File
@@ -7,10 +7,10 @@ Electron + electron-vite + React 终端工具(本地终端 / SSH / SFTP)。
- 开发:`npm run dev`(主进程改动不热重建,需重启)
- dev 实例使用独立用户数据目录 `%APPDATA%\OpenTerminal-dev` 与独立单实例锁(`src/main/index.ts` 顶部 `!app.isPackaged` 分支),窗口标题带 `(dev)`:**可与已安装的正式版同时运行,互不干扰**,也不会把测试设置/会话写进真实配置
- 类型检查:`npm run typecheck`(tsconfig.node.json + tsconfig.web.json;只看渲染层可单跑 `npx tsc --noEmit -p tsconfig.web.json`)
- 测试:`npm test`(**npm 生命周期先自动跑 `pretest` 做类型检查**,再 `node tests/build-bundles.cjs` 重建 esbuild bundle,然后依次跑可离线运行的 18 个测试:ssh-loopback、commands-store、connections-store、settings-store、local-path-grants、lock-store、lock-controller、lock-shortcuts、hl-split-smoke、hl-rules、reserved-accelerators、ipc-guard、updater-fallback、log-sanitizer、sftp-timeout、terminal-title、zmodem-e2e、ssh-session-e2e、sysinfo-e2e;真实服务器测试需 JD_* 凭据,不在此列)
- 测试:`npm test`(**npm 生命周期先自动跑 `pretest` 做类型检查**,再 `node tests/build-bundles.cjs` 重建 esbuild bundle,然后依次跑可离线运行的 22 个测试:ssh-loopback、commands-store、connections-store、settings-store、local-path-grants、lock-store、lock-controller、lock-shortcuts、hl-split-smoke、hl-rules、reserved-accelerators、ipc-guard、updater-fallback、log-sanitizer、sftp-timeout、terminal-title、zmodem-e2e、ssh-session-e2e、sysinfo-e2e、terminal-cwd、terminal-links、broadcast-store;真实服务器测试需 JD_* 凭据,不在此列)。后三个渲染层纯函数测试用 Node 22.18+ 的原生 TS 类型剥离**直接 import `.ts` 源文件**(不经 esbuild bundle),Node 20/22.17 以下会 `ERR_UNKNOWN_FILE_EXTENSION`
- 依赖分类规则:**只有 `src/main/`/`src/preload/` 实际 import 的包才能进 `dependencies`**(node-pty/ssh2/zmodem.js/font-list/electron-updater);纯渲染层依赖一律 devDependencies(Vite 全量打包进 out/renderer,`externalizeDepsPlugin` 不作用渲染层)——这条让 asar 从 98MB 瘦到 8.1MB,加新依赖时别放错边
- 下载量统计:`node scripts/download-stats.cjs`(Gitea + GitHub release 资产的 download_count 汇总;GitHub 优先直连、失败自动回退 `HTTPS_PROXY`/本地 7897;更新通道无计数接口不计入)
- 打包:`npm run dist`(**生命周期先自动跑 `predist` → `npm test`,即类型检查 + 18 个离线测试全部通过后才 build/package**,typecheck 全程只跑一次;predist 末尾的 `npm install --package-lock-only` 会把 `package-lock.json` 根版本号对齐 `package.json`,**发布提交必须带上 package-lock.json**),产物在 `release/`(exe + latest.yml + blockmap)
- 打包:`npm run dist`(**生命周期先自动跑 `predist` → `npm test`,即类型检查 + 22 个离线测试全部通过后才 build/package**,typecheck 全程只跑一次;predist 末尾的 `npm install --package-lock-only` 会把 `package-lock.json` 根版本号对齐 `package.json`,**发布提交必须带上 package-lock.json**),产物在 `release/`(exe + latest.yml + blockmap)
- GitHub Actions:`.github/workflows/ci.yml` 在 windows-latest + Node 22 上跑 `npm ci` / `npm test`(含 pretest typecheck)/ `npm run build`,只做验证,不打包安装器、不发布
- 国内网络需镜像:`ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ npm run dist`
- 依赖一致性闸门:`predist` 开头跑 `node scripts/verify-deps.cjs`(逐条比对 `node_modules` 与 `package-lock.json` 的版本,跨平台 optional 依赖缺失跳过;不一致就 exit 1)——`npm install --package-lock-only` **只重算 lockfile、不碰 node_modules**(本版 npm 还会把 `node_modules/.package-lock.json` 一并重写成理想树,制造「已经装好了」的假象),所以**改动依赖版本后必须真实 `npm install` 或 `npm ci` 再构建**,别指望 lockfile 对齐就等于树里换了包;v1.0.21 的中文输入法回归正是这个坑(`@xterm/xterm` 锁 6.1.0-beta.304,树里还是 6.0.0,打进去的是旧代码)
@@ -37,8 +37,9 @@ Electron + electron-vite + React 终端工具(本地终端 / SSH / SFTP)。
- Gitea:release(exe 资产)→ Gitea 更新通道(`api/packages/admin/generic/openterminal-update/stable`:exe.blockmap → exe → release-notes.md → **latest.yml 最后**)
- GitHub:release 资产再次随版本同步发布(exe + exe.blockmap + latest.yml),electron-updater 标准 GitHub provider 直接吃 release 资产
- 通道不再先删旧版:新版本文件全部传完、latest.yml 生效后才清掉上一版 exe/blockmap,中途失败不会把通道打空;同一版本可重复运行(release 复用、已传资产跳过)
- 上传前先比通道版本:`assertNoDowngrade()` 读通道 latest.yml,若线上版本**高于**待发布版本就直接拒绝——三条本地护栏只比本地产物,旧分支发旧版本号会一路通过,覆盖 latest.yml 之后 `pruneChannel` 会把线上新版本的 exe/blockmap 删掉
- 上传前先比通道版本:`assertNoDowngrade()` 读通道 latest.yml,若线上版本**高于**待发布版本就直接拒绝——三条本地护栏只比本地产物,旧分支发旧版本号会一路通过,覆盖 latest.yml 之后 `pruneChannel` 会把线上新版本的 exe/blockmap 删掉。该探测**fail-closed**:网络错误 / 非 404 失败 / 解析不出 version 一律抛错中止发布,只有通道真空(404)才放行;`pruneChannel` 前那次探测是 best-effort(失败只跳过清理并打日志)
- 只补通道:`node scripts/release.cjs <版本号> --channel-only`(不建 release、不发 GitHub)
- 跳过部分目标:`--skip-github` 只跳 GitHub release;`--skip-gitea-release` **只跳 Gitea release、更新通道照常上传**;`--skip-gitea` 两者都跳(会打印醒目警告——国内用户将收不到该版本,只想跳 release 请用 `--skip-gitea-release`)
- 国内通道全程直连,**不需要设代理**;GitHub 请求走 `HTTPS_PROXY=http://127.0.0.1:7897`(脚本只把它用于 GitHub 请求)
5. 验证更新通道:`curl https://git.codingplan.site/api/packages/admin/generic/openterminal-update/stable/latest.yml` 应返回新版本号
6. `git tag v<版本号>` 并推送两个远程(代码/tag 与 release 资产的镜像保持同步)。注意顺序坑:release.cjs 创建 release 时若远端尚无该 tag,Gitea/GitHub 会在**默认分支 HEAD** 自动建一个指向错误 commit 的 tag,第 6 步推送会被拒——要么先建 tag 推上去再跑 release.cjs,要么事后 `git push -f <远端> v<版本号>` 强制修正到 release commit(v1.0.22 即踩过)
@@ -72,6 +73,8 @@ Electron + electron-vite + React 终端工具(本地终端 / SSH / SFTP)。
- `keyPath`(SSH 私钥):realpath → stat → 普通文件且 ≤1MB 才读(防设备文件永久阻塞 UI 线程/符号链接逃逸)
- `src/shared/reservedAccelerators.ts`:设置页录制器与主进程 `applyGlobalShortcut` **共用同一张保留键表**(Ctrl+L、Ctrl+=/-/0/PgUp/PgDn),两处分表曾漂移出洞,加新全局快捷键时两边自动一致
- SFTP 操作有 per-op 超时(`sftp.ts` 的 `bounded()`:元数据 30s / 传输块 60s / open 10s),超时按 transport 错误驱逐半死通道并重试一次;`setSftpTimeouts`/`setUpdateTimeouts` 是**测试缝**,生产无调用者,别接设置项
- **PTY_EXIT 是一次性广播、不做重放**:绑定晚于退出的窗格靠 `SESSION_STATE` 查询通道(`sessionState()` + 退出码登记表 `sessionExits`,上限 512 条 FIFO)补偿,`TerminalView` 订阅完成后查一次。同理,SSH **飞行中连接**(`connectSsh` 未返回)登记在 `pendingOpens`(带 owner 与 `onClient` 最早引用),`killPtysByOwner`/`killAllPtys` 靠它中止握手,否则 renderer 崩溃后会留下无主孤儿会话
- **zmodem 接收路径的背压靠 `pauseSource`/`resumeSource` 钩子**(`pty.ts` 注入,暂停喂入 sentry 的 ssh 流):fs WriteStream 缓冲无上限,慢盘 + 快对端会 OOM;`touchActivity` 只在写入被接受或 drain 恢复时计数,否则 stall 看门狗会把「堆在缓冲里」当进展。删除路径的 ENOENT 一律视为已删成功(`withSftp` 会整函数重跑,非幂等操作必须自己容忍「目标已消失」)
- `webPreferences` 显式写死 `contextIsolation: true / nodeIntegration: false / webSecurity: true`(`index.ts` 唯一窗口创建点),防默认值被将来改动
- 终端标签自动标题(「终端 N」)是**存储的显示文本**(进布局模板/会话快照/广播注册),`src/shared/terminalTitle.ts` 负责两个方向:反解用**全部 4 语言**的 pattern(任何语言生成的都能认出编号),渲染用当前语言;语言切换/快照恢复/模板应用时 `retitleAutoTitles` 原地重渲染(SSH 面板标题是用户起的连接名,永不动)。需要指定语言渲染时用 i18n 的 `tFor(lang, key, vars)`
@@ -94,6 +97,8 @@ Electron + electron-vite + React 终端工具(本地终端 / SSH / SFTP)。
- 应用模板(`Workspace.handleApplyTemplate`)读的是**外来 JSON**。`fromJSON` 一旦中途失败(别的版本写的模板、面板组件已不存在),dockview 会**先把目标 dockview 清空再抛错**(`failed to deserialize layout. Reverting changes`)。清空是逐面板走 `onDidRemovePanel` 的,所以**旧会话在抛错之前就已经被 `killSession` 杀掉了**——恢复出来的面板接不回它们,必须换新会话
- 失败路径:应用前 `toJSON()` 快照两个 dockview → 抛错时只对**这次真的调用过 `fromJSON` 的** dockview 回灌快照(回灌本身会清空该 dockview;把快照灌进没被碰过的那个会连带杀掉它活着的会话)→ 对恢复出来的面板跑 `rebindRestoredPanels` → 再把「没有任何面板引用的 `previousSessions`」kill 掉 → `message.error`。`JSON.parse` 失败同样要提示,不能静默 return
- 会话计数不靠累加器:`releaseSession` 直接扫 `api.panels` 判断还有没有面板在显示该会话——累加器与「`updateParameters` 原地换会话」「整块布局替换」这类无事件变化脱节,会漏杀或误杀
- **旧版(M6.1 前单 dockview)模板只喂 terminal dockview**:成功路径的 rebind 与收尾 kill 都必须按 `sshTouched` / `live` 集合门控——rebind 会把面板参数改写成新**本地**会话,对没被 fromJSON 触碰的 ssh dockview 跑一遍等于把活 SSH 面板降级成终端再杀会话
- 主机密钥队列只渲染队头,且主进程 `DEFAULT_TIMEOUTS.prompt`(120s)超时后**不再广播**:渲染层用同 TTL 的定时清扫(`HOST_KEY_PROMPT_TTL_MS`)移除过期条目,否则陈旧弹窗永远占住队头挡住后续主机;两处 TTL 必须同步改。`respondHostKey` 等 IPC 调用**不得写进 setState 的 updater**(React 可能重放 updater)
## 关键词高亮
+2
View File
@@ -18,6 +18,8 @@
| M9 | 体验打磨:系统托盘 + 关闭行为 + 单实例 + 主题联动标题栏 + 布局菜单重做 + 补全交互修正 + 应用图标 | ✅ 已完成 |
| M10 | 多语言界面(zh-CN / zh-TW / en / ja)+ 离线多语言更新日志 + 快捷键录制 + 可配终端工具条 + 设置健壮性 | ✅ 已完成(v1.0.13) |
| M11 | 审查修复第二轮:渲染层性能 + asar 瘦身 + 安全纵深 + 测试补全 + 文档刷新 | ✅ 已完成(2026-10,Dev_20261001) |
| M12 | 锁屏:主窗口遮罩 + scrypt 密码 + Ctrl+L 立即锁屏 + 冷却阶梯 + 落盘状态 | ✅ 已完成(v1.0.17–1.0.20;v1.0.18 修输入回归,v1.0.20 摘除菜单加固) |
| M13 | 更新源改为 GitHub 优先 + Gitea 通道兜底;高亮引擎性能优化 | ✅ 已完成(v1.0.19–1.0.20) |
## M2 — SSH 远程会话 ✅
+19 -9
View File
@@ -4,7 +4,7 @@
## 当前版本与仓库
- v1.0.20,远程 `git.codingplan.site/admin/OpenTerminal.git`(国内仓)+ `github.com/billowliu2/OpenTerminal.git`(GitHub 镜像仓);凭据存于 `.env`(已 git 忽略),凭据助手按 host 自动读取
- v1.0.23,远程 `git.codingplan.site/admin/OpenTerminal.git`(国内仓)+ `github.com/billowliu2/OpenTerminal.git`(GitHub 镜像仓);凭据存于 `.env`(已 git 忽略),凭据助手按 host 自动读取
- 开源协议:MIT(LICENSE)
- 更新通道 = `https://git.codingplan.site/api/packages/admin/generic/openterminal-update/stable/`(公网可读,含 latest.yml/exe/blockmap)
- 技术栈:Electron + electron-vite + React 19 + TS strict + antd 6(全局深色)+ zustand + dockview-react 8 + @xterm/xterm 6 + @lydell/node-pty + ssh2 + zmodem.js + electron-updater + electron-builder
@@ -56,15 +56,16 @@
- **广播输入**:渲染层 zustand(broadcastStore)维护 enabled/targets/sessions;TerminalView 全部写路径(onData/补全/粘贴/Workspace runCommand)走 writeBroadcast 扇出;目标 <2 自动禁用;tab 目标圆点 + 按钮计数徽标
- **ZMODEM**:主进程引擎(src/main/zmodem.ts,仅 SSH 会话;本地 pty 走 ConPTY 只给 UTF-8 字符串,二进制会损坏——不支持,注释已说明);Sentry 常驻分流非 zmodem 字节;offer→渲染层选文件/目录→respond;传输期抑制 PTY_DATA/replay/日志并丢弃用户键入;offer 120s/传输 90s 看门狗;进度复用 TransferPanel(kind=zmodem-upload/download);测试 tests/zmodem-e2e.mjs 用第二个 zmodem.js Sentry 模拟远端,双向内容一致性断言
- **快捷键**:Ctrl+=/-/0 字号(main.tsx capture 监听,xterm-helper-textarea 放行——隐藏 textarea 曾被误判为输入框导致终端聚焦时失效,已修);globalShowHide accelerator(globalShortcuts.ts,设置页系统分区可配,注册失败仅 warn);Ctrl+PgUp/PgDn 面板循环(Workspace capture 监听)
- **打包**:electron-builder.yml(msi 固定 upgradeCode 5ab9f79e-e4eb-4052-9df6-3af3a301ab0a + nsis;asarUnpack @lydell/node-pty + ssh2;npmRebuild false);updater.ts(仅 packaged 启用,OT_UPDATE_URL/OT_UPDATE_TOKEN env,默认 feed=上述 generic package 地址)
- **打包**:electron-builder.yml(**NSIS x64 是唯一安装包**——v1.0.22 起不再构建 MSI,electron-updater 本就不支持 MSI 自动更新;asarUnpack @lydell/node-pty + ssh2;npmRebuild false);updater.ts(仅 packaged 启用,OT_UPDATE_URL/OT_UPDATE_TOKEN env,默认 feed=上述 generic package 地址)
## 发布流程(下一版本照抄)
1. `package.json` version 升位 + 写 `RELEASE_NOTES.md`(可选 `.zh-TW/.en/.ja` 译文)→ `node scripts/sync-changelog.cjs`(**在 dist 之前**:更新日志会打进安装包)→ `npm run dist`(env:ELECTRON_MIRROR + ELECTRON_BUILDER_BINARIES_MIRROR=npmmirror;dist:dir 后先删 release/win-unpacked 避免占用 EPERM)。`predist` 会先跑 `npm test`(typecheck + 12 个离线测试)再 `npm install --package-lock-only` 同步锁文件根版本号,**release 提交要包含 package-lock.json**(否则根版本会漂移,v1.0.15–1.0.19 曾漂了 5 个版本)
2. `node scripts/release.cjs <版本号>`(**不带 skip 参数**,Gitea 与 GitHub 一起发):脚本自己建 Gitea release(msi/exe 资产)→ 传更新通道 `exe.blockmap → exe → release-notes.md → latest.yml`(**latest.yml 最后**);再把 exe/exe.blockmap/latest.yml 作为 release 资产同步发到 GitHub(electron-updater 标准 GitHub provider 直接吃 release 资产)。不再先删旧版,latest.yml 生效后才清掉上一版 exe/blockmap。上传前 `assertNoDowngrade()` 会读通道 latest.yml,线上版本更高时直接拒绝
1. `package.json` version 升位 + 写 `RELEASE_NOTES.md`(可选 `.zh-TW/.en/.ja` 译文)→ `node scripts/sync-changelog.cjs`(**在 dist 之前**:更新日志会打进安装包)→ `npm run dist`(env:ELECTRON_MIRROR + ELECTRON_BUILDER_BINARIES_MIRROR=npmmirror;dist:dir 后先删 release/win-unpacked 避免占用 EPERM)。`predist` 会先跑 `npm test`(typecheck + 19 个离线测试)再 `npm install --package-lock-only` 同步锁文件根版本号,**release 提交要包含 package-lock.json**(否则根版本会漂移,v1.0.15–1.0.19 曾漂了 5 个版本)
2. `node scripts/release.cjs <版本号>`(**不带 skip 参数**,Gitea 与 GitHub 一起发):脚本自己建 Gitea release(exe 资产)→ 传更新通道 `exe.blockmap → exe → release-notes.md → latest.yml`(**latest.yml 最后**);再把 exe/exe.blockmap/latest.yml 作为 release 资产同步发到 GitHub(electron-updater 标准 GitHub provider 直接吃 release 资产)。不再先删旧版,latest.yml 生效后才清掉上一版 exe/blockmap。上传前 `assertNoDowngrade()` 会读通道 latest.yml,线上版本更高时直接拒绝——**该探测已 fail-closed**:网络错误 / 非 404 失败 / 解析不出 version 一律抛错中止发布,只有通道真空(404)才放行;通道探测在 `pruneChannel` 前那次是 best-effort(失败只跳过清理并打日志,不中断已生效的发布)
3. 通道传坏了只补通道:`node scripts/release.cjs <版本号> --channel-only`(不建 release、不发 GitHub;同一版本可重复运行:release 复用、已传资产跳过)
4. 校验:无 token `curl .../generic/openterminal-update/stable/latest.yml` 应 200 且 version 正确
5. `git tag vX.Y.Z && git push origin main vX.Y.Z`(GitHub 镜像推代码/tag + release 资产,与 Gitea 保持同步)
4. 跳过部分目标的 flag:`--skip-github` 只跳 GitHub release;`--skip-gitea-release` 只跳 Gitea release,**更新通道照常上传**;`--skip-gitea` 两者都跳(打印醒目警告——国内用户将收不到该版本,只想跳 release 请用 `--skip-gitea-release`)
5. 校验:无 token `curl .../generic/openterminal-update/stable/latest.yml` 应 200 且 version 正确
6. `git tag vX.Y.Z && git push origin main vX.Y.Z`(GitHub 镜像推代码/tag + release 资产,与 Gitea 保持同步)
> GitHub 请求走 `HTTPS_PROXY=http://127.0.0.1:7897`(脚本只把它用于 GitHub);国内通道全程直连,不设代理
@@ -73,22 +74,31 @@
```bash
npm run typecheck # tsconfig.node.json + tsconfig.web.json
npm run build
npm test # = pretest(typecheck) + node tests/build-bundles.cjs + 下面 12 个测试(依次,全部离线可跑)
npm test # = pretest(typecheck) + node tests/build-bundles.cjs + 下面 19 个测试(依次,全部离线可跑)
node tests/ssh-loopback.mjs
node tests/commands-store.mjs
node tests/connections-store.mjs # SSH 书签 CRUD / 公开-密文切分 / 损坏文件备份
node tests/settings-store.mjs # 设置清洗器(closeAction/高亮规则修复/旧预设升级/告警日志)
node tests/local-path-grants.mjs # 本地路径准入(对话框授权、realpath+stat、大小写折叠)
node tests/lock-store.mjs # 锁屏密码校验值(scrypt 往返、损坏文件)
node tests/lock-controller.mjs # 冷却阶梯、并发串行化、落盘恢复、闲置触发、清除联动
node tests/lock-shortcuts.mjs # 锁屏快捷键分类器(表驱动)
node tests/reserved-accelerators.mjs # 保留快捷键表(设置页录制器与主进程注册守卫共用一张表)
node tests/.terminal-title.cjs # 终端自动标题多语言反解与原地重渲染
node tests/ipc-guard.mjs # IPC sender guard(走真实注册路径灌伪造帧)
node tests/updater-fallback.mjs # 更新源回退与超时预算(GitHub→Gitea)
node tests/log-sanitizer.mjs # 会话日志纯文本转换(ANSI 状态机 + 字节切分 fuzz)
node tests/sftp-timeout.mjs # SFTP per-op 超时与半死通道驱逐
node tests/.hl-split-smoke.cjs
node tests/.hl-rules.cjs # 内置高亮预设(词边界、大小写、负向词、危险命令)
node tests/zmodem-e2e.mjs
node tests/ssh-session-e2e.mjs
node tests/sysinfo-e2e.mjs
# `node tests/build-bundles.cjs` 单独重建全部 esbuild bundle(别名只存在于该脚本):
# .session-e2e.cjs(pty.ts) .sftp-svc.mjs(sftp.ts,ESM) .commands-store.cjs .connections-store.cjs
# .known-hosts.cjs .settings-store.cjs .lock-store.cjs .lock-controller.cjs .lock-shortcuts.cjs
# .session-e2e.cjs(pty.ts) .ssh.cjs(ssh.ts) .sftp-svc.mjs(sftp.ts,ESM) .commands-store.cjs
# .connections-store.cjs .known-hosts.cjs .settings-store.cjs .local-path-grants.cjs
# .lock-store.cjs .lock-controller.cjs .lock-shortcuts.cjs .reserved-accelerators.cjs
# .terminal-title.cjs .updater.cjs .ipc.cjs .log-sanitizer.cjs .ipc-channels.cjs
# .zmodem-e2e.cjs .hl-split-smoke.cjs .hl-rules.cjs —— 少 --alias:@shared=./src/shared 会编译失败
# 真实服务器测试(需 JD 环境变量凭据,旧凭据已过期,不在 npm test 内):
# JD_HOST=... JD_USER=root JD_PASS=... node tests/sftp-real.mjs / tests/sftp-chmod.mjs
+52 -12
View File
@@ -1,7 +1,16 @@
// OpenTerminal release publisher
// Usage: node scripts/release.cjs <version> [--skip-github] [--skip-gitea] [--channel-only]
// Usage: node scripts/release.cjs <version> [--skip-github] [--skip-gitea-release] [--skip-gitea] [--channel-only]
// node scripts/release.cjs 1.0.2
// node scripts/release.cjs 1.0.2 --channel-only # update channel only (repair)
// node scripts/release.cjs 1.0.2 --channel-only # update channel only (repair)
// Flags:
// --skip-github skip the GitHub release (assets included)
// --skip-gitea-release skip only the Gitea *release*; the Gitea update
// channel still runs (this is what you want if the
// release already exists / is not needed)
// --skip-gitea skip the Gitea release AND the Gitea update channel
// — Chinese users then never see this version; prefer
// --skip-gitea-release unless you really mean both
// --channel-only update channel only (repair); no release, no GitHub
// Reads release notes from RELEASE_NOTES.md (repo root, gitignored).
// Credentials from .env (repo root, gitignored): GIT_TOKEN, GH_TOKEN.
// If HTTPS_PROXY / HTTP_PROXY (env or .env) is set, traffic goes through it
@@ -13,11 +22,16 @@ const path = require('node:path')
const ROOT = path.join(__dirname, '..')
const V = process.argv[2]
if (!V || !/^\d+\.\d+\.\d+$/.test(V)) {
console.error('usage: node scripts/release.cjs <x.y.z> [--skip-github] [--skip-gitea] [--channel-only]')
console.error('usage: node scripts/release.cjs <x.y.z> [--skip-github] [--skip-gitea-release] [--skip-gitea] [--channel-only]')
console.error(' --skip-github skip the GitHub release')
console.error(' --skip-gitea-release skip only the Gitea release; the update channel still runs')
console.error(' --skip-gitea skip the Gitea release AND the update channel (CN users get no update)')
console.error(' --channel-only update channel only (repair)')
process.exit(1)
}
const SKIP_GH = process.argv.includes('--skip-github')
const SKIP_GITEA = process.argv.includes('--skip-gitea')
const SKIP_GITEA_RELEASE = process.argv.includes('--skip-gitea-release')
const CHANNEL_ONLY = process.argv.includes('--channel-only')
/** Strip one pair of matching quotes. A .env quotes its values for the shell
@@ -179,16 +193,24 @@ async function assertNoDowngrade() {
console.log(`channel currently serves v${live}`)
}
/** Version the update channel serves right now (read from its latest.yml), or
undefined when the channel is empty/unreachable. */
/** Version the update channel serves right now (read from its latest.yml).
Fail-closed: only an *empty* channel (HTTP 404) reports "unknown". A network
error, a non-404 failure or a body without a version all throw — otherwise a
probe that fails for any reason would read as "no live version" and let a
downgrade through the guard below. */
async function channelVersion(base) {
let resp
try {
const resp = await fetch(`${base}/latest.yml`)
if (!resp.ok) return undefined
return (await resp.text()).match(/^version:\s*(\S+)/m)?.[1]
} catch {
return undefined
resp = await fetch(`${base}/latest.yml`)
} catch (e) {
throw new Error(`cannot reach update channel (${e.message})`)
}
// 404 = nothing published yet; that is a legal empty channel, not an error.
if (resp.status === 404) return undefined
if (!resp.ok) throw new Error(`update channel latest.yml returned HTTP ${resp.status}`)
const version = (await resp.text()).match(/^version:\s*(\S+)/m)?.[1]
if (!version) throw new Error('update channel latest.yml carries no version field')
return version
}
/** Size of a channel file, or -1 when it is not there. */
@@ -264,7 +286,17 @@ async function giteaChannel() {
// (ECONNRESET) can no longer leave the channel empty and unrepairable.
const isLatest = ([f]) => path.basename(f) === 'latest.yml'
const ordered = [...channelFiles.filter((e) => !isLatest(e)), ...channelFiles.filter(isLatest)]
const previous = await channelVersion(base)
// Best effort by design: this probe only decides whether the previous
// version's binaries get pruned afterwards. It now throws on a failed probe
// (fail-closed for the downgrade guard), and by the time we are here the new
// latest.yml is about to be live — a hiccup in a cleanup helper must never
// abort the tail of the publish.
let previous
try {
previous = await channelVersion(base)
} catch (e) {
console.log(` cannot read the live channel version (${e.message}) — skipping prune of the previous release`)
}
for (const [f, name] of ordered) {
const stat = fs.statSync(f)
const url = `${base}/${encodeURIComponent(name)}`
@@ -407,7 +439,15 @@ async function main() {
console.log('done (channel only)')
return
}
if (!SKIP_GITEA) { await gitea(); await giteaChannel() }
if (SKIP_GITEA) {
console.warn('*** WARNING: --skip-gitea skips the Gitea release AND the Gitea update channel. ***')
console.warn('*** Domestic users will NOT receive this version — it is only on GitHub, which most ***')
console.warn('*** of them cannot reach. To skip just the release, use --skip-gitea-release. ***')
}
// --skip-gitea-release: the release object already exists (or is not wanted),
// but the channel still has to be fed — that is the whole update path at home.
if (!SKIP_GITEA && !SKIP_GITEA_RELEASE) await gitea()
if (!SKIP_GITEA) await giteaChannel()
if (!SKIP_GH) await github()
console.log('done')
}