0.2.15 #19
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |