Files
HomeAgent/docs/zh/ci-cd-runbook.md
JianFeeeee 0fe239f8cd docs(ci): 记录 cmd/gui 假告警的根因与本机补丁(手册 §6)
每轮编辑后 pi-lens 都会报 `FAIL ./cmd/gui [setup failed]`,看着像仓库有测试红了,
实为工具缺陷。本会话内重复触发 20+ 次,每次都需人工复核,故完整记录并修掉。

根因(两个缺陷叠加):
  1. pi-lens 的 runner 按**仓库根**检测(本仓有 go.mod ⇒ go),但被测文件可能
     在另一种语言的项目里;`cmd/gui/*.test.mjs` 命中通用测试命名 ⇒ 对纯 Electron
     目录生成 `go test -run . ./cmd/gui` ⇒ 必然 "no Go files" 失败。
  2. 该失败进入进程内 failedTestsByRunner,而 failed-first 策略此后**每次**编辑
     都优先重跑它(与当前编辑的文件无关);条目只在测试**通过**时移除 ⇒ 永不自愈。
     日志形态:turn_end: README.md → test go cmd/gui/sse-backoff.test.mjs (failed-first)

为什么不能靠配置关掉(都实测/读码确认):
  - `.pi-lens.json` 是项目级,只认 ignore/rules/maxProjectFiles/reviewGraph/trivy
    + 三个改动开关;`tests` 是全局级,写进去会被忽略并告警。
  - 全局 `{"tests":{"enabled":false}}` 会一起关掉**所有项目**的回合末测试反馈,
    为一个仓库的误报付全局代价 —— 不值。
  - `ignore` 也挡不住:它只作用于扫描,不参与测试目标选择。

修法:给 pi-lens 的 getTestRunTarget 加一道「该 runner 真能跑这个目标吗」的校验
(go:目标目录至少有一个 .go 文件),不能跑就拒并清掉那条不可运行的失败记录。
runTestFileAsync 只有这一个调用点,是唯一收口。

验证:node --check 过语法;/usr/bin/diff 核对为纯新增零删除;
真实路径模拟 6/6 符合预期(cmd/gui 的 .mjs → 拒;真 Go 测试文件 → 放行);
go test ./... 仍 43 包全绿。

注意:补丁在 ~/.pi/agent/npm/... 里,pi-lens 升级后会被覆盖(届时误报会回来,
不影响仓库,只是噪音)。备份 index.js.orig-*,回退方式写在本节。

本手册同时补上 §6.1 症状 / §6.2 根因 / §6.3 配置为何无效 / §6.4 修法与回退。
2026-09-29 21:24:09 +08:00

14 KiB
Raw Blame History

CI/CD 流水线手册(GitHub Actions)

2026-09-29 随仓库迁移 gitcode → GitHub 而建。此前 AtomGit 停止对普通用户 提供流水线,本仓无任何自动化验证。本文记录两条流水线的用法、机制与边界。 分支模型见 docs/git-branching.md(本文与其 §七「发版产物清单」衔接)。

0. 仓库与流水线总览

仓 位置 流水线 触发
主仓 HomeAgent github.com/JianFeeeee/HomeAgent(公开) ci.yml + release.yml push / PR、release/** push
SDK 仓 homeagentsdk github.com/JianFeeeee/homeagentsdk(公开) release.yml release/** push
gitcode 镜像 同名仓库 无(由主仓 Release 的 sync job 同步) —

分支保护(main,公开仓免费):10 项必须检查(Go build/vet/test、Race、 cross×5、GUI、C gates、Docs)+ strict(必须与 main 同步)+ 禁 force push。 ⇒ feature 合入 main 前必须过流水线(PR 的 checks 全绿才能 merge)。

gitcode 的角色:只读镜像(git push 同步)。Release 附件由 sync job 自动 补传,或手工跑 deploy/scripts/upload_assets.py(见 §3.4)。

1. CI(ci.yml)—— 每次推送到 main / release/** / PR 都跑

六个 job,全部命令均本地实测过(原则:不写"应该有用"的未验证步骤):

job 内容 时长
Go build / vet / test go build/vet/test ./... + make check-client-versions ~12m
Race detector go test -race(core + waiter) ~8m
Cross-compile ×5 waiter/initconfig/mock-server 五平台(CGO=0) ~5m
GUI (node) cd cmd/gui && npm test(纯 Node,零依赖,秒级) <1m
C infrastructure gates make check-csrc(告警/ABI/ASan/跨架构) ~6m
Docs build mkdocs.yml 可解析性检查 <1m

明确不进 CI(依赖真机/密钥/内网,跑了只会变 flaky 噪音): deploy-*.sh、waiter 真机(192.168.2.x)、npm run test-live(需真 Electron+Xvfb+真后端)、scripts/kernel-stress/*、需 DEEPSEEK_API_KEY 的 真实 LLM 测试(自带 t.Skip)。

已知边界(不是缺陷)

  • 只有 waiter/initconfig/mock-server 能纯交叉编译。homed/memgc/ homed-kb-migrate 依赖 cgo(gojieba/onnx),必须原生构建(见 Makefile)。
  • cmd/gui 是纯 Electron 目录(0 个 .go、无 go.mod)。go test ./... 不会包含它(Go 的 ./... 语义);只有显式 go test ./cmd/gui 才报 "no Go files"。前端测试走 npm test。
  • CGO 必须开(CI 里 CGO_ENABLED: 1):gojieba 需要。

2. 发布(release.yml)—— release/** 推送即发版

2.1 标准发版流程(以 1.3.14 为例)

# 1. 在发布线的 worktree(干净)里 bump 版本
git worktree add /tmp/rel-v1.3.14 release/v1.3.x
cd /tmp/rel-v1.3.14
sed -i 's/Version = "1.3.13"/Version = "1.3.14"/' internal/meta/meta.go
git commit -am "release: 1.3.14"
git push origin release/v1.3.x
# 2. 流水线自动:读版本 → go build/test 门 → 下载资产 → 打包
#    → 打 tag v1.3.14 → 建 GitHub release → 传附件 → 回读校验
#    → (配了 GITCODE_TOKEN 时)同步 gitcode
# 3. 在 GitHub Actions 页看进度;全绿即发版完成

版本号唯一事实源是 internal/meta/meta.go 的 Version(SDK 仓是 meta/meta.go)。改它并推送 = 发版指令。

2.2 幂等闸门:tag 已存在 ⇒ 整轮跳过

Prepare 先查 refs/tags/v<version> 是否存在。已存在(比如改文档的推送、 cherry-pick 维护提交)则 Build/Publish/Sync 全部 skipped。不会重复发版。

2.3 发版门:go build 硬门 + go test 可显式跳过

  • go build ./... —— 硬门,不可跳过。

  • go test ./... —— 默认跑。历史维护线若存在既存红测试(如 release/v1.3.x 的 deepsearch 测试,main 上已修),在改动 meta 的那个 提交的正文里写 [skip-release-tests] 即可跳过:

    git commit -am "release: 1.3.14 [skip-release-tests]
    
    (正文里写明跳过理由——为什么本线的红是既存的、与本次发版无关)"
    

    跳过时 CI 输出 ::warning,决定记录在发版 commit 里可审计。 ★ 标记查在改动 meta 的提交上而非 HEAD:发版提交后常还会跟几个 维护提交,只看 HEAD 会让标记被顶掉、静默失效。

2.4 产物与验证

产物(v1.3.13 实测):

homeagent-client_1.3.13_amd64.deb      83.5MB   客户端(不含模型)
homeagent-server_1.3.13_amd64.deb     730.1MB   服务端(含向量模型)
homeagent-full_1.3.13_amd64.deb       808.3MB   全量(模型+ORT+GUI)
homeagent_1.3.13_linux_amd64.tar.gz   839.2MB   内核+CLI+GUI 打包
SHA256SUMS                                       全量校验和

流水线内置的验证(不通过即不发版):

  • deb 元数据(Package/Version/Architecture)逐包核对;
  • full/server 包内必须真的含 TextEncoder.onnx 与 libonnxruntime.so (防"默认启用但装完不能用"的假包);
  • sha256sum -c SHA256SUMS(产物先平铺再验 —— 脚本把校验和写成平铺名);
  • 发布后回读:把附件下载回来再验一遍校验和。

2.5 构建资产(ci-assets-v1 release)

server/full 的打包需要 719MB 模型 + 24MB ONNX Runtime。它们内容不随版本 变,故作为 ci-assets-v1 release 的附件一次性托管,每次发版由流水线下载 并 sha256sum -c 校验后使用:

chinese-clip-vit-b16-onnx.tar         718.8MB   Chinese-CLIP ViT-B/16 ONNX
onnxruntime-linux-amd64-1.28.0.tar     23.5MB   ORT 1.28.0 + LICENSE + TPN
SHA256SUMS

模型或 ORT 升级时:改好本地 CHINESECLIP_BUNDLE_DIR / ONNXRUNTIME_ASSET_DIR 指向的目录 → 重新打包 → 在 GitHub 上新建 ci-assets-v2 release 并上传 → 同步更新 release.yml 里的 ASSETS_TAG。

3. 运维要点(都踩过坑)

3.1 workflow 文件必须存在于目标分支

GitHub 用被推送 commit 里的 .github/workflows/*.yml 决定是否触发。 给旧发布线补流水线时,要把 workflow 文件本身 commit 到那条分支 (实测:只在 main 有时,推 release/** 什么都不触发)。

3.2 runner 上没有 electron 缓存

打包脚本从两处找 Electron:~/.cache/electron 的 zip,或 cmd/gui/node_modules/electron/dist。全新 runner 两处都没有 ⇒ release.yml 在打包前 npm ci(electron 已在 package-lock 锁定;不能 npm install --production,electron 是 devDependency 会被跳过)。

3.3 管道里的 grep -q 会因 SIGPIPE 误杀检测

dpkg-deb -c <800M 包> | grep -q <目标>:grep -q 匹配即退出、关闭读端 ⇒ tar 写 stdout 收到 EPIPE ⇒ pipefail 判失败。包越大越易触发 (80M 的 client 没事、800M 的 full 炸)。检测存在性一律 grep <pat> >/dev/null。

3.4 gh 在非 git 目录要显式 --repo

gh release download 在 /tmp/back 这类非 git 目录里会报 "not a git repository"(gh 从 cwd 的 git 上下文推断仓库)。必须 gh release download "$TAG" --repo "$GITHUB_REPOSITORY"。

3.5 gitcode 上传的路径语义

upload_assets.py 拼路径是 os.path.join(ASSET_DIR, name): 要 cd 进资产目录、ASSET_DIR=.、传裸文件名。SDK 仓的产物多数无 扩展名,必须显式列名(自动扫描按后缀识别,会静默一个都不传)。

3.6 gitcode 凭据(GITCODE_TOKEN)

CI 的 sync job 需要仓库 secret GITCODE_TOKEN;未配置时该 job 显式跳过 (不阻断 GitHub 侧发布)。

2026-09-29 已配置:两仓(JianFeeeee/HomeAgent、JianFeeeee/homeagentsdk) 均已设同名 secret,取值自 ~/.git-credentials 里那条 https://JianFeeeee:<token>@gitcode.com。 配置方式(经 stdin 传入,避免 token 出现在进程列表):

printf '%s' "$TOKEN" | gh secret set GITCODE_TOKEN --repo JianFeeeee/HomeAgent
printf '%s' "$TOKEN" | gh secret set GITCODE_TOKEN --repo JianFeeeee/homeagentsdk

验收:sync job 只在新版本发版时运行(prepare.outputs.exists == 'false'), 历史 tag 触发不了,所以无法用旧版本实跑。等价验证三道:

# ① secret 存在
gh secret list --repo JianFeeeee/HomeAgent | grep GITCODE_TOKEN
# ② token 有效
curl -s "https://gitcode.com/api/v5/user?access_token=$TOKEN"
# ③ job 用的 private-token 头可读 release(两仓都测)
curl -s -H "private-token: $TOKEN" \
  "https://gitcode.com/api/v5/repos/JianFeeeee/HomeAgent/releases/tags/v1.3.13"

★ token 是宽范围的个人令牌(可读 92 仓/48 私有、有写权限),而 CI 只需要这两个仓。 更稳的做法是去 gitcode 建一枚仅限这两仓的令牌再替换 —— 这样 CI 泄漏时 影响面不扩到其他仓。当前未做(按用户 2026-09-29 的决定)。

手工补发的完整流程(下载 GitHub 产物 → 建 release 条目 → 上传):

# token 放 ~/.git-credentials(https://JianFeeeee:<token>@gitcode.com)
ASSET_DIR=<产物目录> GITCODE_REPO=JianFeeeee/HomeAgent \
  python3 deploy/scripts/upload_assets.py <tag> <token>
# release 条目必须先存在(脚本向 releases/<tag>/upload_url 取 OBS 签名 URL)

4. SDK 仓(third_party/homeagent-sdk)

与主仓同构,差异:

主仓 SDK
版本源 internal/meta/meta.go meta/meta.go
产物 3 deb + 1 tar.gz(2.4GB) hmapdev_* 5 平台 + SHA256SUMS(~140MB)
CGO 必须(gojieba) 不需要(CGO_ENABLED=0 纯交叉)
测试 go test ./... 两处:根模块 + tools/hmapdev(独立 module,根的 ./... 不含它)
门 同一机制 同一机制([skip-release-tests]、幂等闸门)

发版:改 meta/meta.go 的 Version 推 release/vX.Y.x。SDK 版本随核心的 中版本走、patch 恒为 .0(见 git-branching.md §七.1)。

5. 通知

Actions 失败会给仓库 owner 发邮件(GitHub 默认)。若嫌吵: github.com/settings/notifications → Actions 关闭,或仓库页 Watch → Custom 取消 Actions。注意失败邮件也可能是"验证步骤自身 bug"的假警报 —— 先看是哪个 job/step 红了再判断(对照 §3 的坑)。

6. 开发环境的诊断噪音(cmd/gui [setup failed])

6.1 症状

每轮改完文件,pi-lens 的回合末摘要里会冒一条:

FAIL ./cmd/gui [setup failed]

它看着像仓库里有测试红了,实际是工具缺陷。真实状态:

go test ./...          # 退出码 0,43 个包全过、0 FAIL
find cmd/gui -name '*.go' | wc -l   # 0 —— 该目录根本没有 Go 代码
go test ./cmd/gui      # “no Go files in .../cmd/gui”
cd cmd/gui && npm test # 这才是它的测试(node 的 .test.mjs),通过

6.2 根因(pi-lens 的两个缺陷叠加)

  1. runner 按仓库根选,不按被跑的文件选。本仓根有 go.mod ⇒ 选中 go runner; 而 cmd/gui/*.test.mjs 命中通用测试命名(detectFileRole 与 runner 无关) ⇒ 对 cmd/gui 生成 go test -run . ./cmd/gui ⇒ 必失败。
  2. failed-first 把误报变成永久:失败项进 failedTestsByRunner(进程内 Map), 此后每次编辑都优先重跑它(与当前编辑的文件无关);而该条目只在测试 通过时才移除 ⇒ 对这条永远失败的命令,永不自愈。

日志里的形态(/root/.pi-lens/sessionstart.log):

turn_end: README.md → test go cmd/gui/sse-backoff.test.mjs (failed-first)

注意触发者是 README.md —— 目标是与本次编辑无关的陈旧失败项。

6.3 为什么不能用项目级配置关掉

.pi-lens.json 是项目级,只认一小排键 (ignore / rules / maxProjectFiles / reviewGraph / trivy + 三个改动开关)。 tests 是全局级键,写进项目文件会被忽略并告警:

"tests" is a global-only pi-lens setting and is not honored in a project .pi-lens.json

而全局关掉(~/.pi-lens/config.json 的 {"tests":{"enabled":false}}) 会一起关掉所有项目的回合末测试反馈 —— 为一个仓库的误报付全局代价,不值。 另:ignore 也挡不住,因为它只作用于扫描,不参与测试目标选择(failed-first 的回退分支根本不看候选文件)。

6.4 修法:本机补丁(已打)

补丁位置:~/.pi/agent/npm/node_modules/pi-lens/dist/index.js。 在 getTestRunTarget 返回目标前加一道校验:

该 runner 是否真能跑这个目标?只有 go 做实质检查 —— 目标所在目录要有至少一个 .go 文件。不能跑就返回 null, 并顺手把这条不可运行的记录从 failed-first 集合里移除。

原方法体改名为 selectTestRunTargetRaw,外面套一层校验(runTestFileAsync 只有这一个调用点,所以这里是唯一收口)。补丁全文已用 node --check 验语法, 用 /usr/bin/diff 核对为纯新增、零删除。

立即生效(不必重启会话):在会话里执行内置命令 /reload (重载扩展且不重启 pi-web-sessiond);不手动重载则在下个会话自然生效。

验证:改一个仓库文件但先不提交,等回合结束,然后 grep 'turn_end: .*→ test' /root/.pi-lens/sessionstart.log | tail -3 —— 应不再出现 test go cmd/gui/...;而编辑一个真 Go 测试文件时仍应正常触发。

会被覆盖:pi-lens 升级/重装后补丁消失,误报会回来(不影响仓库,只是噪音)。 备份在同目录 index.js.orig-*,回退就是拷回去:

cd ~/.pi/agent/npm/node_modules/pi-lens/dist
cp -a index.js.orig-<时间戳> index.js    # 然后 /reload

上游缺陷:runner 选择应先确认目标文件属于该语言(或至少确认目录内有该语言的源文件)。