Skip to content

0.2.15

0.2.15 #19

Workflow file for this run

name: Release
# 触发:本地 `pnpm version <major|minor|patch>` 自动改 package.json + Cargo.toml + commit + tag;
# 之后 `git push && git push --tags`,tag `v*` 推上来这条 workflow 被触发。
#
# 工作流被拆成两阶段,满足 "全部成功才发布 / 任一失败整体中止":
# 1. `build` 矩阵(6 平台并行,fail-fast: true)—— 直接调 `pnpm tauri build`,产物 upload-artifact
# 到 Actions 私有缓存。**不动 Release**。任一 platform 失败立即取消其他 jobs,避免"半成品 draft"
# 被部分 upload 出去。
# 2. `release` job(needs: build, if: success())—— 下载所有 6 个 artifact,给 installer 加平台
# 前缀,自己合成 `latest.json`,最后用 softprops/action-gh-release 一次性创建 draft Release +
# 上传所有 asset。
#
# 为什么不用 tauri-apps/tauri-action:它把 build + 创建 release + upload + 合并 latest.json 全打包
# 到一个 step,无法在多 matrix job 之间做 "全部成功才上传" 的原子切片。直接调 Tauri CLI 损失的只是
# keychain import 的便利——Tauri 2 CLI 自身已经认 `APPLE_CERTIFICATE` 等 env 自动处理 keychain。
#
# 首次启用前必做(一次性):
# 1. `pnpm tauri signer generate -w ~/.tauri/openspeech.key`(已完成)
# 2. 把输出 pubkey 贴到 src-tauri/tauri.conf.json → plugins.updater.pubkey(已完成)
# 3. GitHub Secrets:
# - TAURI_SIGNING_PRIVATE_KEY (整个 key 文件内容)
# - TAURI_SIGNING_PRIVATE_KEY_PASSWORD (生成时输入的密码,无密码留空)
# 4. **组织级权限**:OpenLoaf org → Settings → Actions → General → Workflow permissions 必须设
# "Read and write permissions"。否则 release job 创建 draft Release 时返回 "Resource not
# accessible by integration"。这是组织级配置,repo 改不了,也无法用 yml `permissions:` 提权。
#
# macOS 代码签名 + 公证(Secrets 命名跟 OpenLoaf 仓库对齐,便于跨项目复用一套 Apple 证书):
# MAC_CER_BASE64 = Developer ID Application 证书 .p12 的 base64
# MAC_CER_PASSWORD = 上述 .p12 的导出密码
# APPLE_SIGNING_IDENTITY = "Developer ID Application: 你的名字 (TEAMID)"
# APPLE_ID = Apple ID 邮箱
# APPLE_APP_SPECIFIC_PASSWORD= App-Specific Password(appleid.apple.com 生成)
# APPLE_TEAM_ID = 10 位 Team ID
# 全部为空时 Tauri CLI 自动跳过签名,回退到未签名包。
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
build:
strategy:
# 任一 platform 失败立即取消其他正在跑的 jobs,避免:
# 1. 部分 platform 跑一两个小时再因为已知失败而被人工 cancel,浪费配额;
# 2. release job 因为 needs: build 失败永远不会进,所以让 build 早死早超生。
fail-fast: true
matrix:
include:
# ---- macOS ----
- name: macOS ARM64
platform: macos-latest
rust-target: aarch64-apple-darwin
args: '--target aarch64-apple-darwin'
artifact: macOS-arm64
arch-tag: aarch64
# Intel 也跑在 Apple Silicon runner 上交叉编译 x86_64 —— GitHub 已把 macos-13
# (Intel)从免费 runner 池退役,留下来的 Intel runner 走付费 Larger Runner 标签,
# 现状 v0.2.0/v0.2.1 的 Intel job 永远排不到队。dtolnay/rust-toolchain 里声明
# target,cargo 自动交叉编译;OpenSpeech 的 Rust 依赖(cpal/tauri/reqwest 等)
# 没有需要 brew 的 native 依赖,纯 cross OK。OpenLoaf 仓库 publish-desktop.yml
# 同样的处理。
- name: macOS Intel
platform: macos-latest
rust-target: x86_64-apple-darwin
args: '--target x86_64-apple-darwin'
artifact: macOS-intel
arch-tag: x64
# ---- Linux ----
- name: Linux x86_64
platform: ubuntu-22.04
rust-target: ''
args: ''
artifact: Linux-x86_64
arch-tag: ''
- name: Linux ARM64
platform: ubuntu-22.04-arm
rust-target: ''
args: ''
artifact: Linux-arm64
arch-tag: ''
# ---- Windows ----
- name: Windows x86_64
platform: windows-latest
rust-target: ''
args: ''
artifact: Windows-x86_64
arch-tag: ''
- name: Windows ARM64
platform: windows-11-arm
rust-target: ''
args: ''
artifact: Windows-arm64
arch-tag: ''
name: Build · ${{ matrix.name }}
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v5
with:
version: 10
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 20
cache: 'pnpm'
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.rust-target }}
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
key: ${{ matrix.rust-target || matrix.platform }}
# Linux 打包 deb + rpm + AppImage 三件套需要的全套工具链:
# - 运行时/编译:webkit2gtk-4.1(不是 4.0)、gtk-3、ayatana appindicator、librsvg2、openssl
# - 音频:libasound2-dev(cpal/alsa-sys 找 alsa.pc)
# - 打包:patchelf / rpm / fakeroot / file / wget / libfuse2
# - **xdg-utils**:tauri-plugin-opener 在 Linux 打包阶段会校验 `/usr/bin/xdg-open` 存在,
# 缺则直接 "failed to bundle project xdg-open binary not found"。
- name: Install Linux dependencies
if: startsWith(matrix.platform, 'ubuntu-')
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libssl-dev \
libasound2-dev \
patchelf \
rpm \
fakeroot \
file \
wget \
libfuse2 \
xdg-utils
- name: Install frontend deps
run: pnpm install --frozen-lockfile
# 直接调 Tauri CLI;macOS 签名 / 公证 / updater 签名都靠 env 透传给 CLI 自己处理。
# tauri-action 替你做的 keychain import 是 Tauri CLI 2.x 已经内置的能力,env 配齐即可。
- name: Tauri build
shell: bash
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
# macOS 签名+公证 env(其它平台无害;CLI 看到没填的 env 自动跳过对应步骤)
APPLE_CERTIFICATE: ${{ secrets.MAC_CER_BASE64 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.MAC_CER_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
# tauri.conf.json 默认 `createUpdaterArtifacts: false`(让本地 / 协作者裸跑 build 不需要 updater 私钥);
# 发版构建在这里临时 override 打开,CI 的 TAURI_SIGNING_PRIVATE_KEY 已注入。
run: pnpm tauri build ${{ matrix.args }} --config '{"bundle":{"createUpdaterArtifacts":true}}'
# 防 ad-hoc 倒灌:tauri-action / Tauri CLI 在 Apple secrets 缺失时会**自动跳签**,产物变 ad-hoc。
# ad-hoc 包跑起来每次访问 Keychain 都会弹"输入登录密码"框(每次签名指纹都变 → ACL 不认)。
# 校验逻辑:
# - 配齐了 `MAC_CER_BASE64` → 强校验 `Authority=Developer ID Application` + `TeamIdentifier`,
# 回退 ad-hoc 直接 fail,避免悄悄退化。
# - 没配 `MAC_CER_BASE64` → 跳过校验(视为"开发者尚未配齐签名 secrets,本次接受 ad-hoc 包"),
# 但仍然在日志里 echo 出 codesign 信息便于排查。
- name: Verify macOS code signature
if: startsWith(matrix.platform, 'macos')
shell: bash
env:
RUST_TARGET: ${{ matrix.rust-target }}
MAC_CER_BASE64: ${{ secrets.MAC_CER_BASE64 }}
run: |
set -euo pipefail
app="src-tauri/target/${RUST_TARGET}/release/bundle/macos/OpenSpeech.app"
out=$(codesign -dvvv "$app" 2>&1 || true)
echo "$out"
if [[ -z "${MAC_CER_BASE64:-}" ]]; then
echo "::warning::MAC_CER_BASE64 secret not configured — skipping signature verification (ad-hoc bundle accepted)."
exit 0
fi
if echo "$out" | grep -qE '^Signature=adhoc'; then
echo "::error::OpenSpeech.app 是 ad-hoc 签名 — Apple Developer ID secrets 没生效"
exit 1
fi
if echo "$out" | grep -qE '^TeamIdentifier=not set'; then
echo "::error::OpenSpeech.app 缺 TeamIdentifier — 不是 Developer ID Application 证书"
exit 1
fi
if ! echo "$out" | grep -qE '^Authority=Developer ID Application'; then
echo "::error::OpenSpeech.app 不是 Developer ID Application 签名"
exit 1
fi
# macOS .app.tar.gz 默认命名是 `OpenSpeech.app.tar.gz` —— 不带 arch!ARM64 + Intel 两个 job
# 上传同名 artifact 互不影响(按 artifact name 分组),但下载到 release job 的 staging 目录
# 时会同名覆盖。因此在 upload 之前就给它加上 arch 后缀,跟 dmg/exe/AppImage 那种"自带 arch
# 后缀"的命名风格对齐。`.sig` 同步重命名(updater 拉 .app.tar.gz 时按同名找 sig)。
- name: Tag macOS updater artifact with arch
if: startsWith(matrix.platform, 'macos')
shell: bash
env:
ARCH_TAG: ${{ matrix.arch-tag }}
RUST_TARGET: ${{ matrix.rust-target }}
run: |
set -euo pipefail
version=$(node -p "require('./package.json').version")
dir="src-tauri/target/${RUST_TARGET}/release/bundle/macos"
if [[ -f "$dir/OpenSpeech.app.tar.gz" ]]; then
mv "$dir/OpenSpeech.app.tar.gz" "$dir/OpenSpeech_${version}_${ARCH_TAG}.app.tar.gz"
mv "$dir/OpenSpeech.app.tar.gz.sig" "$dir/OpenSpeech_${version}_${ARCH_TAG}.app.tar.gz.sig"
echo "[mv] OpenSpeech.app.tar.gz -> OpenSpeech_${version}_${ARCH_TAG}.app.tar.gz"
fi
- name: Upload bundle artifacts
uses: actions/upload-artifact@v7
with:
name: bundle-${{ matrix.artifact }}
if-no-files-found: error
retention-days: 3
# 各平台只取实际产出的文件类型;nullglob 行为由 actions/upload-artifact 自己处理。
path: |
src-tauri/target/**/release/bundle/dmg/*.dmg
src-tauri/target/**/release/bundle/macos/*.app.tar.gz
src-tauri/target/**/release/bundle/macos/*.app.tar.gz.sig
src-tauri/target/**/release/bundle/nsis/*-setup.exe
src-tauri/target/**/release/bundle/nsis/*-setup.nsis.zip
src-tauri/target/**/release/bundle/nsis/*-setup.nsis.zip.sig
src-tauri/target/**/release/bundle/deb/*.deb
src-tauri/target/**/release/bundle/rpm/*.rpm
src-tauri/target/**/release/bundle/appimage/*.AppImage
src-tauri/target/**/release/bundle/appimage/*.AppImage.tar.gz
src-tauri/target/**/release/bundle/appimage/*.AppImage.tar.gz.sig
release:
name: Publish draft release
needs: build
if: success()
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- name: Download all bundles
uses: actions/download-artifact@v7
with:
path: artifacts
pattern: bundle-*
# staging/ 是即将上传到 Release 的扁平目录。**所有文件统一重命名为**
# `OpenSpeech-<version>-<platform>.<ext>`,包括 updater 链路(.app.tar.gz / .nsis.zip /
# .AppImage.tar.gz + .sig)。`matrix.artifact` 已经一次性表达 platform+arch(macOS-arm64 /
# Linux-x86_64 / Windows-arm64 等),不再保留 Tauri 默认名里的冗余 arch 段。
# 由于 `latest.json` 在下一个 step 里**自己生成**、url 字段直接拼新文件名,updater 客户端
# 拉到的 url ↔ 实际 asset 仍然自洽。
- name: Stage release files
shell: bash
env:
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
mkdir -p staging
version="${TAG#v}"
for art in artifacts/bundle-*; do
[[ -d "$art" ]] || continue
platform="${art##*/bundle-}" # 例:macOS-arm64 / Linux-x86_64 / Windows-arm64
base_name="OpenSpeech-${version}-${platform}"
while IFS= read -r f; do
[[ -f "$f" ]] || continue
base="$(basename "$f")"
case "$base" in
# 注意 `*.tar.gz` / `*.tar.gz.sig` 的匹配要放在 `*.gz` 之前;用具体扩展规避歧义
*.app.tar.gz) cp "$f" "staging/${base_name}.app.tar.gz" ;;
*.app.tar.gz.sig) cp "$f" "staging/${base_name}.app.tar.gz.sig" ;;
*.AppImage.tar.gz) cp "$f" "staging/${base_name}.AppImage.tar.gz" ;;
*.AppImage.tar.gz.sig) cp "$f" "staging/${base_name}.AppImage.tar.gz.sig" ;;
*.nsis.zip) cp "$f" "staging/${base_name}-setup.nsis.zip" ;;
*.nsis.zip.sig) cp "$f" "staging/${base_name}-setup.nsis.zip.sig" ;;
*.dmg) cp "$f" "staging/${base_name}.dmg" ;;
*.exe) cp "$f" "staging/${base_name}-setup.exe" ;;
*.deb) cp "$f" "staging/${base_name}.deb" ;;
*.rpm) cp "$f" "staging/${base_name}.rpm" ;;
*.AppImage) cp "$f" "staging/${base_name}.AppImage" ;;
*)
echo "::warning::unrecognized artifact, skipping: $f"
;;
esac
done < <(find "$art" -type f)
done
echo "--- staging/ ---"
ls -la staging/
# latest.json 是 Tauri updater 客户端 fetch 的入口(plugins.updater.endpoints 指向它)。
# 字段:version / pub_date / platforms[<platform-arch>].{signature, url}。
# signature 取 .sig 文件全文;url 指向 Release 上的对应 asset 直链(保留原名)。
- name: Generate latest.json
shell: bash
env:
TAG: ${{ github.ref_name }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
cd staging
pub_date="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
platforms='{}'
for sig in *.sig; do
[[ -f "$sig" ]] || continue
base="${sig%.sig}"
sig_content="$(cat "$sig")"
url="https://github.com/${REPO}/releases/download/${TAG}/${base}"
# stage step 已经把 platform 写进文件名(OpenSpeech-<version>-<platform>.<ext>),
# 这里直接按 -<platform>. / -<platform>- 段匹配,比凑 arch 关键字稳。
case "$base" in
OpenSpeech-*-macOS-arm64.app.tar.gz) key="darwin-aarch64" ;;
OpenSpeech-*-macOS-intel.app.tar.gz) key="darwin-x86_64" ;;
OpenSpeech-*-Linux-x86_64.AppImage.tar.gz) key="linux-x86_64" ;;
OpenSpeech-*-Linux-arm64.AppImage.tar.gz) key="linux-aarch64" ;;
OpenSpeech-*-Windows-x86_64-setup.nsis.zip) key="windows-x86_64" ;;
OpenSpeech-*-Windows-arm64-setup.nsis.zip) key="windows-aarch64" ;;
*)
echo "::warning::unknown sig pattern, skipping: $base"
continue
;;
esac
platforms="$(jq -n --argjson p "$platforms" --arg key "$key" --arg sig "$sig_content" --arg url "$url" \
'$p + {($key): {signature: $sig, url: $url}}')"
done
jq -n \
--arg version "$TAG" \
--arg pub_date "$pub_date" \
--argjson platforms "$platforms" \
'{version: $version, notes: "Auto-generated release", pub_date: $pub_date, platforms: $platforms}' \
> ../latest.json
echo "--- latest.json ---"
cat ../latest.json
# Release 正文 = changelog 原文,不再追加下载表 / 自动更新说明等内容。
# changelog SSoT = `docs/changelogs/{version}/zh.md`(缺失则回退 en.md,再缺回退占位文)。
- name: Build release notes
shell: bash
env:
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
version="${TAG#v}"
changelog_dir="docs/changelogs/${version}"
if [[ -f "${changelog_dir}/zh.md" ]]; then cp "${changelog_dir}/zh.md" release_notes.md
elif [[ -f "${changelog_dir}/en.md" ]]; then cp "${changelog_dir}/en.md" release_notes.md
else
echo "本版本暂未提供变更说明,详细改动请查看 [commit 历史](https://github.com/${GITHUB_REPOSITORY}/commits/${TAG})。" > release_notes.md
echo "::warning::缺失 ${changelog_dir}/zh.md 与 en.md,使用占位文案"
fi
echo "--- release_notes.md ---"
cat release_notes.md
- name: Publish draft release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ github.ref_name }}
name: 'OpenSpeech ${{ github.ref_name }}'
draft: true
prerelease: false
fail_on_unmatched_files: true
body_path: release_notes.md
files: |
staging/*
latest.json