feat(ota): kernelupgrade 抽成独立包 + 插件仓库迁移脚本

## 1. internal/kernelupgrade:抽包(避免两份实现漂移)

原来 641 行全在 cmd/homed-upgrade 的 main 包里。内核侧的 kernel_update
工具要复用同一套逻辑,而 main 包不可被 import —— 照抄一份的后果是
"CLI 能升级、agent 调用却把机器搞坏"。

导出:ReadBinVersion / CompareVersions / ParseSemver / CmpSemver /
ProbePluginCompat / ExtractPkg / FindFile / DoUpgrade / WaitHealthy 等。
CLI 变薄壳(只留 flag 解析与 fail 输出)。

★ 顺带修一个真实 bug(判据抓到):ParseSemver 原来只挡 len<2,
于是 "1.4" 漏过去、缺失的 patch 被零值填成 1.4.0 —— 会让
「1.4 → 1.4.0」被判成完全相同(进而说"无需升级"),而两者内容未必相同。
现在要求恰好三段。

## 2. scripts/migrate-plugins-to-org.sh:插件迁成独立仓库

为什么迁:插件散在 SDK 仓 example/ 下有三个问题 ——
  ① 插件版本与 SDK 版本绑死(一次 SDK 发版把所有插件都带上)
  ② 插件作者不能单独为自己的插件发版
  ③ plugin_update 需要"仓库地址 → 拉源码 → 编译",而 example/ 是
     别人的仓库,不是插件自己的仓库

脚本纪律:
· 默认 --dry-run,不碰 GitHub
· 产物不入库(dist/build/rawfile/node_modules/*.hmap 全排除)——
  实测源码 3MB、产物 1.9G,后者进了仓库就再也删不干净
· 幂等:仓库已存在(422)则跳过创建,重跑安全
· 自动给 plg.json 补 repo 字段,使 plugin_update 能定位仓库
· 用 tar 管道而非 rsync:后者在最小化系统里常常没有(本机实测就没有)

执行结果:26 个插件全部迁移(25 Go + 1 Lua),仓库名与插件名同名。
This commit is contained in:
JianFeeeee
2026-10-26 00:06:59 +08:00
parent 17f36ac430
commit 02022bc13d
4 changed files with 777 additions and 572 deletions

View File

@ -1,6 +1,11 @@
package main
import "testing"
import (
"strings"
"testing"
"github.com/JianFeeeee/HologramAgent/internal/kernelupgrade"
)
// ── 版本比较的判据 ────────────────────────────────────────────
//
@ -10,47 +15,47 @@ import "testing"
func TestCompareVersions(t *testing.T) {
cases := []struct {
name string
cur *binVersion
nv *binVersion
cur *kernelupgrade.BinVersion
nv *kernelupgrade.BinVersion
wantSub string // 结论文案必须包含的片段
}{
{
name: "正常升级",
cur: &binVersion{Version: "1.3.0", Commit: "aaa"},
nv: &binVersion{Version: "1.4.0", Commit: "bbb"},
cur: &kernelupgrade.BinVersion{Version: "1.3.0", Commit: "aaa"},
nv: &kernelupgrade.BinVersion{Version: "1.4.0", Commit: "bbb"},
wantSub: "可升级",
},
{
name: "降级必须显式警告",
cur: &binVersion{Version: "1.4.0", Commit: "aaa"},
nv: &binVersion{Version: "1.3.0", Commit: "bbb"},
cur: &kernelupgrade.BinVersion{Version: "1.4.0", Commit: "aaa"},
nv: &kernelupgrade.BinVersion{Version: "1.3.0", Commit: "bbb"},
wantSub: "降级",
},
{
name: "同版本同 commit = 无需升级",
cur: &binVersion{Version: "1.4.0", Commit: "aaa"},
nv: &binVersion{Version: "1.4.0", Commit: "aaa"},
cur: &kernelupgrade.BinVersion{Version: "1.4.0", Commit: "aaa"},
nv: &kernelupgrade.BinVersion{Version: "1.4.0", Commit: "aaa"},
wantSub: "完全相同",
},
{
// ★ 这条最容易被忽略:版本号没变但内容变了(同版本重发)。
// 不报出来会让用户以为"升级了其实没升"或反之。
name: "同版本不同 commit 必须点明",
cur: &binVersion{Version: "1.4.0", Commit: "aaa"},
nv: &binVersion{Version: "1.4.0", Commit: "bbb"},
cur: &kernelupgrade.BinVersion{Version: "1.4.0", Commit: "aaa"},
nv: &kernelupgrade.BinVersion{Version: "1.4.0", Commit: "bbb"},
wantSub: "同版本重发",
},
{
// 非标准形态(1.4.0dev):走字符串回退,不能说"降级"
name: "非标准版本号走字符串比较",
cur: &binVersion{Version: "1.4.0", Commit: "aaa"},
nv: &binVersion{Version: "1.4.0dev", Commit: "bbb"},
cur: &kernelupgrade.BinVersion{Version: "1.4.0", Commit: "aaa"},
nv: &kernelupgrade.BinVersion{Version: "1.4.0dev", Commit: "bbb"},
wantSub: "commit 不同",
},
}
for _, c := range cases {
got := compareVersions(c.cur, c.nv)
if !containsStr(got, c.wantSub) {
got := kernelupgrade.CompareVersions(c.cur, c.nv)
if !strings.Contains(got, c.wantSub) {
t.Errorf("%s: 结论应含 %q,实际 %q", c.name, c.wantSub, got)
}
}
@ -61,11 +66,11 @@ func TestCompareVersions(t *testing.T) {
// 这是实测踩到的:线上内核是加 --version 之前的版本,第一次探测必然失败。
// 若此时直接退出,所有用户第一次升级都要人工介入。
func TestCompareVersions_UnknownCurrentDoesNotBlock(t *testing.T) {
got := compareVersions(nil, &binVersion{Version: "1.4.0", Commit: "bbb"})
if containsStr(got, "❌") {
got := kernelupgrade.CompareVersions(nil, &kernelupgrade.BinVersion{Version: "1.4.0", Commit: "bbb"})
if strings.Contains(got, "❌") {
t.Errorf("当前版本未知不应判失败,实际: %s", got)
}
if !containsStr(got, "跳过比较") {
if !strings.Contains(got, "跳过比较") {
t.Errorf("应说明跳过比较,实际: %s", got)
}
}
@ -73,13 +78,22 @@ func TestCompareVersions_UnknownCurrentDoesNotBlock(t *testing.T) {
// ── 版本解析 ─────────────────────────────────────────────────
func TestParseSemver(t *testing.T) {
if v := parseSemver("1.4.0"); v == nil || v.major != 1 || v.minor != 4 || v.patch != 0 {
t.Errorf("1.4.0 解析错: %+v", v)
// 只测**可观察行为**(返回 nil 与否),不探私有字段 ——
// 跨包断言内部字段会把重构锁死,而"解析对不对"最终要由
// CompareVersions 的行为体现(上面那组用例已覆盖)。
if kernelupgrade.ParseSemver("1.4.0") == nil {
t.Error("1.4.0 应能解析")
}
// dev 后缀不算正式语义版本(否则 1.4.0dev 会被当成 1.4.0 比大小)
if v := parseSemver("1.4.0dev"); v != nil {
if v := kernelupgrade.ParseSemver("1.4.0dev"); v != nil {
t.Errorf("1.4.0dev 应回退字符串比较,实际解析成 %+v", v)
}
// 不完整串同样不算
for _, bad := range []string{"", "1", "1.4", "v1.4.0", "1.4.x"} {
if v := kernelupgrade.ParseSemver(bad); v != nil {
t.Errorf("%q 不应被解析成语义版本,实际 %+v", bad, v)
}
}
}
// ── waiter 兜底解析 ──────────────────────────────────────────
@ -87,20 +101,13 @@ func TestParseSemver(t *testing.T) {
func TestFirstSemverLike(t *testing.T) {
// waiter 的真实输出形态
line := "waiter 1.4.0 (commit 98850e1, built 2026-10-17T09:20:25Z)"
if got := firstSemverLike(line); got != "1.4.0" {
if got := kernelupgrade.FirstSemverLike(line); got != "1.4.0" {
t.Errorf("应从 waiter 输出取到 1.4.0,实际 %q", got)
}
// 不能把日期当版本号
if got := firstSemverLike("built 2026-10-17"); got != "" {
if got := kernelupgrade.FirstSemverLike("built 2026-10-17"); got != "" {
t.Errorf("日期不应被识别为版本号,实际 %q", got)
}
}
func containsStr(s, sub string) bool {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
}