Files
HomeAgent/.github/workflows/ci.yml
JianFeeeee 16bb794a43 ci: 二次加固 —— 上一版对 apt 卡死无效,改用 timeout 硬超时并去掉 -qq
## 上一版为什么没解决

第一版加了 `Acquire::http::Timeout=60` + `Retries=3` + step 6 分钟超时。
CI 实测结果:

    15:39:48  需要安装: gcc-aarch64-linux-gnu
    15:46:01  ##[error]The action '安装 C 工具链(已装则跳过)' has timed out after 6 minutes.

**依然卡满 6 分钟**,而且日志里**一个 apt 输出都没有** —— 因为 `-qq` 把
所有输出静默了。两个结论:

1. `Acquire::*Timeout` 只管**单个连接**,不管 apt 整体 —— 源重试、DPKG 锁
   等待、依赖计算都不受它约束,所以它拦不住这类卡死
2. `-qq` 让卡死**完全不可观测**:既看不到卡在哪个源,也看不到在下载什么。
   不可观测正是它能悄悄拖满超时的原因

(顺带证明"已装则跳过"这条是有效的:cmake/clang 都在镜像里,只列出了
`gcc-aarch64-linux-gnu` 一项要装。)

## 这一版改法

| 改动 | 理由 |
|---|---|
| 去掉 `-qq`,保留可读进度 | 下次卡死必须能看出卡在哪,否则无从诊断 |
| `timeout 120 apt-get update` | **外层硬超时**,强制结束,不依赖 apt 自身行为 |
| `timeout 180 apt-get install` | 同上;120+180=300s 配 step 5 分钟正好 |
| 失败即 `exit 1` + `::error::`,不重试整段 | 宁可红一次让人看见,不要用重试掩盖真实问题 |
| `--no-install-recommends` 保留 | 减少下载量与依赖面 |

## 本地实测

| 验证 | 结果 |
|---|---|
| YAML 可解析 | ✅ |
| `timeout 3 sleep 60` 行为 | ✅ 3 秒强制结束,退出码 124,可被 `if !` 捕获 |
| "已装则跳过"分支 | ✅ 「✅ 跳过 apt-get」 |
| 上一版缺工具 `[SKIP] ... exit 0` 风险 | ✅ 已复现并拦截(见前一提交) |

## 仍待 CI 验证

这一版的真实效果只有在 runner 上才知。若这次仍然卡满,说明 `timeout` 对
sudo+apt 组合失效(可能性低),届时改用「换源」或「预装镜像」方案。
2026-10-22 23:47:15 +08:00

391 lines
18 KiB
YAML
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.

# 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 工具链(已装则跳过)
# 120s(update) + 180s(install) = 300s,给 5 分钟正好;
# 超出说明 timeout 命令本身没生效,此时应当红而不是继续等。
timeout-minutes: 5
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"
# ★ 2026-10-22 实测:加 Acquire::*Timeout=60 + Retries=3 之后仍然卡满
# 6 分钟 step 超时,且**日志里一个 apt 输出都没有** —— 因为 `-qq`
# 把一切静默了。卡死不可观测正是它拖满超时的原因:既看不到卡在哪个
# 源,也看不到到底在下载什么。
#
# 所以:
# · 去掉 `-qq`,保留可读进度(否则下次卡死仍然无从诊断)
# · 用 `timeout` 命令做外层硬超时 —— Acquire::Timeout 只管单个连接,
# 不管 apt 整体(源重试、DPkg 锁等待都不受它约束)
# · 失败后**不重试整段**:宁可红一次让人看见,也不要为偶发网络
# 抖动掩盖真实问题
APT_OPTS="-o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 -o Acquire::Retries=2"
# shellcheck disable=SC2086
if ! timeout 120 sudo apt-get $APT_OPTS update; then
echo "::error::apt-get update 超时/失败(120s)—— runner 网络或源不可用"
exit 1
fi
# shellcheck disable=SC2086
if ! timeout 180 sudo apt-get $APT_OPTS install -y --no-install-recommends $need; then
echo "::error::apt-get install 超时/失败(180s)需要安装:$need"
exit 1
fi
# ── 缺工具即失败(★ 这条是本次加固的核心)────────────────────
#
# 此前门禁在缺工具时打 `[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}"