Skip to content

Release Tauri App

Release Tauri App #36

Workflow file for this run

name: Release Electron App
# 由 semantic-release 创建的 v* tag 触发:构建 Electron 安装包并附加到对应 Release
on:
push:
tags: ['v*']
# 兜底触发:semantic-release 若以内置 GITHUB_TOKEN 推 tag,
# GitHub 防递归机制不会派发 push 事件,导致本工作流从不运行、
# Release 资产为空(latest.yml 404)。Release 发布事件可正常触发补跑。
release:
types: [published]
# 手动兜底:在 UI 选择对应 tag 运行,用于补发历史版本资产
workflow_dispatch:
# 上传构建产物到 GitHub Release 需要写权限
permissions:
contents: write
# 同一 tag 的多个触发源(push/release)串行执行,避免重复上传竞争
concurrency:
group: release-electron-${{ github.ref_name }}
cancel-in-progress: false
jobs:
lint-test:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: client/package-lock.json
- run: cd client && npm ci
- run: cd client && npm run lint
- run: cd client && npm run test -- --run
build:
needs: lint-test
# 本产品为 Windows-only(electron-builder.yml 仅定义 win target)。
# macOS 无有效构建目标必然报 ERR_ELECTRON_BUILDER_CANNOT_EXECUTE,
# 且默认 fail-fast 会连带取消 Windows 构建,导致 release/同步步骤永不执行。
# 如未来支持 mac,需补回带签名的 mac target 后再加入 matrix。
strategy:
matrix:
os: [windows-latest]
runs-on: ${{ matrix.os }}
# Electron 打包 + 上传 + CDN 同步:上限 90 分钟
timeout-minutes: 90
steps:
# lfs: true 必需:app-icon.png 等 PNG 由 Git LFS 托管(.gitattributes:
# *.png filter=lfs),不拉取则仅为 131 字节指针文件,
# electron-builder 图标转换(PNG→ICO)解码失败,
# 报 ERR_ELECTRON_BUILDER_CANNOT_EXECUTE。
- uses: actions/checkout@v4
with:
lfs: true
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: client/package-lock.json
- run: cd client && npm ci
# 进程环回原生模块(ADR-001):windows-latest 自带 VS 2022。
# 必须走 native:build 脚本(本地与 CI 同一入口):直接用 client 下的
# electron-rebuild 会命中旧版 node-gyp,在 CI 报
# "Could not find any Visual Studio installation"(它仍在找 VS 2013/2015)。
# continue-on-error:它是可选增强(运行时未加载到则降级为端点环回),
# 不应因其编译失败而阻断整条发布流水线
- name: Build native process-audio module
continue-on-error: true
run: |
cd client
npm run native:install
npm run native:build
- name: Check native module artifact
shell: bash
run: |
cd client
if [ -f native/process-audio/build/Release/process_audio.node ]; then
echo "进程环回原生模块已就绪"
else
echo "::warning::进程环回原生模块未构建成功,本包仅支持端点环回采集"
fi
# --publish never:发布由下方 release job 统一负责,避免 electron-builder
# 因检测到 tag 而自行尝试发布(github provider 无 GH_TOKEN 会报错)
#
# 更新源(CDN)已直接固化在 electron-builder.yml 的 generic provider 中,
# 本地打包与 CI 打包产物的 app-update.yml 完全一致,不再用
# DOWNLOAD_BASE_URL sed 改写(行锚定模式在 yml 变动后会静默失效,已移除)
- name: Build Electron app
shell: bash
run: cd client && npm run electron:build -- --publish never
- uses: actions/upload-artifact@v4
with:
name: release-${{ matrix.os }}
# latest*.yml 与 *.blockmap 是 electron-updater 自动更新的必需文件
path: |
client/release/*.exe
client/release/*.blockmap
client/release/latest*.yml
release:
needs: build
runs-on: ubuntu-latest
timeout-minutes: 30
# secrets 上下文**不可用于 if 条件**(仅限 env/with/run),否则整个
# workflow 文件解析失败、所有触发立即 0 秒报错。
# 故先将条件所需值注入 job 级 env,再在 step 的 if 中用 env.* 引用。
env:
DOWNLOAD_BASE_URL: ${{ secrets.DOWNLOAD_BASE_URL }}
OSS_BUCKET: ${{ secrets.OSS_BUCKET }}
# 仅作存在性判定的布尔标志,避免将密钥本身提到 job 级 env
HAS_ALIYUN_AK: ${{ secrets.ALIYUN_ACCESS_KEY_ID != '' }}
steps:
- uses: actions/download-artifact@v4
with:
pattern: release-*
- uses: softprops/action-gh-release@v2
with:
files: release-*/*
# ---------------------------------------------------------------
# 同步 Windows 安装包至自建服务器(国内主下载源 + 自动更新源)
# latest.json 供官网下载页动态展示版本/直链;latest.yml 供 electron-updater
# ---------------------------------------------------------------
- name: Generate latest.json for website
id: meta
run: |
YML=release-windows-latest/latest.yml
VERSION=$(grep -m1 '^version:' "$YML" | awk '{print $2}')
FILE=$(grep -m1 '^path:' "$YML" | sed 's/^path: //')
SIZE=$(stat -c%s "release-windows-latest/$FILE")
SHA256=$(sha256sum "release-windows-latest/$FILE" | awk '{print $1}')
DATE=$(date -u +%F)
printf '{"version":"%s","fileName":"%s","size":%s,"sha256":"%s","releaseDate":"%s"}\n' \
"$VERSION" "$FILE" "$SIZE" "$SHA256" "$DATE" > release-windows-latest/latest.json
cat release-windows-latest/latest.json
# 供下方 CDN 预热步骤拼接 URL
echo "file=$FILE" >> "$GITHUB_OUTPUT"
- name: Ensure downloads directory on server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: mkdir -p /opt/Entropydecrease/downloads
# 先传二进制(exe/blockmap),后传元数据(yml/json):
# 避免 latest.yml 率先就位时指向尚未上传完成的安装包(更新 404 窗口期)
- name: Upload installer binaries to server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: release-windows-latest/*.exe,release-windows-latest/*.blockmap
target: /opt/Entropydecrease/downloads
strip_components: 1
- name: Publish update metadata (last, avoids 404 window)
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: release-windows-latest/latest.yml,release-windows-latest/latest.json
target: /opt/Entropydecrease/downloads
strip_components: 1
# ---------------------------------------------------------------
# 同步至 OSS(CDN 源站)
# 为何需要 OSS:ECS 出网仅约 6Mbps,而 CDN 预热需**每个边缘节点各
# 拉一份完整文件**,回源总量 = 节点数 x 172MB,以 ECS 为源站时
# 预热耗时达几十分钟至数小时且会打满带宽影响 API。
# OSS 出网带宽极大,可使预热降至分钟级且完全不占用 ECS。
#
# 路径与 ECS 保持一致(downloads/ 前缀),因此无需改变
# DOWNLOAD_BASE_URL,也无需改前端与 electron-updater 配置。
# ECS 同步仍保留:存量客户端的更新源仍指向 ECS,且作为兜底。
# ---------------------------------------------------------------
- name: Upload to OSS (CDN origin)
if: env.OSS_BUCKET != '' && env.HAS_ALIYUN_AK == 'true'
env:
AK_ID: ${{ secrets.ALIYUN_ACCESS_KEY_ID }}
AK_SECRET: ${{ secrets.ALIYUN_ACCESS_KEY_SECRET }}
OSS_ENDPOINT_IN: ${{ secrets.OSS_ENDPOINT }}
FILE_NAME: ${{ steps.meta.outputs.file }}
run: |
set -eo pipefail
# 未配置 OSS_ENDPOINT 时默认杭州(与 ECS 同地域)
OSS_ENDPOINT="${OSS_ENDPOINT_IN:-oss-cn-hangzhou.aliyuncs.com}"
curl -sL https://aliyuncli.alicdn.com/aliyun-cli-linux-latest-amd64.tgz -o /tmp/aliyun.tgz
tar -xzf /tmp/aliyun.tgz -C /tmp
# 新版 aliyun CLI 的 oss 子命令已移除 -i/-k 短参数(会报
# unknown flag -i),故改用非交互式 configure 写入 profile 携带凭证
/tmp/aliyun configure set --profile ossci --mode AK --region cn-hangzhou \
--access-key-id "$AK_ID" --access-key-secret "$AK_SECRET"
oss_cp() {
/tmp/aliyun oss cp "$1" "oss://$OSS_BUCKET/downloads/$2" \
--force -e "$OSS_ENDPOINT" --profile ossci
}
cd release-windows-latest
# 同 ECS 的顺序约定:先二进制后元数据,避免 latest.yml 率先就位
# 而指向尚未上传完成的安装包
oss_cp "$FILE_NAME" "$FILE_NAME"
oss_cp "$FILE_NAME.blockmap" "$FILE_NAME.blockmap"
oss_cp latest.yml latest.yml
oss_cp latest.json latest.json
echo "OSS sync done: oss://$OSS_BUCKET/downloads/"
# 保留策略:仅保留最近 3 个版本的安装包,防止磁盘增长
- name: Prune old versions (keep last 3)
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /opt/Entropydecrease/downloads
ls -t -- *.exe 2>/dev/null | tail -n +4 | while read -r f; do
rm -f -- "$f" "$f.blockmap"
done
# ---------------------------------------------------------------
# CDN 预热:大文件在 CDN 上按分片缓存,未被请求过的分片不在缓存中,
# 首批用户会退化为实时回源而受源站带宽限制。
# 实测:已缓存分片 10.5MB/s,未缓存分片仅 106KB/s(相差约 95 倍),
# 故预热不是可选优化而是大文件分发的必要步骤。
# 预热为异步任务,API 返回后后台继续拉取,此处不等待完成。
# ---------------------------------------------------------------
- name: Prefetch CDN cache
# 仅在已切换至 CDN 且配置了预热凭证时执行;未配置则跳过
if: env.DOWNLOAD_BASE_URL != '' && env.HAS_ALIYUN_AK == 'true'
# 预热失败不应推翻已成功的发布,仅标记警告
continue-on-error: true
env:
AK_ID: ${{ secrets.ALIYUN_ACCESS_KEY_ID }}
AK_SECRET: ${{ secrets.ALIYUN_ACCESS_KEY_SECRET }}
FILE_NAME: ${{ steps.meta.outputs.file }}
run: |
set -eo pipefail
# 上一步可能被跳过(未配 OSS),故不假设 CLI 已存在
if [ ! -x /tmp/aliyun ]; then
curl -sL https://aliyuncli.alicdn.com/aliyun-cli-linux-latest-amd64.tgz -o /tmp/aliyun.tgz
tar -xzf /tmp/aliyun.tgz -C /tmp
fi
/tmp/aliyun configure set --profile cdnci --mode AK --region cn-hangzhou \
--access-key-id "$AK_ID" --access-key-secret "$AK_SECRET"
BASE="${DOWNLOAD_BASE_URL%/}"
# 预热安装包与 blockmap(差量更新依赖);
# latest.yml/json 按设计不缓存,且仅几百字节,无需预热
OBJECTS="$BASE/$FILE_NAME"$'\n'"$BASE/$FILE_NAME.blockmap"
echo "Prefetching:"
echo "$OBJECTS"
/tmp/aliyun cdn PushObjectCache \
--profile cdnci \
--ObjectPath "$OBJECTS" \
--Area domestic
# ---------------------------------------------------------------
# 安装包体积监控:防止包体积无序膨胀影响用户下载体验与 CDN 成本。
# 当 .exe 超过阈值时仅输出警告(不阻塞发布),以便团队及时排查
# 是否误打包了大文件或依赖。
# ---------------------------------------------------------------
- name: Check installer size
env:
# 安装包体积警告阈值(单位:字节),200MB
SIZE_THRESHOLD: 209715200
run: |
set -eo pipefail
echo '## 📦 安装包体积报告' >> "$GITHUB_STEP_SUMMARY"
echo '' >> "$GITHUB_STEP_SUMMARY"
echo "| 文件 | 大小 |" >> "$GITHUB_STEP_SUMMARY"
echo "|------|------|" >> "$GITHUB_STEP_SUMMARY"
for exe in release-windows-latest/*.exe; do
[ -f "$exe" ] || continue
SIZE=$(stat -c%s "$exe")
NAME=$(basename "$exe")
# 转换为人类可读的 MB
SIZE_MB=$(awk "BEGIN {printf \"%.2f\", $SIZE / 1048576}")
THRESHOLD_MB=$(awk "BEGIN {printf \"%.0f\", $SIZE_THRESHOLD / 1048576}")
echo "| $NAME | ${SIZE_MB} MB |" >> "$GITHUB_STEP_SUMMARY"
if [ "$SIZE" -gt "$SIZE_THRESHOLD" ]; then
echo "::warning::${NAME} 体积 ${SIZE_MB}MB 超过阈值 ${THRESHOLD_MB}MB,请检查是否误打包了大文件或依赖"
echo '' >> "$GITHUB_STEP_SUMMARY"
echo "> ⚠️ **${NAME}** 体积 ${SIZE_MB} MB 超过阈值 ${THRESHOLD_MB} MB,请关注包体积膨胀" >> "$GITHUB_STEP_SUMMARY"
else
echo "${NAME}: ${SIZE_MB} MB(阈值 ${THRESHOLD_MB} MB,正常)"
fi
done
# ---------------------------------------------------------------
# 发布结果校验:"空 Release"已静默发生过两次(v0.27.0 因 macOS 腿
# 拖垮构建、v0.28.7 因 workflow 解析失败),而两次都是事后手工才
# 发现的——此时 electron-updater 拉不到 latest.yml,自动更新静默断链。
# 此步骤使该故障在 CI 中即时暴露。
# ---------------------------------------------------------------
- name: Verify release assets
if: startsWith(github.ref, 'refs/tags/')
env:
GH_TOKEN: ${{ github.token }}
# release job 不做 checkout(仅下载 artifact),gh 无法从 git 仓库
# 推断仓库而报 "not a git repository",故必须显式指定 GH_REPO
GH_REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: |
set -eo pipefail
names=$(gh release view "$TAG" --json assets --jq '.assets[].name')
echo "Assets of $TAG:"
echo "$names"
for required in '\.exe$' '\.exe\.blockmap$' '^latest\.yml$'; do
if ! echo "$names" | grep -qE "$required"; then
echo "::error::Release $TAG 缺少匹配 $required 的资产,自动更新将不可用"
exit 1
fi
done
echo "发布资产完整校验通过"