mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-29 22:12:52 +00:00
## 症状(第三次试发布)
Build 全绿、tag 已建、release 已建、2.40GB 附件全部上传成功 ——
唯独最后一道「回读校验」失败,整个 workflow 因此标记为 failure:
failed to run git: fatal: not a git repository (or any of the
parent directories): .git
##[error]Process completed with exit code 1.
## 根因
回读校验为了"下一份干净副本"先 `cd /tmp/back`,那里不是 git 仓库。
而 `gh release download` 默认从**当前目录的 git 上下文**推断仓库与
host(GITHUB_REPOSITORY / GH_HOST 之类环境变量不足以让它跳过推断),
于是报 "not a git repository"。
## 修法
gh release download "$TAG" --repo "$GITHUB_REPOSITORY"
两个仓的 workflow 都有同一处(主仓 + SDK),一并修。
## 顺带
`third_party/homeagent-sdk/.github/` 加入 .gitignore —— 与 skills/ 同类:
SDK 仓自己的 workflow 由 SDK 仓跟踪管理(那边已跟踪),本仓不参与
构建,不需要在这边重复一份。未加规则时它会出现在本仓的未跟踪列表里。
399 lines
17 KiB
YAML
399 lines
17 KiB
YAML
# 发布流水线:release/** 分支推送即发版。
|
||
#
|
||
# 设计依据 docs/git-branching.md §七(发版产物清单)与 git-release-discipline
|
||
# skill。核心事实:**推 tag ≠ 完成发版** —— 完整发版是四件事:
|
||
# bump meta.Version → 打 tag → 打包产物 → 建 release 条目并上传附件。
|
||
# (v1.3.1–v1.3.6 曾只推了 tag,产物与 release 条目全缺,事后补做。)
|
||
#
|
||
# 版本号来源:internal/meta/meta.go 的 Version(唯一事实源)。
|
||
# 所以发版动作 = 在 release/vX.Y.x 上把 meta.Version 改成目标版本后推送。
|
||
# 版本未变的推送(如改文档)会因 tag 已存在而**整轮跳过**,不会重复发版。
|
||
#
|
||
# 发版前的 go test 门可以显式跳过(见下面 skip_tests 的说明)。
|
||
name: Release
|
||
|
||
on:
|
||
push:
|
||
branches: ['release/**']
|
||
workflow_dispatch:
|
||
inputs:
|
||
skip_tests:
|
||
description: '跳过发版前的 go test 门(仅用于已知红的历史维护线)'
|
||
type: boolean
|
||
default: false
|
||
|
||
# 发布必须能写仓库(打 tag、建 release、传附件)。
|
||
permissions:
|
||
contents: write
|
||
|
||
# 发布不允许并发/取消:半途中断会留下 tag 存在但附件不全的状态。
|
||
concurrency:
|
||
group: release-${{ github.ref }}
|
||
cancel-in-progress: false
|
||
|
||
env:
|
||
# gojieba 需要 cgo;onnxruntime 版本经 dlopen 加载,编译期无需装 ORT。
|
||
CGO_ENABLED: 1
|
||
GOFLAGS: -buildvcs=false
|
||
# CI 用的大资产(模型/运行库)存于这个 release。
|
||
ASSETS_TAG: ci-assets-v1
|
||
|
||
jobs:
|
||
# ── 读版本号并判断是否需要发版 ──
|
||
prepare:
|
||
name: Prepare
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 10
|
||
outputs:
|
||
version: ${{ steps.ver.outputs.version }}
|
||
tag: ${{ steps.ver.outputs.tag }}
|
||
prerelease: ${{ steps.ver.outputs.prerelease }}
|
||
exists: ${{ steps.ver.outputs.exists }}
|
||
skip_tests: ${{ steps.ver.outputs.skip_tests }}
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
fetch-depth: 0
|
||
# 发版前的 go test 门为什么可以跳过:
|
||
#
|
||
# 新旧发布线的测试健康状况不同。实测 release/v1.3.x(历史维护线)上
|
||
# internal/plugins 的 TestRealPlugin_DeepSearchKeepsSharedBackendOnStop
|
||
# 失败、GUI 尚无 npm test 脚本、csrc 基础设施不存在 —— 而这三项在 main
|
||
# 上都正常。给旧线补新流水线等于用今天的门去量旧代码,硬门会让该线
|
||
# **完全无法发版**。
|
||
#
|
||
# 故:默认严格(测试必跑);发版人若确知该线测试是既存红的,可在
|
||
# 发版 commit 里写 [skip-release-tests] 显式跳过 —— 决定因此记录在
|
||
# **定义该次发版的那个 commit** 里,git 历史可审计。
|
||
# go build 仍是硬门(产物不可能建立在编译失败的代码上)。
|
||
- id: ver
|
||
name: 读取 meta.Version 并检查 tag
|
||
run: |
|
||
set -euo pipefail
|
||
V=$(sed -n 's/^[[:space:]]*Version = "\(.*\)"/\1/p' \
|
||
internal/meta/meta.go | head -1)
|
||
if [ -z "$V" ]; then
|
||
echo "ERROR: 无法从 internal/meta/meta.go 读出 Version"
|
||
exit 1
|
||
fi
|
||
echo "version=$V" >> "$GITHUB_OUTPUT"
|
||
echo "tag=v$V" >> "$GITHUB_OUTPUT"
|
||
# SemVer 预发布(1.3.13-beta.1)⇒ release 标记为预发布
|
||
case "$V" in
|
||
*-*) echo "prerelease=true" >> "$GITHUB_OUTPUT" ;;
|
||
*) echo "prerelease=false" >> "$GITHUB_OUTPUT" ;;
|
||
esac
|
||
# 幂等闸门:tag 已存在说明该版本发过了,整轮跳过。
|
||
if git ls-remote --exit-code --tags origin "refs/tags/v$V" \
|
||
>/dev/null 2>&1; then
|
||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||
echo " tag v$V 已存在 —— 跳过发版"
|
||
else
|
||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||
echo " 将为 v$V 发版"
|
||
fi
|
||
|
||
# 是否跳过发版前的 go test 门(默认不跳)。
|
||
# 两个来源:手动触发的输入,或发版 commit 里的显式标记。
|
||
# 后者使决定落在定义该次发版的 commit 上,可以从 git 历史审计。
|
||
#
|
||
# 标记查在**改动 meta.Version 的那个提交**上,而不是 HEAD:
|
||
# 发版提交之后往往还会跟几个提交(如同步 workflow、改文档),
|
||
# 若只看 HEAD,标记就会被后续提交顶掉,静默失效。
|
||
SKIP="${{ inputs.skip_tests }}"
|
||
MARKER=0
|
||
REL_COMMIT=$(git log -1 --format=%H -- internal/meta/meta.go)
|
||
REL_MSG=$(git log -1 --pretty=%B "$REL_COMMIT")
|
||
case "$REL_MSG" in
|
||
*'[skip-release-tests]'*) MARKER=1 ;;
|
||
*) MARKER=0 ;;
|
||
esac
|
||
echo " 发版提交: ${REL_COMMIT:0:12}"
|
||
if [ "$SKIP" = "true" ] || [ "$MARKER" = "1" ]; then
|
||
echo "skip_tests=true" >> "$GITHUB_OUTPUT"
|
||
echo ""
|
||
echo " ⚠️ **已请求跳过发版前的 go test 门**"
|
||
echo " 来源:${SKIP} = true / commit 标记 = $MARKER"
|
||
echo " 后果:产物可能建立在单元测试失败的代码上。"
|
||
echo " 理由应当记录在发版 commit 的正文里。"
|
||
else
|
||
echo "skip_tests=false" >> "$GITHUB_OUTPUT"
|
||
echo " 发版前会跑 go test 门(可用 [skip-release-tests] 标记跳过)"
|
||
fi
|
||
|
||
# ── 构建 Linux 产物(amd64)──
|
||
#
|
||
# 三个 deb + 一个 tar.gz,总约 2.4GB(server/full/tar 含 719MB 模型)。
|
||
# 编译不需要 ONNX Runtime —— onnxruntime_go 是 dlopen 方式,运行期才加载
|
||
# libonnxruntime.so;但**打包**需要它(要打进 deb),故从 ASSETS_TAG 下载。
|
||
build-linux:
|
||
name: Build linux/amd64
|
||
needs: prepare
|
||
if: needs.prepare.outputs.exists == 'false'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 120
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- uses: actions/setup-go@v7
|
||
with:
|
||
go-version-file: go.mod
|
||
cache: true
|
||
|
||
- name: 确认 cgo 工具链
|
||
run: |
|
||
gcc --version | head -1
|
||
g++ --version | head -1
|
||
|
||
# 发版前的门。
|
||
#
|
||
# go build 是**硬门**:产物不可能建立在编译失败的代码上。
|
||
# go test 默认也跑,但可在发版 commit 里写 [skip-release-tests] 跳过
|
||
# —— 历史维护线的既有红测试不应阻断该线的一切发版(详见 prepare job)。
|
||
- name: go build(硬门)
|
||
run: |
|
||
set -euo pipefail
|
||
go build ./...
|
||
|
||
- name: go test(发版前验证)
|
||
if: needs.prepare.outputs.skip_tests != 'true'
|
||
run: go test ./... -count=1 -timeout 20m
|
||
|
||
- name: go test 被跳过(显式声明的后果)
|
||
if: needs.prepare.outputs.skip_tests == 'true'
|
||
run: |
|
||
echo "::warning title=go test 门已跳过::本次发版未跑 go test,产物可能建立在单元测试失败的代码上。"
|
||
|
||
# GUI 依赖 Electron 运行时。打包脚本从两处找它:
|
||
# 1) ~/.cache/electron 里的 electron-v<ver>-linux-<arch>.zip
|
||
# 2) cmd/gui/node_modules/electron/dist(同架构时)
|
||
# 全新 runner 两处都没有 —— 而脚本在都没有时**只能跳过 GUI**,
|
||
# 于是 client/full 包会静默地不含界面(这正是脚本作者担心的“假包”)。
|
||
# 所以这里显式装一份:npm 会解析出 ^33.0.0 的实际版本并落到
|
||
# node_modules,脚本便走第 2 条路径(runner 是 amd64 == 目标架构)。
|
||
#
|
||
# ⚠️ 不能用 `npm install --production`(那会跳过 devDependencies,
|
||
# 而 electron 正是 devDependency)。
|
||
- uses: actions/setup-node@v7
|
||
with:
|
||
node-version: '22'
|
||
|
||
- name: 安装 Electron(GUI 打包需要)
|
||
working-directory: cmd/gui
|
||
run: |
|
||
set -euo pipefail
|
||
# 用 npm ci 而非 `npm install electron@<range>`:后者是非确定性的
|
||
# (range 会随上游发布漂到新版本),且锁不住传递依赖。
|
||
# package-lock.json 里已锁定 electron(lockfileVersion 3),
|
||
# ci 严格按 lock 安装,同一个 commit 永远得到同一套依赖。
|
||
npm ci --no-audit --no-fund
|
||
test -f node_modules/electron/dist/electron
|
||
echo " 已就绪:$(node_modules/electron/dist/electron --version)"
|
||
|
||
- name: 下载构建资产(模型 + ONNX Runtime)
|
||
run: |
|
||
set -euo pipefail
|
||
BASE="https://github.com/${GITHUB_REPOSITORY}/releases/download/${ASSETS_TAG}"
|
||
mkdir -p /tmp/assets/model /tmp/assets/ort
|
||
for f in chinese-clip-vit-b16-onnx.tar \
|
||
onnxruntime-linux-amd64-1.28.0.tar SHA256SUMS; do
|
||
echo " 下载 $f"
|
||
curl -sSL --retry 3 -o "/tmp/assets/$f" "$BASE/$f"
|
||
done
|
||
# 校验(资产是构建输入,损坏会打出坏包)
|
||
(cd /tmp/assets && sha256sum -c SHA256SUMS)
|
||
tar -xf /tmp/assets/chinese-clip-vit-b16-onnx.tar \
|
||
-C /tmp/assets/model
|
||
ORT_TAR=/tmp/assets/onnxruntime-linux-amd64-1.28.0.tar
|
||
tar -xf "$ORT_TAR" -C /tmp/assets/ort
|
||
echo " 模型文件:"
|
||
ls /tmp/assets/model/chinese-clip-vit-b16-onnx
|
||
echo " ORT 文件:"
|
||
ls /tmp/assets/ort
|
||
|
||
- name: 打包(tar.gz + full/server/client deb)
|
||
env:
|
||
VERSION: ${{ needs.prepare.outputs.version }}
|
||
CHINESECLIP_BUNDLE_DIR: /tmp/assets/model/chinese-clip-vit-b16-onnx
|
||
ONNXRUNTIME_ASSET_DIR: /tmp/assets/ort
|
||
run: |
|
||
set -euo pipefail
|
||
bash deploy/packaging/package-linux.sh amd64 all
|
||
|
||
- name: 平铺产物(附件必须同目录,SHA256SUMS 用平铺名)
|
||
run: |
|
||
set -euo pipefail
|
||
mkdir -p /tmp/out
|
||
cp dist/linux/deb/*.deb /tmp/out/
|
||
cp dist/linux/tar/*.tar.gz /tmp/out/
|
||
cp dist/linux/SHA256SUMS /tmp/out/
|
||
echo " 产物:"
|
||
for f in /tmp/out/*; do
|
||
printf " %8.1fMB %s\n" \
|
||
"$(stat -c %s "$f" | awk '{print $1/1048576}')" "$(basename "$f")"
|
||
done
|
||
|
||
- name: 验证产物(deb 元数据 + 校验和自验)
|
||
run: |
|
||
set -euo pipefail
|
||
cd /tmp/out
|
||
for f in *.deb; do
|
||
echo " $f"
|
||
dpkg-deb -f "$f" Package Version Architecture | sed 's/^/ /'
|
||
done
|
||
# full/server 必须真的带模型,否则是“默认启用但装完不能用”的假包。
|
||
#
|
||
# ★ 不能用 grep -q:它匹配到就退出,关闭管道读端,dpkg-deb 内部
|
||
# 的 tar 写 stdout 时收到 EPIPE(“stdout: write error”),
|
||
# 在 pipefail 下整条 pipeline 变成失败 —— 检测项本身是好的,
|
||
# 却被检测手段误杀(首次试发布就死在这里)。改用 >/dev/null,
|
||
# grep 会读完整个输入再退出,不产生 SIGPIPE。
|
||
dpkg-deb -c homeagent-full_*_amd64.deb \
|
||
| grep "chinese-clip-vit-b16-onnx/TextEncoder.onnx" >/dev/null
|
||
echo " ✓ full 包含模型"
|
||
dpkg-deb -c homeagent-full_*_amd64.deb \
|
||
| grep "libonnxruntime.so" >/dev/null
|
||
echo " ✓ full 包含 ONNX Runtime"
|
||
dpkg-deb -c homeagent-server_*_amd64.deb \
|
||
| grep "chinese-clip-vit-b16-onnx/TextEncoder.onnx" >/dev/null
|
||
echo " ✓ server 包含模型"
|
||
sha256sum -c SHA256SUMS
|
||
|
||
- uses: actions/upload-artifact@v7
|
||
with:
|
||
name: linux-amd64
|
||
path: /tmp/out/*
|
||
retention-days: 7
|
||
if-no-files-found: error
|
||
|
||
# ── 建 tag、建 release、上传附件 ──
|
||
publish:
|
||
name: Publish
|
||
needs: [prepare, build-linux]
|
||
if: needs.prepare.outputs.exists == 'false'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 60
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- uses: actions/download-artifact@v8
|
||
with:
|
||
name: linux-amd64
|
||
path: dist
|
||
|
||
- name: 打 tag(打在触发本次发版的 commit 上)
|
||
env:
|
||
TAG: ${{ needs.prepare.outputs.tag }}
|
||
run: |
|
||
set -euo pipefail
|
||
git config user.name "github-actions[bot]"
|
||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||
git tag -a "$TAG" -m "$TAG"
|
||
git push origin "$TAG"
|
||
|
||
- name: 建 release 并上传附件
|
||
env:
|
||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
TAG: ${{ needs.prepare.outputs.tag }}
|
||
VERSION: ${{ needs.prepare.outputs.version }}
|
||
PRE: ${{ needs.prepare.outputs.prerelease }}
|
||
run: |
|
||
set -euo pipefail
|
||
cd dist
|
||
FLAGS=()
|
||
[ "$PRE" = "true" ] && FLAGS+=(--prerelease)
|
||
gh release create "$TAG" \
|
||
--title "$TAG" \
|
||
--notes "HomeAgent $VERSION
|
||
|
||
产物清单与校验见 SHA256SUMS。
|
||
- \`homeagent_${VERSION}_linux_amd64.tar.gz\` — 内核 + CLI + GUI 打包
|
||
- \`homeagent-client_${VERSION}_amd64.deb\` — 客户端
|
||
- \`homeagent-server_${VERSION}_amd64.deb\` — 服务端(含向量模型)
|
||
- \`homeagent-full_${VERSION}_amd64.deb\` — 全量" \
|
||
"${FLAGS[@]}" \
|
||
./*.deb ./*.tar.gz ./SHA256SUMS
|
||
echo "=== release 内容 ==="
|
||
gh release view "$TAG" --json assets \
|
||
--jq '.assets[] | " \(.name) \(.size) 字节"'
|
||
|
||
- name: 回读校验(下载回来验证附件可读且校验和成立)
|
||
env:
|
||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
TAG: ${{ needs.prepare.outputs.tag }}
|
||
run: |
|
||
set -euo pipefail
|
||
mkdir -p /tmp/back
|
||
cd /tmp/back
|
||
# ★ 必须显式 --repo:gh 默认从**当前目录的 git 上下文**推断仓库,
|
||
# 而 /tmp/back 不是 git 仓库 ⇒ 报
|
||
# "failed to run git: fatal: not a git repository"。
|
||
# 首次试发布就死在这里 —— 产物其实全部上传成功(tag 与
|
||
# release 已建、2.40GB 附件齐备),只是这道回读校验自己失败了。
|
||
gh release download "$TAG" --repo "$GITHUB_REPOSITORY"
|
||
for f in *; do
|
||
printf " %8.1fMB %s\n" \
|
||
"$(stat -c %s "$f" | awk '{print $1/1048576}')" "$f"
|
||
done
|
||
sha256sum -c SHA256SUMS
|
||
echo " ✓ 回读校验通过"
|
||
|
||
# ── 同步到 gitcode(国内镜像)──
|
||
#
|
||
# 需要仓库 secret GITCODE_TOKEN;未配置则跳过(不阻断 GitHub 侧发布)。
|
||
# gitcode 的 release 附件是"同名只写一次",故只在此处上传一次。
|
||
sync-gitcode:
|
||
name: Sync to gitcode
|
||
needs: [prepare, publish]
|
||
if: needs.prepare.outputs.exists == 'false'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 60
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
- 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 同步"
|
||
fi
|
||
- uses: actions/download-artifact@v8
|
||
if: steps.tok.outputs.ok == 'true'
|
||
with:
|
||
name: linux-amd64
|
||
path: dist
|
||
- name: 推 tag 与附件到 gitcode
|
||
if: steps.tok.outputs.ok == 'true'
|
||
env:
|
||
GC_TOKEN: ${{ secrets.GITCODE_TOKEN }}
|
||
TAG: ${{ needs.prepare.outputs.tag }}
|
||
run: |
|
||
set -euo pipefail
|
||
# 1) 推 tag(附件上传前 release 条目必须先存在)
|
||
git config user.name "github-actions[bot]"
|
||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||
git tag -a "$TAG" -m "$TAG" 2>/dev/null || true
|
||
GC_URL="https://JianFeeeee:${GC_TOKEN}@gitcode.com"
|
||
git push "${GC_URL}/JianFeeeee/HomeAgent.git" "$TAG"
|
||
# 2) 建 release 条目
|
||
curl -sS --max-time 60 -X POST \
|
||
-H "private-token: ${GC_TOKEN}" \
|
||
-H "Content-Type: application/json" \
|
||
"https://gitcode.com/api/v5/repos/JianFeeeee/HomeAgent/releases" \
|
||
-d "{\"tag_name\":\"$TAG\",\"body\":\"同步自 GitHub\"}" \
|
||
-o /tmp/.gcrel -w " 建 release → %{http_code}\n"
|
||
# 3) 上传附件(用仓库既有脚本,它处理 OBS 预签名两步流程)
|
||
cd dist
|
||
# 脚本的路径语义是 os.path.join(ASSET_DIR, name)。
|
||
# 不传文件名时它扫描 ASSET_DIR 并按后缀识别发布产物 —— 这里正合适
|
||
# (.deb/.tar.gz/SHA256SUMS 都在白名单里)。
|
||
# 不要传 "./x" 或 "dist/x":那会拼成 dist/dist/x。
|
||
ASSET_DIR=. GITCODE_REPO=JianFeeeee/HomeAgent \
|
||
python3 ../deploy/scripts/upload_assets.py "$TAG" "$GC_TOKEN"
|