name: QMAI Multi-Platform Release on: push: tags: - "v*" permissions: contents: write jobs: build-release: name: Build ${{ matrix.display_label }} if: github.event_name == 'push' runs-on: ${{ matrix.runner }} env: CARGO_PROFILE_RELEASE_LTO: "false" CARGO_PROFILE_RELEASE_CODEGEN_UNITS: "16" CARGO_PROFILE_RELEASE_OPT_LEVEL: "1" strategy: fail-fast: false matrix: include: - label: windows-x64 display_label: Windows x64 runner: windows-latest rust_targets: "" tauri_args: "--bundles nsis" artifact_globs: | src-tauri/target/**/release/bundle/nsis/*.exe src-tauri/target/**/release/bundle/nsis/*.exe.sig - label: macos-aarch64 display_label: macOS Apple Silicon runner: macos-latest rust_targets: "aarch64-apple-darwin" tauri_args: "--target aarch64-apple-darwin --bundles dmg,app --no-sign" artifact_globs: | src-tauri/target/**/release/bundle/dmg/*.dmg release-assets/*.app.tar.gz - label: macos-intel display_label: macOS Intel runner: macos-15-intel rust_targets: "x86_64-apple-darwin" tauri_args: "--target x86_64-apple-darwin --bundles dmg,app --no-sign" artifact_globs: | src-tauri/target/**/release/bundle/dmg/*.dmg release-assets/*.app.tar.gz - label: linux-x64 display_label: Linux x64 runner: ubuntu-22.04 rust_targets: "" tauri_args: "--bundles deb,appimage" artifact_globs: | src-tauri/target/release/bundle/deb/*.deb src-tauri/target/release/bundle/appimage/*.AppImage steps: - name: Checkout uses: actions/checkout@v5 - name: Install Rust stable if: matrix.rust_targets == '' uses: dtolnay/rust-toolchain@stable - name: Install Rust stable with targets if: matrix.rust_targets != '' uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.rust_targets }} - name: Rust cache uses: Swatinem/rust-cache@v2 with: workspaces: src-tauri shared-key: qmai-release add-job-id-key: false cache-on-failure: true - name: Install macOS dependencies if: runner.os == 'macOS' run: | brew untap aws/tap || true brew install protobuf - name: Install protoc (Windows) if: runner.os == 'Windows' uses: arduino/setup-protoc@v3 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Install Linux dependencies if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y libwebkit2gtk-4.1-dev librsvg2-dev patchelf protobuf-compiler libgtk-3-dev libayatana-appindicator3-dev - name: Setup Node.js uses: actions/setup-node@v5 with: node-version: 24 cache: npm - name: Install frontend dependencies run: npm ci - name: Prepare release notes shell: bash run: | set -euo pipefail version="$(node -p "require('./package.json').version")" mkdir -p release-assets node scripts/release-notes.mjs "$version" --out release-assets/release-notes.txt - name: Ensure GitHub Release exists env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: bash run: | set -euo pipefail tag="${GITHUB_REF_NAME}" notes_path="release-assets/release-notes.txt" if ! gh release view "$tag" >/dev/null 2>&1; then gh release create "$tag" --title "QMAI $tag" --notes-file "$notes_path" --verify-tag || true fi gh release edit "$tag" --notes-file "$notes_path" --draft=false - name: Build Tauri app env: 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 }} run: npx tauri build ${{ matrix.tauri_args }} - name: Publish Windows updater manifest if: matrix.label == 'windows-x64' env: 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 }} shell: pwsh run: | $ErrorActionPreference = "Stop" $version = node -p "require('./package.json').version" $bundleDir = "src-tauri/target/release/bundle/nsis" $installer = Get-ChildItem $bundleDir -File -Filter "*.exe" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if (-not $installer) { throw "未找到 Windows 安装包" } $signaturePath = "$($installer.FullName).sig" if (-not (Test-Path $signaturePath)) { npx tauri signer sign $installer.FullName if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } if (-not (Test-Path $signaturePath)) { throw "Windows 安装包签名生成失败:$signaturePath" } $releaseDir = "release-assets" New-Item -ItemType Directory -Force -Path $releaseDir | Out-Null $assetName = "QMaiWrite_$($version)_windows_X64$($installer.Extension)" $assetPath = Join-Path $releaseDir $assetName $assetSignaturePath = "$assetPath.sig" Copy-Item $installer.FullName $assetPath -Force $signature = (Get-Content $signaturePath -Raw).Trim() Copy-Item $signaturePath $assetSignaturePath -Force $notesPath = Join-Path $releaseDir "release-notes.txt" $notes = Get-Content -Path $notesPath -Raw -Encoding utf8 $latest = [ordered]@{ version = $version notes = "QMAI $version 发布版本" pub_date = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ", [System.Globalization.CultureInfo]::InvariantCulture) platforms = [ordered]@{ "windows-x86_64" = [ordered]@{ signature = $signature url = "https://github.com/Mochocyang/QMAI/releases/latest/download/$assetName" } } } $latestPath = Join-Path $releaseDir "latest.json" $latest.notes = $notes $latest | ConvertTo-Json -Depth 5 | Set-Content -Path $latestPath -Encoding utf8 gh release edit "${{ github.ref_name }}" --notes-file $notesPath --draft=false gh release upload "${{ github.ref_name }}" $assetPath $assetSignaturePath $latestPath --clobber - name: Package macOS release assets if: runner.os == 'macOS' shell: bash run: | set -euo pipefail version="$(node -p "require('./package.json').version")" if [ "${{ matrix.label }}" = "macos-intel" ]; then system_label="macOS_Intel" else system_label="macOS_AppleSilicon" fi mkdir -p release-assets dmg_path="$(find src-tauri/target -path '*/release/bundle/dmg/*.dmg' -print -quit)" if [ -n "$dmg_path" ]; then cp "$dmg_path" "release-assets/QMaiWrite_${version}_${system_label}.dmg" fi app_path="$(find src-tauri/target -path '*/release/bundle/macos/*.app' -print -quit)" if [ -z "$app_path" ]; then echo "No .app bundle found" exit 1 fi tar -C "$(dirname "$app_path")" -czf "release-assets/QMaiWrite_${version}_${system_label}.app.tar.gz" "$(basename "$app_path")" - name: Attach macOS assets to release if: runner.os == 'macOS' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: bash run: gh release upload "${{ github.ref_name }}" release-assets/QMaiWrite_*_macOS_* --clobber - name: Build Windows portable if: matrix.label == 'windows-x64' shell: pwsh run: | $ErrorActionPreference = "Stop" $version = node -p "require('./package.json').version" node scripts/build-portable.mjs $portableExe = "release-portable/QMaiWrite.exe" if (-not (Test-Path $portableExe)) { throw "便携版 EXE 未生成:$portableExe" } $assetName = "QMaiWrite_${version}_windows_X64_portable.exe" $assetPath = "release-assets/$assetName" New-Item -ItemType Directory -Force -Path "release-assets" | Out-Null Copy-Item $portableExe $assetPath -Force - name: Attach Windows portable to release if: matrix.label == 'windows-x64' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: bash run: gh release upload "${{ github.ref_name }}" release-assets/QMaiWrite_*_portable.exe --clobber - name: Package Linux release assets if: matrix.label == 'linux-x64' shell: bash run: | set -euo pipefail version="$(node -p "require('./package.json').version")" mkdir -p release-assets deb_path="$(find src-tauri/target -path '*/release/bundle/deb/*.deb' -print -quit)" if [ -n "$deb_path" ]; then cp "$deb_path" "release-assets/QMaiWrite_${version}_linux_X64.deb" fi appimage_path="$(find src-tauri/target -path '*/release/bundle/appimage/*.AppImage' -print -quit)" if [ -n "$appimage_path" ]; then cp "$appimage_path" "release-assets/QMaiWrite_${version}_linux_X64.AppImage" fi - name: Attach Linux assets to release if: matrix.label == 'linux-x64' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: bash run: gh release upload "${{ github.ref_name }}" release-assets/QMaiWrite_*_linux_* --clobber - name: Upload bundles as workflow artifacts if: failure() uses: actions/upload-artifact@v6 with: name: qmai-${{ matrix.label }} path: ${{ matrix.artifact_globs }} if-no-files-found: warn retention-days: 14