Files
HomeAgent/.github/workflows/release.yml
JianFeeeee 192e63cf52 ci(release): gh release download 需显式 --repo(非 git 目录无法推断)
## 症状(第三次试发布)

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 仓跟踪管理(那边已跟踪),本仓不参与
构建,不需要在这边重复一份。未加规则时它会出现在本仓的未跟踪列表里。
2026-09-29 14:39:08 +08:00

399 lines
17 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.

# 发布流水线: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"