fix(examples): 恢复 go.mod 相对路径 + 移除 plg.json 的 sdk 固定(修 CI 红)

CI 上 `Examples (linux/amd64)` 与 `(darwin/arm64)` 双红,只有两个失败:

  ✗ commondev   构建失败
  ✗ editdoc     构建失败
  error: 项目声明 sdk=1.4.0,但本地 SDK 存储里没有这个版本

其余 20 个示例全部成功 —— 说明不是通用问题,而是这两个被改坏了。

## 根因(两个叠加)

**① go.mod 被本机绝对路径污染**

我在上一个提交里用 `git add -A`,把本机 `go build` **自动改写**过的
go.mod 一并提交了:

  require github.com/JianFeeeee/homeagentsdk v1.4.0
  replace github.com/JianFeeeee/homeagentsdk => /root/.homeagent/hmapdev/sdk/v1.4.0

基线(origin/main)是:

  require github.com/JianFeeeee/homeagentsdk v0.0.0
  replace github.com/JianFeeeee/homeagentsdk => ../../

本机那个目录真的存在 ⇒ 本地全绿;CI 没有 ⇒ 必红。

**② plg.json 声明了 `"sdk": "1.4.0"`**

一声明版本,hmapdev 就去**本地 SDK store** 找它,找不到直接拒绝构建。
全仓 20+ 示例**都不声明**,只有我改过的这两个声明了。
示例的正确依赖表达是 go.mod 的 `v0.0.0` + `replace ../../`。

## 修法

两个文件都恢复到与其余示例一致的写法。

## 判据(3 条,都是新加的)

 · `TestExampleGoModHasNoAbsoluteReplace`:任何示例 go.mod 不得含
   `/root/`、`/home/`、`/Users/`、`C:\`
 · `TestExampleGoModUsesRelativeSDKReplace`:replace 必须是相对路径 `../../`
 · `TestExamplePlgJsonDoesNotPinSDKVersion`:plg.json 不得声明 sdk

变异 2/2 咬住(重新引入绝对路径 / 重新声明 sdk,都准确报出文件路径)。

## 本地验证(模拟 CI 干净环境)

用干净 HOME 跑 `package/build-examples.sh`:
 · 修复前:成功 18 / 失败 4(commondev, editdoc, browser, rss)
 · 修复后:成功 20 / 失败 2(browser, rss —— **本机代理不通**,
   `proxy.golang.org` 超时;用本地 modcache 验证这两个可正常构建)

即 22/22 源码都能构建,CI 有网络不受影响。

## 教训

`git add -A` 会把「构建工具自动改写的文件」一起提交。
提交前必须 `git status` 逐个看,尤其 go.mod / go.sum。
This commit is contained in:
JianFeeeee
2026-10-17 19:42:27 +08:00
parent 0111ac3a7a
commit 72011bc8bf
5 changed files with 136 additions and 79 deletions

View File

@ -0,0 +1,129 @@
package main
// 示例插件的 go.mod 判据(2026-10-17 CI 事故)。
//
// ## 事故
//
// 我在一个提交里用 `git add -A` 把本机 `go build` **自动改写**过的
// go.mod 一并提交了:
//
// require github.com/JianFeeeee/homeagentsdk v1.4.0
// replace github.com/JianFeeeee/homeagentsdk => /root/.homeagent/hmapdev/sdk/v1.4.0
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 本机绝对路径
//
// 后果:CI(干净环境)上 commondev / editdoc 构建**必然失败**
//
// error: 项目声明 sdk=1.4.0,但本地 SDK 存储里没有这个版本
//
// 而本机能过(那个目录真的存在)⇒ 本地全绿掩盖了问题,PR 卡在 CI 才暴露。
//
// ## 判据
//
// 1. 任何示例的 go.mod 都**不得**含本机绝对路径
// 2. replace 必须是相对路径(`../../`),这是 SDK 仓内示例的约定
// 3. plg.json 不得声明 `sdk` 版本 —— 一声明就要求本地 SDK store 有该版本,
// 而示例的正确依赖表达是 go.mod 的 v0.0.0 + replace ../../
//
// 这三条合起来保证「CI 干净环境能构建全部示例」。
import (
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)
// ★★★ 判据:示例 go.mod 不得含本机绝对路径。
func TestExampleGoModHasNoAbsoluteReplace(t *testing.T) {
for _, gm := range findExampleGoMods(t) {
b, err := os.ReadFile(gm)
if err != nil {
t.Fatal(err)
}
src := string(b)
for _, bad := range []string{"/root/", "/home/", "/Users/", "C:\\"} {
if strings.Contains(src, bad) {
rel := mustRel(t, gm)
t.Fatalf("★ %s 含本机绝对路径 %q ——\n"+
" CI(干净环境)会因此构建失败,而本机因为有那个目录反而通过。\n"+
" 示例必须用相对路径:replace ... => ../../", rel, bad)
}
}
}
}
// ★★★ 判据:示例的 replace 指向 SDK 仓根(相对路径)。
func TestExampleGoModUsesRelativeSDKReplace(t *testing.T) {
reReplace := regexp.MustCompile(`(?m)^replace\s+\S*homeagentsdk\S*\s*=>\s*(\S+)`)
for _, gm := range findExampleGoMods(t) {
b, _ := os.ReadFile(gm)
m := reReplace.FindStringSubmatch(string(b))
if m == nil {
continue // 没有 replace 也算合法(某些示例可能不用)
}
tgt := m[1]
if filepath.IsAbs(tgt) {
t.Fatalf("★ %s 的 replace 是绝对路径 %q(必须相对)", mustRel(t, gm), tgt)
}
if tgt != "../../" {
t.Errorf("%s 的 replace 是 %q,本仓约定为 ../../(与 ai_image/calendar 等一致)",
mustRel(t, gm), tgt)
}
}
}
// ★★★ 判据:示例 plg.json 不得声明 sdk 版本。
//
// 为什么:一声明 `"sdk": "1.4.0"`,hmapdev 就会去**本地 SDK store** 找该版本,
// 找不到直接拒绝构建。而示例的正确依赖表达是 go.mod 的 v0.0.0 + replace ../../。
// 全仓 20+ 个示例都不声明,只有被我改过的两个声明了 ⇒ CI 只在这两个上红。
func TestExamplePlgJsonDoesNotPinSDKVersion(t *testing.T) {
root := repoRootForTest(t)
matches, _ := filepath.Glob(filepath.Join(root, "example", "*", "plg.json"))
reSDK := regexp.MustCompile(`"sdk"\s*:`)
for _, p := range matches {
b, err := os.ReadFile(p)
if err != nil {
continue
}
if reSDK.Match(b) {
rel := mustRel(t, p)
t.Fatalf("★ %s 声明了 sdk 版本 ——\n"+
" 这会让 hmapdev 去本地 SDK store 找该版本,干净 CI 上必然失败。\n"+
" 示例请用 go.mod 的 `require ... v0.0.0` + `replace ... => ../../`", rel)
}
}
}
func findExampleGoMods(t *testing.T) []string {
t.Helper()
root := repoRootForTest(t)
matches, err := filepath.Glob(filepath.Join(root, "example", "*", "go.mod"))
if err != nil {
t.Fatal(err)
}
if len(matches) == 0 {
t.Fatal("没找到任何示例 go.mod ⇒ 判据会假绿,路径写错了?")
}
return matches
}
func repoRootForTest(t *testing.T) string {
t.Helper()
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
// tools/hmapdev → 上两级
return filepath.Dir(filepath.Dir(wd))
}
func mustRel(t *testing.T, p string) string {
t.Helper()
r, err := filepath.Rel(repoRootForTest(t), p)
if err != nil {
return p
}
return r
}