feat: 支持 CLI/TUI 程序启动时静默检测新版本与平滑升级#340
Conversation
- 引入 `go-selfupdate`,支持检测 Github Releases 并自动替换二进制文件 - 新增 `internal/version` 包,配合 `.goreleaser.yaml` 注入构建版本号 - 在根命令 `PersistentPreRunE` 挂载异步静默版本检测(屏蔽 `url-dispatch`) - 优化 TUI 生命周期,利用提示缓冲实现 AltScreen 退出后的更新提醒 - 新增 `neocode update [--prerelease]` 手动升级命令 - fix(test): 修复 Windows 环境下 config 目录权限测试由于 `os.Chmod` 不兼容导致的误报
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
| if err := runGlobalPreload(cmd.Context()); err != nil { | ||
| return err | ||
| } | ||
| runSilentUpdateCheck(cmd.Context()) |
There was a problem hiding this comment.
runSilentUpdateCheck currently runs for every command except url-dispatch, so neocode update also triggers an extra background release check. That adds an unnecessary second network call and can produce a stale post-exit notice right after a manual update. Consider skipping silent checks for update (and potentially other short-lived utility commands).
| if strings.TrimSpace(result.LatestVersion) == "" { | ||
| return | ||
| } | ||
| setUpdateNotice(fmt.Sprintf("🚀 发现新版本: %s,运行 neocode update 即可升级", result.LatestVersion)) |
There was a problem hiding this comment.
result.LatestVersion comes from remote release metadata and is interpolated directly into terminal output. Please sanitize/strip control characters (especially ANSI escape sequences) before rendering, to avoid terminal escape injection if upstream metadata is ever malformed or compromised.
| if latest == "" { | ||
| latest = "unknown" | ||
| } | ||
| _, _ = fmt.Fprintf(out, "Already up-to-date (latest: %s).\n", latest) |
There was a problem hiding this comment.
latest (remote metadata) is printed directly to stdout. For consistency with security hardening, sanitize control characters before output (same concern applies to the success line at Updated successfully: ... -> ...).
| } | ||
| return runGlobalPreload(cmd.Context()) | ||
| if err := runGlobalPreload(cmd.Context()); err != nil { | ||
| return err |
There was a problem hiding this comment.
runSilentUpdateCheck is invoked for every command that passes preload, including neocode update. That means update can trigger a background check concurrently with the foreground upgrade flow, and may later print a stale "new version" notice even after a successful update. Consider skipping silent check for the update command (similar to url-dispatch).
| // defaultSilentUpdateCheck 在后台异步检查新版本并缓存退出后提示文案。 | ||
| func defaultSilentUpdateCheck(ctx context.Context) { | ||
| currentVersion := version.Current() | ||
| if !version.IsSemverRelease(currentVersion) { |
There was a problem hiding this comment.
The silent check runs in a detached goroutine and the process does not wait for it before printing/consuming notice at program end. For short-lived commands, this often drops the notice entirely because the process exits first. If the intent is to reliably show a post-exit notice, add a bounded synchronization point or persist the check result for next run.
| if err := runGlobalPreload(cmd.Context()); err != nil { | ||
| return err | ||
| } | ||
| runSilentUpdateCheck(cmd.Context()) |
There was a problem hiding this comment.
runSilentUpdateCheck 会在所有非 url-dispatch 命令前执行,因此 neocode update 也会并发触发一次“新版本检测”。这会导致一个可见回归:用户刚执行完升级后,进程退出时仍可能打印“发现新版本”的过期提示(静默检查结果比升级动作更早缓存)。建议将 update 也加入跳过名单,或在 update 命令执行路径里禁用/清空该提示。
| if err := runGlobalPreload(cmd.Context()); err != nil { | ||
| return err | ||
| } | ||
| runSilentUpdateCheck(cmd.Context()) |
There was a problem hiding this comment.
PersistentPreRunE 会在执行 neocode update 时也触发 runSilentUpdateCheck,导致一次手动升级流程里并发发起两次 Release 检测请求。这个回归会放大 GitHub API 限流/抖动风险,并可能让前台 update 命令偶发失败。建议在 shouldSkipGlobalPreload(或独立判断)里把 update 命令也加入跳过列表。
| } | ||
|
|
||
| // defaultUpdateCommandRunner 执行手动升级流程并返回升级结果。 | ||
| func defaultUpdateCommandRunner(ctx context.Context, options updateCommandOptions) (updater.UpdateResult, error) { |
There was a problem hiding this comment.
defaultUpdateCommandRunner 直接沿用 cmd.Context(),而 main 里传入的是 context.Background(),没有超时或取消边界。网络异常时 neocode update 可能长时间阻塞。建议为手动更新增加明确超时(如 context.WithTimeout)并返回可诊断的超时错误。
| if err := runGlobalPreload(cmd.Context()); err != nil { | ||
| return err | ||
| } | ||
| runSilentUpdateCheck(cmd.Context()) |
There was a problem hiding this comment.
PersistentPreRunE 在 update 命令路径也会调用 runSilentUpdateCheck。这会与手动升级流程并发访问 release API,并可能在升级成功后仍输出“发现新版本”的过期提示。建议将 update 加入跳过名单,或在该命令路径禁用静默检查。
| if strings.TrimSpace(result.LatestVersion) == "" { | ||
| return | ||
| } | ||
| setUpdateNotice(fmt.Sprintf("🚀 发现新版本: %s,运行 neocode update 即可升级", result.LatestVersion)) |
There was a problem hiding this comment.
这里把远端 LatestVersion 直接拼接进终端输出。若上游字符串包含 ANSI 控制序列,可能导致终端转义注入。建议在展示前统一剥离控制字符(例如仅允许可打印字符,或过滤 ESC/CSI 序列)。
| } | ||
|
|
||
| // defaultUpdateCommandRunner 执行手动升级流程并返回升级结果。 | ||
| func defaultUpdateCommandRunner(ctx context.Context, options updateCommandOptions) (updater.UpdateResult, error) { |
There was a problem hiding this comment.
update 命令直接使用 cmd.Context() 进入网络更新流程,没有显式超时边界。遇到网络劣化/长时间重试时可能阻塞命令。建议在 runner 内包一层 context.WithTimeout(可配置),并在超时时返回可读错误。
- 拦截 `update` 命令的静默检测逻辑,避免与手动升级流程产生并发冲突及输出过期提示 - 新增 `sanitizeVersionForTerminal`,剥离远端版本字符串中的 ANSI 控制序列,防止终端转义注入 - 为 `neocode update` 手动升级命令增加 5 分钟显式超时上下文,并优化网络超时错误提示 - 补全边界场景单元测试(涵盖跳过静默检测、字符串清洗及网络超时触发分支)
|
@xgopilot 将internal/security/workspace.go、internal/context/microcompact.go、internal/updater/updater.go、internal/session/store.go、internal/tui/core/app/update.go、internal/subagent/worker.go等文件的测试覆盖率提升至百分之九十五以上 |
执行结果已完成并推送提交: 本次改动
覆盖率结果(目标文件)
未完成项说明本次未能将
|
Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: pionxe <148670367+pionxe@users.noreply.github.com>
|
@xgopilot 针对本次PR,请给我一份给AIreview用的提示词,针对的是修改中所有可能存在的风险点 |
AIreview 提示词(可直接复制)你是资深 Go 代码审查员。请仅基于 审查范围文件(重点):
请重点检查以下风险点(逐项给结论):
输出格式要求:
说明上面这份提示词已经按本 PR 的改动内容定制,直接粘贴到 AIreview 即可。 |
|
@xgopilot 你是资深 Go 代码审查员。请仅基于
|
High
Medium
Low
逐项结论(其余风险点)
|
|
/simplify |
|
已完成请求中的三件事:检查未解决 review comment、修复值得修复的问题、执行代码简化并推送。 处理结果
测试
提交与推送
|
Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: pionxe <148670367+pionxe@users.noreply.github.com>
📝 动机与背景 (Motivation)
之前用户只能通过手动重新执行
install脚本来升级版本,且没有任何新版本更新提醒,导致版本迭代和分发效率较低。本次 PR 引入了原生的自动更新机制(Auto-Update),旨在不阻塞主 TUI 启动、不干扰 AltScreen 渲染的前提下,提供优雅的版本更新体验。
Closes #336
✨ 核心功能 (Key Features)
url-dispatch)前,启动带有超时(3s)的异步 Goroutine 检测远端 Release。Execute之后的安全生命周期,确保在 TUI 完全退出后才将提示打印到终端。neocode update命令。支持自动解析架构、下载tar.gz/.zip、校验checksums.txt并原地替换可执行文件。neocode update --prerelease选择升级到尝鲜版。🛠️ 技术实现细节 (Technical Details)
internal/version包,修改.goreleaser.yaml,通过ldflags -X在 CI 构建时将 GitHub Tag 动态注入程序。开发模式下默认回退至dev并禁用自动提醒。github.com/creativeprojects/go-selfupdate,处理了复杂的跨平台(特别是 Windows)执行文件覆盖锁问题。Config.Filters与Config.OS/Arch,完美适配我们现有的构建产物命名规则。internal/config包中TestLoadCustomProvidersReadDirAndStatErrors用例在 Windows 环境下因os.Chmod无法剥夺目录权限而导致的偶发性/必然性失败,引入了针对性的t.Skip跨平台兼容策略。✅ 测试情况 (Testing)
go test ./...全量绿灯(历史失败用例已修复)。--prerelease标志位路由正确。dev版本编译时,不触发网络更新提示。scoop/shims/neocode.exe)。README.md与docs/guides/update.md中补充新命令的使用说明。