Skip to content

chore: bump version to 2.8.1 #86

chore: bump version to 2.8.1

chore: bump version to 2.8.1 #86

Workflow file for this run

name: 自动打包发布
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
build-and-release:
name: 构建并发布 - ${{ matrix.name }}
strategy:
fail-fast: false
matrix:
include:
# macOS Apple Silicon 包(原生)
- name: macOS (Apple Silicon)
platform: macos-latest
tauriArgs: '--target aarch64-apple-darwin'
# macOS 通用包(Universal 2:arm64 + x86_64)
- name: macOS (Universal)
platform: macos-latest
tauriArgs: '--target universal-apple-darwin'
# Windows 安装包(使用 NSIS,避免 WiX/Light.exe 失败导致整条流水线挂掉)
- name: Windows (x64)
platform: windows-latest
tauriArgs: '--bundles nsis'
runs-on: ${{ matrix.platform }}
env:
# Tauri Updater 签名密钥(用于生成 *.sig 与 latest.json)
# 需要在仓库 Secrets 中配置:TAURI_SIGNING_PRIVATE_KEY / TAURI_SIGNING_PRIVATE_KEY_PASSWORD(可为空)
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
steps:
- name: 拉取源代码
uses: actions/checkout@v4
- name: 安装系统依赖 (Ubuntu)
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: 获取当前时间 (北京时间)
shell: bash
run: |
# macOS runner 使用 BSD date,不支持 GNU 的 `-d` 参数;用 TZ 方式兼容 macOS/Linux
BUILD_TIME=$(TZ=Asia/Shanghai date "+%Y-%m-%d %H:%M:%S")
echo "BUILD_TIME=$BUILD_TIME" >> $GITHUB_ENV
if: runner.os != 'Windows'
- name: 获取当前时间 (北京时间 - Windows)
shell: pwsh
run: |
$buildTime = (Get-Date).ToUniversalTime().AddHours(8).ToString("yyyy-MM-dd HH:mm:ss")
echo "BUILD_TIME=$buildTime" >> $env:GITHUB_ENV
if: runner.os == 'Windows'
- name: 安装 Node.js 环境
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: './desktop/package-lock.json'
- name: 安装 Rust 环境
uses: dtolnay/rust-toolchain@stable
with:
# macOS 通用包需要同时具备 arm64 + x86_64 两个 target
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: 获取完整 Git 历史
run: git fetch --prune --unshallow || git fetch --prune
- name: Rust 缓存
uses: swatinem/rust-cache@v2
with:
workspaces: './desktop/src-tauri -> target'
- name: 安装 Go 环境
uses: actions/setup-go@v5
with:
go-version: '1.21'
cache-dependency-path: './backend/go.sum'
- name: 生成 Release Notes
id: release_notes
shell: bash
run: |
# 获取上一个 tag,如果不存在则获取第一个 commit
PREV_TAG=$(git describe --tags --abbrev=0 ${{ github.ref_name }}^ 2>/dev/null || git rev-list --max-parents=0 HEAD)
echo "Previous tag: $PREV_TAG"
# 提取 commit 记录(不包括 merge commit)
# --no-merges: 只显示实际代码变更的 commit,过滤掉 PR merge 产生的合并提交
LOGS=$(git log $PREV_TAG..${{ github.ref_name }} --pretty=format:"%s" --no-merges)
# 初始化分类内容
FEATS=""
FIXES=""
DOCS=""
OTHERS=""
# 按前缀归类
while IFS= read -r line; do
[ -z "$line" ] && continue
# 清理可能导致 Markdown 渲染问题的特殊字符(如反斜杠、反引号等)
# 这里简单处理,保证基础展示正常
clean_line=$(echo "$line" | sed 's/\\/\\\\/g; s/`/\\`/g')
if [[ $line =~ ^feat ]]; then
FEATS="$FEATS\n- ${clean_line#feat: }"
elif [[ $line =~ ^fix ]]; then
FIXES="$FIXES\n- ${clean_line#fix: }"
elif [[ $line =~ ^docs ]]; then
DOCS="$DOCS\n- ${clean_line#docs: }"
elif [[ $line =~ ^refactor ]] || [[ $line =~ ^perf ]]; then
FIXES="$FIXES\n- ${clean_line}"
elif [[ $line =~ ^bump ]]; then
OTHERS="$OTHERS\n- ${clean_line}"
else
OTHERS="$OTHERS\n- ${clean_line}"
fi
done <<< "$LOGS"
# 组合最终正文
BODY=""
[ -n "$FEATS" ] && BODY="$BODY\n### 🚀 新功能 (Features)$FEATS\n"
[ -n "$FIXES" ] && BODY="$BODY\n### 🔧 修复与优化 (Bug Fixes & Refactor)$FIXES\n"
[ -n "$DOCS" ] && BODY="$BODY\n### 📝 文档更新 (Documentation)$DOCS\n"
[ -n "$OTHERS" ] && BODY="$BODY\n### 📦 其他改动 (Others)$OTHERS\n"
[ -z "$BODY" ] && BODY="本次发布包含内部优化与稳定性提升。"
# 处理换行符并存入环境变量(使用多行模式)
{
echo "body<<EOF"
echo -e "$BODY"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: 准备 Go 边车程序
shell: bash
run: |
mkdir -p desktop/src-tauri/bin
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
cd backend && CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -o ../desktop/src-tauri/bin/server-x86_64-pc-windows-msvc.exe cmd/server/main.go
else
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -o ../desktop/src-tauri/bin/server-aarch64-apple-darwin cmd/server/main.go
cd .. && cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -o ../desktop/src-tauri/bin/server-x86_64-apple-darwin cmd/server/main.go
# Universal target 是“虚拟 target”,Tauri 期望用户提供一个通用的 sidecar(二进制需自行 lipo 合并)
if [ "${{ matrix.name }}" = "macOS (Universal)" ]; then
lipo -create \
../desktop/src-tauri/bin/server-aarch64-apple-darwin \
../desktop/src-tauri/bin/server-x86_64-apple-darwin \
-output ../desktop/src-tauri/bin/server-universal-apple-darwin
chmod +x ../desktop/src-tauri/bin/server-universal-apple-darwin
fi
fi
- name: 安装前端依赖
run: npm install
working-directory: ./desktop
- name: 导入 Apple 签名证书 (macOS)
id: import_apple_cert
if: matrix.platform == 'macos-latest'
continue-on-error: true
shell: bash
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
set -euo pipefail
if [ -z "${APPLE_CERTIFICATE:-}" ] || [ -z "${APPLE_CERTIFICATE_PASSWORD:-}" ]; then
echo "No APPLE_CERTIFICATE/APPLE_CERTIFICATE_PASSWORD provided, skip signing." >&2
exit 1
fi
CERT_PATH="$RUNNER_TEMP/tauri-signing-certificate.p12"
printf '%s' "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_PATH"
KEYCHAIN_PASSWORD="$(uuidgen)"
echo "KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> "$GITHUB_ENV"
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security list-keychains -d user -s build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
security import "$CERT_PATH" -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
security find-identity -v -p codesigning build.keychain
CERT_ID="$(security find-identity -v -p codesigning build.keychain | awk -F '\"' '/\"/ {print $2; exit}')"
if [ -z "$CERT_ID" ]; then
echo "No codesigning identity found in keychain" >&2
exit 1
fi
echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> "$GITHUB_ENV"
- name: 构建并发布到 GitHub Release (macOS Signed)
if: matrix.platform == 'macos-latest' && steps.import_apple_cert.outcome == 'success'
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }}
with:
projectPath: './desktop'
args: ${{ matrix.tauriArgs }}
tagName: ${{ github.ref_name }}
releaseName: '大香蕉 AI ${{ github.ref_name }}'
releaseBody: |
## 🎨 大香蕉 AI - 创作从未如此简单
### 🚀 本次更新
${{ steps.release_notes.outputs.body }}
### 📦 包含组件
- 桌面客户端 (Tauri)
- 本地推理引擎 (Go Sidecar)
---
*由 GitHub Actions 自动构建发布 - ${{ env.BUILD_TIME }}*
releaseDraft: false
prerelease: false
includeUpdaterJson: true
includeRelease: true
- name: 构建并发布到 GitHub Release (macOS Unsigned)
if: matrix.platform == 'macos-latest' && steps.import_apple_cert.outcome != 'success'
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
projectPath: './desktop'
args: ${{ matrix.tauriArgs }} --no-sign
tagName: ${{ github.ref_name }}
releaseName: '大香蕉 AI ${{ github.ref_name }}'
releaseBody: |
## 🎨 大香蕉 AI - 创作从未如此简单
### 🚀 本次更新
${{ steps.release_notes.outputs.body }}
### 📦 包含组件
- 桌面客户端 (Tauri)
- 本地推理引擎 (Go Sidecar)
---
*由 GitHub Actions 自动构建发布 - ${{ env.BUILD_TIME }}*
releaseDraft: false
prerelease: false
includeUpdaterJson: true
includeRelease: true
# 注意:macOS 下如果使用 `--no-sign`,会同时跳过 Updater(minisign)签名。
# 为确保 App 内“检查更新”可用,这里统一对 .app.tar.gz 生成并上传 .sig(覆盖同名文件,便于重跑)。
- name: 生成并上传 Updater 签名 (macOS)
if: matrix.platform == 'macos-latest'
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
if [ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
echo "TAURI_SIGNING_PRIVATE_KEY is empty, cannot sign updater bundles." >&2
exit 1
fi
REPO="${{ github.repository }}"
TAG="${{ github.ref_name }}"
cd desktop
KEY_PATH="$RUNNER_TEMP/tauri-updater.key"
printf '%s' "$TAURI_SIGNING_PRIVATE_KEY" > "$KEY_PATH"
TARGET_DIR="src-tauri/target"
# matrix.tauriArgs 形如: "--target aarch64-apple-darwin" / "--target universal-apple-darwin"
TARGET_TRIPLE="$(echo "${{ matrix.tauriArgs }}" | sed -E 's/.*--target[[:space:]]+([^[:space:]]+).*/\1/')"
BUNDLE_DIR="$TARGET_DIR/$TARGET_TRIPLE/release/bundle/macos"
UPDATE_BUNDLE="$(ls -1 "$BUNDLE_DIR"/*.app.tar.gz | head -n 1 || true)"
if [ -z "$UPDATE_BUNDLE" ]; then
echo "Updater bundle not found in: $BUNDLE_DIR" >&2
ls -la "$BUNDLE_DIR" || true
exit 1
fi
# 生成 .sig(输出路径为 <file>.sig)
npm run -s tauri -- signer sign -f "$KEY_PATH" -p "${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-}" "$UPDATE_BUNDLE"
SIG_PATH="${UPDATE_BUNDLE}.sig"
if [ ! -f "$SIG_PATH" ]; then
echo "Signature not generated: $SIG_PATH" >&2
exit 1
fi
# 上传到 Release(覆盖同名文件,便于重跑)
# 注意:tauri-action 可能会在上传时重命名 updater bundle(例如:AI_aarch64.app.tar.gz)。
# 为确保 latest.json 能通过 "<bundle>.sig" 找到签名,这里把本地生成的 .sig 重命名成 Release 上的 bundle 文件名 + .sig 再上传。
if [ "$TARGET_TRIPLE" = "universal-apple-darwin" ]; then
MATCH_RE="universal.*\\.app\\.tar\\.gz$"
else
MATCH_RE="aarch64.*\\.app\\.tar\\.gz$"
fi
REMOTE_BUNDLE_NAME="$(gh release view "$TAG" --repo "$REPO" --json assets --jq '.assets[].name' | grep -iE "$MATCH_RE" | head -n 1 || true)"
if [ -z "$REMOTE_BUNDLE_NAME" ]; then
# 兜底:如果没有按架构命名,直接取第一个 *.app.tar.gz
REMOTE_BUNDLE_NAME="$(gh release view "$TAG" --repo "$REPO" --json assets --jq '.assets[].name' | grep -iE '\\.app\\.tar\\.gz$' | head -n 1 || true)"
fi
if [ -z "$REMOTE_BUNDLE_NAME" ]; then
echo "Cannot find uploaded updater bundle in GitHub Release assets for $TAG" >&2
gh release view "$TAG" --repo "$REPO" --json assets --jq '.assets[].name' || true
exit 1
fi
REMOTE_SIG_NAME="${REMOTE_BUNDLE_NAME}.sig"
cp "$SIG_PATH" "$REMOTE_SIG_NAME"
gh release upload "$TAG" "$REMOTE_SIG_NAME" --clobber --repo "$REPO"
- name: 构建并发布到 GitHub Release (Windows)
if: matrix.platform == 'windows-latest'
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
projectPath: './desktop'
args: ${{ matrix.tauriArgs }}
tagName: ${{ github.ref_name }}
releaseName: '大香蕉 AI ${{ github.ref_name }}'
releaseBody: |
## 🎨 大香蕉 AI - 创作从未如此简单
### 🚀 本次更新
${{ steps.release_notes.outputs.body }}
### 📦 包含组件
- 桌面客户端 (Tauri)
- 本地推理引擎 (Go Sidecar)
---
*由 GitHub Actions 自动构建发布 - ${{ env.BUILD_TIME }}*
releaseDraft: false
prerelease: false
includeUpdaterJson: true
includeRelease: true
# 生成/刷新 latest.json(给 Tauri Updater 使用)
# 说明:当前流水线是 matrix 并行构建,tauri-action 在某些组合/重跑场景下可能不会自动上传 latest.json。
# 这里统一在所有平台构建完成后,根据 Release 资产生成 latest.json 并上传(保证 App 内“检查更新”可用)。
publish-updater-json:
name: 发布 Updater latest.json
needs: build-and-release
if: always()
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
- name: 生成 latest.json 并上传
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
TAG="${{ github.ref_name }}"
REPO="${{ github.repository }}"
echo "Fetching release info: $REPO $TAG"
gh api -H 'Accept: application/vnd.github+json' "repos/${REPO}/releases/tags/${TAG}" > release.json
# 使用 python 构建 platforms 映射(从 Release 资产里找 bundle 与 .sig)
python - <<'PY'
import json, re, urllib.request
with open("release.json", "r", encoding="utf-8") as f:
data = json.load(f)
tag = data.get("tag_name") or ""
version = tag[1:] if tag.startswith("v") else tag
assets = data.get("assets") or []
assets_by_name = {a.get("name"): a for a in assets if a.get("name")}
def find_asset(patterns):
for pat in patterns:
rx = re.compile(pat)
for name, a in assets_by_name.items():
if rx.search(name):
return a
return None
# macOS: updater bundle 是 *.app.tar.gz + *.app.tar.gz.sig
mac_arm = find_asset([r"(?i)aarch64.*\.app\.tar\.gz$", r"(?i)\.app\.tar\.gz$.*aarch64"])
mac_universal = find_asset([r"(?i)universal.*\.app\.tar\.gz$", r"(?i)\.app\.tar\.gz$.*universal"])
# windows: createUpdaterArtifacts=true 模式下,updater 复用 setup.exe + setup.exe.sig
win_exe = find_asset([r"(?i)_x64-setup\.exe$"])
# 对应 .sig(优先同名 + .sig)
def sig_for(asset):
if not asset:
return None
name = asset["name"]
return assets_by_name.get(name + ".sig")
mac_arm_sig = sig_for(mac_arm)
mac_universal_sig = sig_for(mac_universal)
win_sig = sig_for(win_exe)
def fetch_text(url):
with urllib.request.urlopen(url) as f:
return f.read().decode("utf-8").strip()
platforms = {}
# Apple Silicon:优先 aarch64;若缺失则回退到 universal
if mac_arm and mac_arm_sig:
platforms["darwin-aarch64"] = {"url": mac_arm["browser_download_url"], "signature": fetch_text(mac_arm_sig["browser_download_url"])}
elif mac_universal and mac_universal_sig:
platforms["darwin-aarch64"] = {"url": mac_universal["browser_download_url"], "signature": fetch_text(mac_universal_sig["browser_download_url"])}
# Intel:使用 universal 更新包
if mac_universal and mac_universal_sig:
platforms["darwin-x86_64"] = {"url": mac_universal["browser_download_url"], "signature": fetch_text(mac_universal_sig["browser_download_url"])}
if win_exe and win_sig:
platforms["windows-x86_64"] = {"url": win_exe["browser_download_url"], "signature": fetch_text(win_sig["browser_download_url"])}
if not platforms:
raise SystemExit("no platforms collected; cannot generate latest.json")
pub_date = data.get("published_at") or data.get("created_at") or ""
notes = data.get("body") or ""
out = {"version": version or tag, "notes": notes, "pub_date": pub_date, "platforms": platforms}
with open("latest.json", "w", encoding="utf-8") as f:
json.dump(out, f, ensure_ascii=False, indent=2)
print("Generated latest.json with platforms:", ", ".join(platforms.keys()))
PY
gh release upload "$TAG" latest.json --clobber --repo "$REPO"