Files
homeagent-sdk/tools/hmapdev/example_gomod_test.go
JianFeeeee 72011bc8bf 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。
2026-10-17 19:42:27 +08:00

130 lines
4.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}