mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 06:47:29 +00:00
## 两个独立问题
### ① 卡死:20 分钟全花在 apt-get,不是门禁
实测证据:本机 `make check-csrc` 五个子门禁**各自 0~1 秒**:
csrc-lint 1s / csrc-abi 0s / csrc-headers 0s / csrc-sanitize 0s / csrc-cross 0s
而 CI 里同一个 job 两次跑到 20 分钟 job 级超时(其中一次直接 cancelled)。
卡死点只可能在 `apt-get update` —— ubuntu-latest runner 上偶发挂住不返回,
既不失败也没进展,只能等 job timeout,而报错信息毫无指向性。
### ② 假通过:缺工具时门禁 `[SKIP] ... exit 0`
这是更危险的一个。Makefile 里每道门禁在缺依赖时:
if ! command -v $CC_ARM64 >/dev/null 2>&1; then
echo " [SKIP] $CC_ARM64 不存在(未装交叉工具链)"; exit 0
fi
本机这样合理(开发者未必装交叉工具链),但在 CI 里意味着:apt-get 悄悄失败
⇒ 五道门禁全部跳过 ⇒ **job 绿** ⇒ 让人以为 C 侧有门禁,实际一个都没跑。
**静默降级比红更危险:红会有人修,绿没人看。**
实测确认(`CC_ARM64=nonexistent-gcc-xyz make csrc-cross`):
== C 侧交叉编译(linux/arm64)==
[SKIP] nonexistent-gcc-xyz 不存在(未装交叉工具链)
--- 退出码: 0 --- ← 这就是问题
## 加固内容
| 措施 | 作用 |
|---|---|
| 逐包检查是否已安装 | runner 镜像通常自带 cmake/clang ⇒ **多数情况完全不碰网络** |
| apt-get 自带 `Acquire::*Timeout=60` + `Retries=3` | 卡住时自身失败,而不是拖到 job 级超时(后者报错无指向性) |
| 装包 step `timeout-minutes: 6` | 双保险 |
| 新增「确认工具链齐备」步骤 | 缺任一工具 ⇒ 红,并 `::error::` 说明"缺工具时门禁会 SKIP 等于假装通过" |
| 跑门禁 step `timeout-minutes: 5` | 门禁实测 0~1 秒,超出即异常(不是慢) |
| `grep '\[SKIP\]'` 二次拦截 | 覆盖将来新增门禁时漏配依赖的情况 |
| job `timeout-minutes` 20 → 15 | 已经是纯 apt-get 余量 |
## 为什么要两道 SKIP 拦截
「确认工具链齐备」+「grep [SKIP]」看似重复,其实针对不同风险:
- 前者:**当前**三道门禁的依赖齐不齐
- 后者:**将来**新增门禁时,新依赖没写进安装步骤 —— 那时前者不知道要检查它,
只有日志里的 `[SKIP]` 能抓到
## 本地实测(逐条验证,不是只改完就推)
| 验证 | 结果 |
|---|---|
| YAML 可解析 | ✅ |
| 三件套本机齐备 | ✅ cmake / clang / aarch64-linux-gnu-gcc |
| `make check-csrc` 无 SKIP | ✅ 五门禁全实际执行 |
| 已装则跳过 apt-get 分支 | ✅ 「三件套已存在,跳过 apt-get」 |
| 部分缺失时拼装需要安装列表 | ✅ 正确识别出 `clang` |
| **变异:交叉工具链缺失** | ✅ grep 拦截生效(若不加这层,CI 会绿) |
377 lines
17 KiB
YAML
377 lines
17 KiB
YAML
# HologramAgent 主仓 CI。
|
||
#
|
||
# 设计原则:**CI 里跑的每一条命令,都是本地已实测通过的命令**。
|
||
# 不写「应该有用来试试」的步骤 —— 未验证的 CI 步骤会把假红灯变成常态,
|
||
# 最后所有人学会忽略它。
|
||
#
|
||
# 覆盖范围与本地 `make test` 对齐(build / vet / test / client-versions /
|
||
# gui / csrc),并按依赖拆成独立 job,便于失败定位。
|
||
#
|
||
# 明确**不在** CI 里跑的东西(依赖真机/密钥/内网,跑了只会变 flaky 噪音):
|
||
# - deploy-*.sh / homed 生产部署
|
||
# - waiter 真机验证(192.168.2.x)
|
||
# - cmd/gui 的 `npm run test-live`(需真 Electron + Xvfb + 真后端)
|
||
# - scripts/kernel-stress/*(需 llmsproxy 与压测端点)
|
||
# - 需要 DEEPSEEK_API_KEY / MEDIALIVE_* 的真实 LLM 测试(已自带 t.Skip)
|
||
name: CI
|
||
|
||
on:
|
||
push:
|
||
branches: [main, 'release/**']
|
||
pull_request:
|
||
workflow_dispatch:
|
||
|
||
# 只读权限:CI 不需要写仓库。
|
||
permissions:
|
||
contents: read
|
||
|
||
# 同一分支连续推送时取消旧跑,省额度也避免过期结果误导。
|
||
concurrency:
|
||
group: ci-${{ github.ref }}
|
||
cancel-in-progress: true
|
||
|
||
env:
|
||
# gojieba / onnx 相关包需要 cgo ⇒ 不能用 CGO_ENABLED=0。
|
||
CGO_ENABLED: 1
|
||
# 减少 go test 输出噪音。
|
||
GOFLAGS: -buildvcs=false
|
||
|
||
jobs:
|
||
# ── Go 后端:构建 + 静态检查 + 全量测试 + 跨平台客户端版本一致性 ──
|
||
go:
|
||
name: Go build / vet / test
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 30
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
|
||
- uses: actions/setup-go@v7
|
||
with:
|
||
go-version-file: go.mod
|
||
cache: true
|
||
|
||
# cgo 需要 gcc/g++(gojieba 会编译自带 C++ 源码)。
|
||
- name: 确认 cgo 工具链
|
||
run: |
|
||
gcc --version | head -1
|
||
g++ --version | head -1
|
||
|
||
- name: go build ./...
|
||
run: go build ./...
|
||
|
||
- name: go vet ./...
|
||
run: go vet ./...
|
||
|
||
# ./... 不点名 cmd/gui(该目录是纯 Electron,无 .go 文件):
|
||
# 显式 `go test ./cmd/gui` 会报 "no Go files",那是误报,不是缺陷。
|
||
- name: go test ./...
|
||
run: go test ./... -count=1 -timeout 20m
|
||
|
||
# 跨平台客户端版本一致性:内核 internal/meta 是唯一事实源,
|
||
# GUI(package.json) / 鸿蒙(AppScope/app.json5) / waiter 都必须跟它一致。
|
||
- name: 客户端版本一致性
|
||
run: make check-client-versions
|
||
|
||
# ── 竞态检测(并发改动的主要防线)──
|
||
race:
|
||
name: Race detector
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 20
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
- uses: actions/setup-go@v7
|
||
with:
|
||
go-version-file: go.mod
|
||
cache: true
|
||
- name: go test -race(并发核心)
|
||
run: |
|
||
go test -race \
|
||
./internal/agent/core/ ./cmd/waiter/ \
|
||
-count=1 -timeout 15m
|
||
|
||
# ── 交叉编译:可在无 cgo 下构建的客户端/工具 ──
|
||
#
|
||
# 只有这三个 cmd 支持纯交叉编译。另外三个依赖 cgo(gojieba / onnx):
|
||
# homed / memgc / homed-kb-migrate → internal/memory(gojieba)
|
||
# homed → internal/agent/api(onnx)
|
||
# 它们必须在原生平台构建(见 Makefile 的 build target)。
|
||
cross:
|
||
name: Cross-compile
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 20
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
include:
|
||
- goos: linux
|
||
goarch: amd64
|
||
ext: ""
|
||
- goos: linux
|
||
goarch: arm64
|
||
ext: ""
|
||
- goos: darwin
|
||
goarch: amd64
|
||
ext: ""
|
||
- goos: darwin
|
||
goarch: arm64
|
||
ext: ""
|
||
- goos: windows
|
||
goarch: amd64
|
||
ext: ".exe"
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
- uses: actions/setup-go@v7
|
||
with:
|
||
go-version-file: go.mod
|
||
cache: true
|
||
- name: 构建 ${{ matrix.goos }}/${{ matrix.goarch }}
|
||
env:
|
||
GOOS: ${{ matrix.goos }}
|
||
GOARCH: ${{ matrix.goarch }}
|
||
CGO_ENABLED: 0
|
||
run: |
|
||
set -euo pipefail
|
||
mkdir -p dist
|
||
for c in waiter initconfig mock-server; do
|
||
out="dist/${c}_${{ matrix.goos }}_${{ matrix.goarch }}${{ matrix.ext }}"
|
||
go build -trimpath -o "$out" "./cmd/${c}"
|
||
echo " ✓ ${c} ${{ matrix.goos }}/${{ matrix.goarch }}"
|
||
done
|
||
|
||
# ── Electron GUI(纯 Node 测试,零依赖)──
|
||
#
|
||
# `npm test` 跑五个 .mjs,全部只 import node: 内置模块(fs/url/path/vm),
|
||
# 所以**不需要 npm ci、不需要 electron**,秒级完成。
|
||
# `npm run test-live` 需真 Electron + 真后端 ⇒ 不进 CI。
|
||
#
|
||
# endpoint-align.test.mjs 跨出 cmd/gui 读 ../../internal/plugins/webui/handler.go
|
||
# 做端点对账 —— checkout 深度足够(默认 fetch-depth:1 已含工作树文件),
|
||
# 不需要额外 checkout 步骤。
|
||
#
|
||
# connections-io.test.mjs 同样是沙箱里跑 main.js 的**真实函数**(node:vm),
|
||
# 在临时目录里断言连接配置的加载/保存行为,不碰真实用户数据。
|
||
gui:
|
||
name: GUI (node)
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 10
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
- uses: actions/setup-node@v7
|
||
with:
|
||
node-version: '22'
|
||
- name: npm test
|
||
working-directory: cmd/gui
|
||
run: npm test
|
||
|
||
# ── C 基础设施门禁(ABI / 告警 / ASan+UBSan / 跨架构)──
|
||
csrc:
|
||
name: C infrastructure gates
|
||
runs-on: ubuntu-latest
|
||
# 门禁本身实测 0~1 秒(csrc-lint/abi/headers/sanitize/cross 各自 <1s)。
|
||
# 这 15 分钟全部是给 apt-get 的余量 —— 卡死历来只发生在装包阶段。
|
||
timeout-minutes: 15
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
|
||
# ── 安装 C 工具链 ─────────────────────────────────────────────
|
||
#
|
||
# ★ 加固点(2026-10-22):实测两次该 job 卡死 20 分钟并超时,
|
||
# 而**本地同一门禁 0~1 秒全通过**(make check-csrc)。卡死点全在
|
||
# apt-get:ubuntu-latest runner 上 `apt-get update` 偶发挂住不返回,
|
||
# 既没有失败也没有进展,只能等 job 级 timeout。
|
||
#
|
||
# 三重加固:
|
||
# 1. 逐包检查是否已安装 —— runner 镜像通常自带 cmake/clang,
|
||
# 已装就跳过 apt-get(多数情况直接走这条,完全不碰网络)
|
||
# 2. 需要装时给 apt-get 加自身的超时与重试,让它**自己失败**,
|
||
# 而不是把整个 job 拖到 job 级 timeout(后者报错信息毫无指向性)
|
||
# 3. 装完立即 `command -v` 复核,缺工具就红 —— 见下方"缺工具即失败"
|
||
- name: 安装 C 工具链(已装则跳过)
|
||
timeout-minutes: 6
|
||
run: |
|
||
set -uo pipefail
|
||
need=""
|
||
command -v cmake >/dev/null || need="$need cmake"
|
||
command -v clang >/dev/null || need="$need clang"
|
||
command -v aarch64-linux-gnu-gcc >/dev/null || need="$need gcc-aarch64-linux-gnu"
|
||
|
||
if [ -z "$need" ]; then
|
||
echo "✅ 三件套已存在,跳过 apt-get:$(command -v cmake clang aarch64-linux-gnu-gcc | tr '\n' ' ')"
|
||
exit 0
|
||
fi
|
||
|
||
echo "需要安装:$need"
|
||
# apt-get 自己的超时:卡住时 4 分钟内失败,而不是拖到 6 分钟 step 超时。
|
||
# 重试一次 —— runner 网络偶发抖动不该让门禁变红。
|
||
sudo apt-get -o Acquire::http::Timeout=60 \
|
||
-o Acquire::https::Timeout=60 \
|
||
-o Acquire::Retries=3 \
|
||
update -qq
|
||
# shellcheck disable=SC2086
|
||
sudo apt-get -o Acquire::http::Timeout=60 \
|
||
-o Acquire::https::Timeout=60 \
|
||
-o Acquire::Retries=3 \
|
||
install -y -qq --no-install-recommends $need
|
||
|
||
# ── 缺工具即失败(★ 这条是本次加固的核心)────────────────────
|
||
#
|
||
# 此前门禁在缺工具时打 `[SKIP] ... exit 0` —— **CI 照样变绿**。
|
||
# 那意味着:apt-get 悄悄失败(或将来有人删掉安装步骤)⇒ 五道门禁
|
||
# 全部跳过 ⇒ job 绿 ⇒ 让人以为 C 侧有门禁,实际一个都没跑。
|
||
#
|
||
# 静默降级比红更危险:红会有人修,绿没人看。
|
||
# 所以 CI 里显式要求工具必须在场,SKIP 视为失败。
|
||
- name: 确认 C 工具链齐备(缺则失败,不接受 SKIP)
|
||
run: |
|
||
set -euo pipefail
|
||
miss=""
|
||
for t in cmake clang aarch64-linux-gnu-gcc; do
|
||
if command -v "$t" >/dev/null 2>&1; then
|
||
echo " ✅ $t: $(command -v "$t")"
|
||
else
|
||
echo " ❌ $t 缺失" >&2
|
||
miss="$miss $t"
|
||
fi
|
||
done
|
||
[ -z "$miss" ] || {
|
||
echo "::error::C 工具链缺失:$miss —— 缺工具时门禁会 SKIP 并 exit 0,等于假装通过"
|
||
exit 1
|
||
}
|
||
|
||
# ── 跑门禁 ──────────────────────────────────────────────────
|
||
# 门禁自身 0~1 秒,给它 5 分钟绰绰有余;超出说明是异常(不是慢)。
|
||
- name: make check-csrc
|
||
timeout-minutes: 5
|
||
run: |
|
||
set -o pipefail
|
||
make check-csrc 2>&1 | tee /tmp/csrc.log
|
||
|
||
# ★ 二次确认:任何 [SKIP] 都视为失败。
|
||
#
|
||
# Makefile 里每道门禁在缺依赖时 `echo "[SKIP] ..."; exit 0`,
|
||
# 这在本机是合理的(开发者未必装了交叉工具链),但在 CI 里
|
||
# 就等于把"没验证"伪装成"通过"。上面那步已保证工具齐备,
|
||
# 这里再拦一次是为了覆盖将来新增门禁时漏配依赖的情况。
|
||
if grep -q '\[SKIP\]' /tmp/csrc.log; then
|
||
echo "::error::门禁出现 [SKIP],说明有检查没真正执行:"
|
||
grep '\[SKIP\]' /tmp/csrc.log >&2
|
||
echo "CI 里不接受 SKIP —— 要么装上依赖,要么明确从 check-csrc 摘掉这条。" >&2
|
||
exit 1
|
||
fi
|
||
echo "✅ 全部门禁实际执行,无 SKIP"
|
||
|
||
# ── 文档站构建(mkdocs,纯 Python,无外部依赖)──
|
||
docs:
|
||
name: Docs build
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 10
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
- uses: actions/setup-python@v7
|
||
with:
|
||
python-version: '3.12'
|
||
- name: 校验站点配置可解析
|
||
# 这里只做「配置与文档源没坏」的轻量校验,不做完整 mkdocs build
|
||
# (站点发布有独立流水线,见 deploy/scripts/deploy-sdk-site.sh)。
|
||
run: |
|
||
set -euo pipefail
|
||
if [ -f mkdocs.yml ]; then
|
||
python -c \
|
||
"import yaml; yaml.safe_load(open('mkdocs.yml'))" \
|
||
&& echo "mkdocs.yml OK"
|
||
else
|
||
echo "无 mkdocs.yml,跳过"
|
||
fi
|
||
test -d docs || echo "无 docs/,跳过"
|
||
|
||
# ── gitcode 镜像同步(main 分支)──
|
||
#
|
||
# 背景(2026-10-06 审计):gitcode 的定位是**只读镜像**(ci-cd-runbook §0),
|
||
# 但既有 sync job 只在 release.yml 里推 **tag** —— main 分支从不同步,
|
||
# 导致镜像的 main 停在 09-29、与主仓分叉 768/746(当时已 force push 对齐一次)。
|
||
#
|
||
# 本 job 把「镜像 = 主仓」交给 CI 保证:main 每次推送且**全部必需检查通过后**
|
||
# 才快进镜像分支 —— 红的代码不进镜像,镜像永远等于主仓。
|
||
#
|
||
# - 只处理 main(feature/release 分支不镜像,与 tag 语义一致)。
|
||
# - 未配置 GITCODE_TOKEN 时显式跳过(与 release.yml 的 sync-gitcode 同口径,
|
||
# 不阻断 GitHub 侧 CI)。
|
||
# - 用快进 push(非 force):正常流水线下镜像只可能落后,不可能领先;
|
||
# 若出现非快进说明有人动过镜像,此时响亮失败而不是悄悄覆盖 ——
|
||
# 镜像被旁路写入是「单向性被破坏」的信号,值得一次人工确认。
|
||
sync-gitcode-main:
|
||
name: Sync main to gitcode (mirror)
|
||
runs-on: ubuntu-latest
|
||
needs: [go, race, cross, gui, csrc, docs]
|
||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||
timeout-minutes: 10
|
||
env:
|
||
# ══════════════════════════════════════════════════════════════════
|
||
# ★★ gitcode 镜像目标仓名 —— **本 job 的唯一定义处**。
|
||
# 改 gitcode 仓名时只改这一行;下面所有消费点都走 ${GC_REPO}。
|
||
# 与 release.yml 的 sync-gitcode 保持同值(两个 workflow 各自独立
|
||
# 定义:改的时候两个文件都要改,但每个文件内部只有一个定义处)。
|
||
#
|
||
# 取值依据(实测 2026-10-21,直连不带代理,非推测):
|
||
# https://gitcode.com/JianFeeeee/HologramAgent → 200 ← 当前用这个
|
||
# https://gitcode.com/JianFeeeee/HologramAgent → 301 ← 旧名,跳转到上面
|
||
# https://gitcode.com/JianFeeeee/hologramagentsdk → 200(SDK 仓,另一对象)
|
||
# https://gitcode.com/JianFeeeee/homeagentsdk → 404
|
||
#
|
||
# 背景:gitcode 侧镜像仓已由 HologramAgent 改名为 HologramAgent(与 GitHub
|
||
# 主仓同名)。旧地址仍 301 跳转,跟随后也能到达,但走重定向 ⇒ 别再用它。
|
||
# 判定两仓同一对象:两地址 git ls-remote 的 refs 列表 md5 相同。
|
||
#
|
||
# ⚠ 失败面:本 job 的 needs 是 [go, race, cross, gui, csrc, docs]。
|
||
# 前置检查 exit 1 ⇒ 本 job 红 ⇒ **main 的整条 CI 红**(不是只红一格)。
|
||
# release.yml 的 sync-gitcode 只红在镜像同步这一格(GitHub 侧产物已发)。
|
||
# ══════════════════════════════════════════════════════════════════
|
||
GC_REPO: JianFeeeee/HologramAgent
|
||
steps:
|
||
- id: tok
|
||
name: 检查 gitcode 凭据
|
||
run: |
|
||
if [ -n "${{ secrets.GITCODE_TOKEN }}" ]; then
|
||
echo "ok=true" >> "$GITHUB_OUTPUT"
|
||
else
|
||
echo "ok=false" >> "$GITHUB_OUTPUT"
|
||
echo " 未配置 GITCODE_TOKEN —— 跳过 gitcode main 同步"
|
||
fi
|
||
- uses: actions/checkout@v7
|
||
if: steps.tok.outputs.ok == 'true'
|
||
with:
|
||
# 必须取完整历史:镜像可能在任意旧提交上,快进需要共同祖先。
|
||
fetch-depth: 0
|
||
# ★ 改名(HologramAgent → HologramAgent)后的必然过渡态:gitcode 上的镜像仓
|
||
# 是**另一个账号下的独立仓库**,改 GitHub 仓名不会自动建出它。
|
||
# 若不在这里显式探测,缺少目标仓时只会得到 git 的原始报错
|
||
# ("repository not found" 混在 push 输出里),看不出是「仓没建」。
|
||
# 语义:token 未配置 = 可选项跳过(上面那步);token 有但仓不存在 = 硬失败。
|
||
#
|
||
# ★ 有界重试(2026-10-21 dfc4e16 的教训):首次跑这一步时 gitcode 网络抖动,
|
||
# 一次 ls-remote 判失败 ⇒ job 红;重跑立刻全绿。现在只对**网络层**失败
|
||
# 重试(3 次,退避 5s/10s),而「仓不存在」与「凭据问题」**立即失败不重试**
|
||
# —— 后两者重试只会把真问题拖长、并把它埋在噪声里。
|
||
#
|
||
# 实现落在 .github/scripts/check-gitcode-repo.sh,与 release.yml 的
|
||
# sync-gitcode **共用同一个文件**(同口径靠同一份代码保证,不靠人抄两遍)。
|
||
- name: 前置检查 gitcode 目标仓是否存在
|
||
if: steps.tok.outputs.ok == 'true'
|
||
env:
|
||
GC_TOKEN: ${{ secrets.GITCODE_TOKEN }}
|
||
# GC_REPO 来自 job 级 env(唯一定义处),此处不重复字面量。
|
||
run: |
|
||
set -euo pipefail
|
||
bash .github/scripts/check-gitcode-repo.sh
|
||
- name: 推 main 到 gitcode(快进)
|
||
if: steps.tok.outputs.ok == 'true'
|
||
env:
|
||
GC_TOKEN: ${{ secrets.GITCODE_TOKEN }}
|
||
run: |
|
||
set -euo pipefail
|
||
GC_URL="https://JianFeeeee:${GC_TOKEN}@gitcode.com"
|
||
# GC_REPO 来自 job 级 env(唯一定义处),此处不重复字面量。
|
||
git push "${GC_URL}/${GC_REPO}.git" \
|
||
"refs/heads/main:refs/heads/main"
|
||
echo " gitcode/main 已快进到 ${GITHUB_SHA}"
|