Skip to content

chore(ci): speed up windows publish #160

chore(ci): speed up windows publish

chore(ci): speed up windows publish #160

Workflow file for this run

name: Publish
on:
push:
tags:
- "v*"
workflow_dispatch:
env:
APP_CARGO_TOML: src-tauri/Cargo.toml
APP_NAME: miaoyu
IS_PRERELEASE: ${{ contains(github.ref_name, '-pre') }}
jobs:
draft:
name: Create Draft Release
runs-on: ubuntu-latest
outputs:
version: ${{ steps.read_version.outputs.value }}
release_id: ${{ steps.create_release.outputs.id }}
release_url: ${{ steps.create_release.outputs.html_url }}
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Extract version from git tag
id: read_version
run: |
VERSION="${GITHUB_REF#refs/tags/}"
VERSION="${VERSION#v}"
echo "value=$VERSION" >> "$GITHUB_OUTPUT"
- name: Generate Changelog
id: changelog
env:
OUTPUT: CHANGELOG-${{ steps.read_version.outputs.value }}.md
run: |
bunx --bun git-cliff --config cliff.toml --verbose --latest --strip header --output "$OUTPUT"
{
echo 'content<<EOF'
cat "$OUTPUT"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
rm "$OUTPUT"
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v2
with:
draft: true
prerelease: ${{ env.IS_PRERELEASE == 'true' }}
tag_name: v${{ steps.read_version.outputs.value }}
name: v${{ steps.read_version.outputs.value }}
body: ${{ steps.changelog.outputs.content }}
generate_release_notes: false
build:
name: Build ${{ matrix.settings.target }}
needs: draft
strategy:
fail-fast: false
matrix:
settings:
- target: x86_64-apple-darwin
runner: macos-latest
os: macOS
- target: aarch64-apple-darwin
runner: macos-latest
os: macOS
- target: x86_64-pc-windows-msvc
runner: windows-latest
os: Windows
runs-on: ${{ matrix.settings.runner }}
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.settings.target }}
- name: Rust Cache (non-Windows)
if: runner.os != 'Windows'
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
key: ${{ matrix.settings.target }}
- name: Enable Git long paths (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: git config --global core.longpaths true
- name: Configure Cargo dirs (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path C:\c | Out-Null
New-Item -ItemType Directory -Force -Path C:\t | Out-Null
- name: Rust Cache (Windows; short dirs)
if: runner.os == 'Windows'
uses: Swatinem/rust-cache@v2
env:
CARGO_HOME: C:\c
CARGO_TARGET_DIR: C:\t
with:
workspaces: src-tauri
key: ${{ matrix.settings.target }}
- name: Clean previous Cargo git checkouts (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$paths = @(
"$env:USERPROFILE\.cargo\git\checkouts",
"C:\c\git\checkouts"
)
foreach ($p in $paths) {
if (Test-Path $p) { Remove-Item -Recurse -Force $p }
}
- name: Restore sccache (Windows)
if: runner.os == 'Windows'
uses: actions/cache@v4
with:
path: C:\sccache
key: sccache-${{ matrix.settings.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
sccache-${{ matrix.settings.target }}-
- name: Setup sccache (Windows)
if: runner.os == 'Windows'
uses: mozilla-actions/sccache@v0.0.3
with:
version: v0.5.4
- name: Create Apple API key file
if: matrix.settings.os == 'macOS'
run: printf '%s' "${{ secrets.APPLE_API_KEY_FILE }}" > api.p8
- name: Import signing certificates
if: matrix.settings.os == 'macOS'
uses: apple-actions/import-codesign-certs@v2
with:
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
- name: Verify signing identity
if: matrix.settings.os == 'macOS'
run: security find-identity -v -p codesigning "$RUNNER_TEMP/build.keychain"
- name: Install dependencies
if: runner.os != 'Windows'
run: bun install
- name: Install dependencies (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: bun install
- name: Create .env file
shell: bash
run: |
cd src-tauri
{
printf 'DEEPSEEK_API_KEY=%s\n' "${{ secrets.DEEPSEEK_API_KEY }}"
printf 'MODELSCOPE_ACCESS_TOKEN=%s\n' "${{ secrets.MODELSCOPE_ACCESS_TOKEN }}"
} > .env
- name: Build Rust artifacts (non-Windows)
if: runner.os != 'Windows'
run: cargo build --manifest-path src-tauri/Cargo.toml --release --target ${{ matrix.settings.target }} --verbose
- name: Build Rust artifacts (Windows; short dirs + git CLI)
if: runner.os == 'Windows'
shell: pwsh
env:
CARGO_NET_GIT_FETCH_WITH_CLI: true
CARGO_HOME: C:\c
CARGO_TARGET_DIR: C:\t
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS: "-Ctarget-feature=+crt-static"
CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_RUSTFLAGS: "-Ctarget-feature=+crt-static"
CARGO_PROFILE_RELEASE_LTO: off
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 16
SHERPA_BUILD_DEBUG: "1"
SHERPA_BUILD_SHARED_LIBS: "1"
RUSTC_WRAPPER: sccache
SCCACHE_DIR: C:\sccache
SCCACHE_CACHE_SIZE: 10G
run: cargo build --manifest-path src-tauri/Cargo.toml --release --target ${{ matrix.settings.target }} --verbose
- name: Build Tauri app (.app only)
if: matrix.settings.os == 'macOS'
env:
CI: false
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_PATH: ${{ github.workspace }}/api.p8
APPLE_KEYCHAIN: ${{ runner.temp }}/build.keychain
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
bun tauri build --target ${{ matrix.settings.target }} --bundles app --config src-tauri/tauri.prod.macos.json
- name: Copy ORT/Sherpa dylibs into Frameworks and fix rpath (macOS)
if: matrix.settings.os == 'macOS'
shell: bash
run: |
set -euo pipefail
BUNDLE_ROOT="src-tauri/target/${{ matrix.settings.target }}/release/bundle/macos"
APP_DIR=$(find "$BUNDLE_ROOT" -maxdepth 2 -type d -name "*.app" | head -n1)
[[ -z "${APP_DIR:-}" ]] && { echo "ERROR: No .app under $BUNDLE_ROOT"; exit 1; }
BIN_DIR="$APP_DIR/Contents/MacOS"
FW_DIR="$APP_DIR/Contents/Frameworks"
mkdir -p "$FW_DIR"
CANDIDATES=("src-tauri/platform-libs/macos" "src-tauri/target/${{ matrix.settings.target }}/release" "$RUNNER_TEMP/miaoyu-libs/macos")
find_one () { local pat="$1"; local out=""; for d in "${CANDIDATES[@]}"; do [[ -d "$d" ]] || continue; out=$(find "$d" -maxdepth 1 -type f -name "$pat" 2>/dev/null | head -n1 || true); [[ -n "$out" ]] && { echo "$out"; return 0; }; done; return 1; }
ORT_VER_SRC=$(find_one "libonnxruntime.*.dylib" || true)
SHERPA_SRC=$(find_one "libsherpa-onnx-c-api*.dylib" || true)
ORT_BARE_SRC=$(find_one "libonnxruntime.dylib" || true)
[[ -z "${ORT_VER_SRC:-}" ]] && { echo "ERROR: no versioned libonnxruntime.*.dylib found"; exit 1; }
[[ -z "${SHERPA_SRC:-}" ]] && { echo "ERROR: no libsherpa-onnx-c-api*.dylib found"; exit 1; }
cp -f "$ORT_VER_SRC" "$FW_DIR/"; cp -f "$SHERPA_SRC" "$FW_DIR/"; [[ -n "${ORT_BARE_SRC:-}" ]] && cp -f "$ORT_BARE_SRC" "$FW_DIR/" || true
ORT_VER="$FW_DIR/$(basename "$ORT_VER_SRC")"; SHERPA="$FW_DIR/$(basename "$SHERPA_SRC")"; ORT_BARE="$FW_DIR/libonnxruntime.dylib"
[[ -f "$ORT_BARE" ]] || ln -s "$(basename "$ORT_VER")" "$ORT_BARE"
EXEC=$(find "$BIN_DIR" -type f -perm -111 | head -n1); [[ -z "${EXEC:-}" ]] && { echo "ERROR: No executable under $BIN_DIR"; exit 1; }
install_name_tool -add_rpath "@loader_path/../Frameworks" "$EXEC" 2>/dev/null || true
install_name_tool -id "@rpath/$(basename "$ORT_BARE")" "$ORT_VER" 2>/dev/null || true
install_name_tool -id "@rpath/$(basename "$SHERPA")" "$SHERPA" 2>/dev/null || true
OLD_REF=$(otool -L "$SHERPA" | awk '/onnxruntime.*dylib/ {print $1; exit}')
if [[ -n "${OLD_REF:-}" && "$OLD_REF" != "@rpath/$(basename "$ORT_BARE")" ]]; then
install_name_tool -change "$OLD_REF" "@rpath/$(basename "$ORT_BARE")" "$SHERPA"
fi
install_name_tool -change "libonnxruntime.dylib" "@rpath/$(basename "$ORT_BARE")" "$EXEC" 2>/dev/null || true
install_name_tool -change "$(basename "$ORT_VER")" "@rpath/$(basename "$ORT_BARE")" "$EXEC" 2>/dev/null || true
install_name_tool -change "$(basename "$SHERPA")" "@rpath/$(basename "$SHERPA")" "$EXEC" 2>/dev/null || true
if security find-identity -v -p codesigning >/dev/null 2>&1; then
codesign --force --timestamp --options runtime --deep --sign "${{ secrets.APPLE_SIGNING_IDENTITY }}" "$APP_DIR"
fi
otool -L "$SHERPA" || true; otool -L "$EXEC" || true; ls -lah "$FW_DIR" || true
- name: Force re-sign with entitlements (macOS)
if: matrix.settings.os == 'macOS'
shell: bash
run: |
set -euo pipefail
BUNDLE_ROOT="src-tauri/target/${{ matrix.settings.target }}/release/bundle/macos"
APP_DIR=$(find "$BUNDLE_ROOT" -maxdepth 2 -type d -name "*.app" | head -n1)
[[ -z "${APP_DIR:-}" ]] && { echo "ERROR: No .app under $BUNDLE_ROOT"; exit 1; }
ENT_PLIST="src-tauri/Entitlements.plist"
[[ -f "$ENT_PLIST" ]] || { echo "Missing Entitlements.plist"; exit 1; }
codesign --force --deep --timestamp --options runtime --entitlements "$ENT_PLIST" --sign "${{ secrets.APPLE_SIGNING_IDENTITY }}" "$APP_DIR"
codesign -dv --entitlements :- "$APP_DIR" | tee verify-entitlements.log
grep -E "audio-input|microphone|app-sandbox" verify-entitlements.log || { echo "::error::Entitlements missing from final signature"; exit 1; }
- name: Create DMG (macOS)
if: matrix.settings.os == 'macOS'
run: |
set -euo pipefail
echo "🧹 Cleaning any previous DMG mounts..."
MOUNTS=$(hdiutil info | awk '/妙语/ {print $1}' || true)
if [[ -n "$MOUNTS" ]]; then
echo "Detaching: $MOUNTS"
while read -r m; do hdiutil detach "$m" -force || true; done <<< "$MOUNTS"
fi
APP_DIR=$(find src-tauri/target/${{ matrix.settings.target }}/release/bundle/macos -maxdepth 2 -type d -name "*.app" | head -n1)
[[ -z "${APP_DIR:-}" ]] && { echo "ERROR: .app not found"; exit 1; }
case "${{ matrix.settings.target }}" in
aarch64-apple-darwin) ARCH_SUFFIX="aarch64" ;;
x86_64-apple-darwin) ARCH_SUFFIX="x64" ;;
*) ARCH_SUFFIX="${{ matrix.settings.target }}" ;;
esac
OUT_DIR="src-tauri/target/${{ matrix.settings.target }}/release/bundle/macos"
OUT_DMG="$OUT_DIR/${{ env.APP_NAME }}_${{ needs.draft.outputs.version }}_${ARCH_SUFFIX}.dmg"
echo "🧹 Cleaning old DMGs..."
rm -f "$OUT_DIR"/*.dmg
STAGE=$(mktemp -d -t miaoyu-stage)
cp -R "$APP_DIR" "$STAGE/"
ln -s /Applications "$STAGE/Applications"
VOLNAME="妙语-${{ matrix.settings.target }}-$(date +%s)"
echo "Creating DMG $OUT_DMG with volname=$VOLNAME ..."
for i in {1..3}; do
if hdiutil create -volname "$VOLNAME" -srcfolder "$STAGE" -ov -format ULMO "$OUT_DMG"; then
break
else
echo "Retrying DMG creation in 5s ($i/3)..."
sleep 5
fi
done
rm -rf "$STAGE"
echo "DMG_OUT=$OUT_DMG" >> $GITHUB_ENV
- name: Notarize & staple DMG (macOS)
if: matrix.settings.os == 'macOS'
shell: bash
env:
APPLE_API_KEY_PATH: ${{ github.workspace }}/api.p8
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
run: |
set -euo pipefail
xcrun notarytool submit "$DMG_OUT" --key "$APPLE_API_KEY_PATH" --key-id "$APPLE_API_KEY" --issuer "$APPLE_API_ISSUER" --wait
xcrun stapler staple "$DMG_OUT"
- name: Sanity check DMG (Frameworks + Applications link)
if: matrix.settings.os == 'macOS'
shell: bash
run: |
set -euo pipefail
MNT=$(mktemp -d)
hdiutil attach "$DMG_OUT" -mountpoint "$MNT" -nobrowse -quiet
APP_PATH=$(find "$MNT" -maxdepth 2 -type d -name "*.app" | head -n1)
[[ -d "$APP_PATH/Contents/Frameworks" ]] || { echo "::error::No Frameworks in DMG app"; hdiutil detach "$MNT" -quiet; exit 1; }
ls -la "$APP_PATH/Contents/Frameworks" || true
test -e "$MNT/Applications" || { echo "::error::Applications link missing"; hdiutil detach "$MNT" -quiet; exit 1; }
hdiutil detach "$MNT" -quiet
- name: Stage runtime DLLs (Windows)
if: matrix.settings.os == 'Windows'
shell: pwsh
env:
TARGET: ${{ matrix.settings.target }} # ← 明确传入
run: |
$ErrorActionPreference = 'Stop' # ← fail-fast
$dst = "src-tauri"
New-Item -ItemType Directory -Force -Path $dst | Out-Null
# 候选目录(把可能的 release/ 和 deps/ 都兜住)
$candidates = @(
"C:\t\$env:TARGET\release",
"C:\t\$env:TARGET\release\deps",
"src-tauri\target\$env:TARGET\release",
"src-tauri\target\$env:TARGET\release\deps",
"src-tauri\bin"
) | Where-Object { Test-Path $_ } | Select-Object -Unique
Write-Host "Search candidates:"
$candidates | ForEach-Object { Write-Host " - $_" }
# 逐项收集;sherpa 用通配并重命名
$need = @(
@{ pat = "onnxruntime.dll"; out = "onnxruntime.dll" },
@{ pat = "onnxruntime_providers_shared.dll"; out = "onnxruntime_providers_shared.dll" },
@{ pat = "sherpa-onnx-c-api*.dll"; out = "sherpa-onnx-c-api.dll" }
)
foreach ($n in $need) {
$hit = $null
foreach ($dir in $candidates) {
$hit = Get-ChildItem -Path $dir -File -Filter $n.pat -ErrorAction SilentlyContinue | Select-Object -First 1
if ($hit) { break }
}
if (-not $hit) { throw "Missing required DLL: $($n.pat)" }
$dstPath = Join-Path $dst $n.out
Copy-Item $hit.FullName $dstPath -Force # ← 强制覆盖
Write-Host "✅ externalBin ready: $dstPath (from $($hit.FullName))"
}
- name: Build Tauri app (Windows)
if: matrix.settings.os == 'Windows'
shell: pwsh
env:
CI: false
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
CARGO_NET_GIT_FETCH_WITH_CLI: true
CARGO_HOME: C:\c
CARGO_TARGET_DIR: C:\t
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS: "-Ctarget-feature=+crt-static"
CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_RUSTFLAGS: "-Ctarget-feature=+crt-static"
CARGO_PROFILE_RELEASE_LTO: off
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 16
SHERPA_BUILD_DEBUG: "1"
SHERPA_BUILD_SHARED_LIBS: "1"
RUSTC_WRAPPER: sccache
SCCACHE_DIR: C:\sccache
SCCACHE_CACHE_SIZE: 10G
run: |
bun tauri build --target ${{ matrix.settings.target }} --config src-tauri/tauri.prod.windows.json
- name: sccache stats (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: sccache --show-stats
- name: Remove Apple API key file
if: matrix.settings.os == 'macOS'
run: rm -f api.p8
- name: Upload release assets (macOS/Linux)
if: runner.os != 'Windows'
env:
TAG_NAME: v${{ needs.draft.outputs.version }}
TARGET: ${{ matrix.settings.target }}
VERSION: ${{ needs.draft.outputs.version }}
RELEASE_ID: ${{ needs.draft.outputs.release_id }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
if [ -n "${DMG_OUT:-}" ] && [ -f "$DMG_OUT" ]; then
gh release upload "$TAG_NAME" "$DMG_OUT" --clobber
else
bundle_dir="src-tauri/target/${TARGET}/release/bundle"
find "$bundle_dir" -type f \( -name '*.dmg' -o -name '*.zip' -o -name '*.tar.gz' -o -name '*.pkg' -o -name '*.json' \) -print0 \
| xargs -0 -I {} gh release upload "$TAG_NAME" "{}" --clobber
fi
- name: Canonicalize Windows artifact filenames
if: matrix.settings.os == 'Windows'
shell: pwsh
env:
VERSION: ${{ needs.draft.outputs.version }}
TARGET: ${{ matrix.settings.target }}
run: |
$candidates = @(
"C:\t\$env:TARGET\release\bundle", # 当设置了 CARGO_TARGET_DIR=C:\t(你现在的情况)
"src-tauri\target\$env:TARGET\release\bundle" # 默认路径(未设置 CARGO_TARGET_DIR 时)
)
$bundle = $null
foreach ($c in $candidates) {
if (Test-Path $c) { $bundle = (Resolve-Path $c).Path; break }
}
if (-not $bundle) {
Write-Host "Tried bundle locations:"
$candidates | ForEach-Object { Write-Host " - $_" }
throw "Bundle directory not found in any known location."
}
Write-Host "Using bundle dir: $bundle"
# 归一化架构名
$arch = if ($env:TARGET -match 'x86_64') { 'x64' }
elseif ($env:TARGET -match 'aarch64') { 'aarch64' }
else { $env:TARGET }
# 递归找到 .exe / .msi 并重命名
Get-ChildItem -Path $bundle -Recurse -File | Where-Object {
$_.Extension -in '.exe','.msi'
} | ForEach-Object {
$newName = "miaoyu_${env:VERSION}_${arch}$($_.Extension)"
$newPath = Join-Path $_.DirectoryName $newName
Move-Item -Force $_.FullName $newPath
Write-Host "Renamed: $($_.Name) → $newName"
}
- name: Upload release assets (Windows)
if: runner.os == 'Windows'
shell: pwsh
env:
TAG_NAME: v${{ needs.draft.outputs.version }}
TARGET: ${{ matrix.settings.target }}
VERSION: ${{ needs.draft.outputs.version }}
RELEASE_ID: ${{ needs.draft.outputs.release_id }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$ErrorActionPreference = 'Stop'
$candidates = @("C:/t/$env:TARGET/release/bundle","src-tauri/target/$env:TARGET/release/bundle")
$bundle = $null
foreach ($c in $candidates) { if (Test-Path $c) { $bundle = (Resolve-Path $c).Path; break } }
if (-not $bundle) {
Write-Host "Tried bundle locations:"; $candidates | ForEach-Object { Write-Host " - $_" }
throw "Bundle directory not found in any known location."
}
Write-Host "Using bundle root: $bundle"
Get-ChildItem -Path $bundle -Recurse -File | Where-Object {
$_.Name -like '*.json' -or $_.Name -like '*.zip' -or $_.Name -like '*.msi' -or $_.Name -like '*.exe'
} | ForEach-Object {
Write-Host "Uploading $($_.FullName)"
gh release upload $env:TAG_NAME $_.FullName --clobber
}
done:
name: Publish Release
needs: [draft, build]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Publish Release
uses: actions/github-script@v7
with:
script: |
await github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: ${{ needs.draft.outputs.release_id }},
draft: false
});
- name: Summary
env:
IS_PRERELEASE: ${{ env.IS_PRERELEASE }}
run: |
TITLE=$([ "${IS_PRERELEASE}" = "true" ] && echo "Pre-release Published" || echo "Release Published")
echo "### 🎉 ${TITLE}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Version: **v${{ needs.draft.outputs.version }}**" >> $GITHUB_STEP_SUMMARY
echo "Release URL: ${{ needs.draft.outputs.release_url }}" >> $GITHUB_STEP_SUMMARY