name: Build Desktop on: workflow_dispatch: inputs: release_tag: description: "Upload to this release tag (e.g. v1.0.0). Leave empty for artifact-only build." required: false default: "" push: tags: - "v*" permissions: contents: write env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} jobs: # ────────────────────────────────────────────── # Windows # ────────────────────────────────────────────── build-windows: runs-on: windows-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set vcpkg path shell: bash run: echo "VCPKG_INSTALLED=$VCPKG_INSTALLATION_ROOT/installed" >> $GITHUB_ENV - name: Restore vcpkg cache id: vcpkg-cache uses: actions/cache/restore@v4 with: path: ${{ env.VCPKG_INSTALLED }} key: vcpkg-win-v2 - name: Install Windows dependencies (vcpkg) if: steps.vcpkg-cache.outputs.cache-hit != 'true' shell: bash env: VCPKG_BINARY_SOURCES: "clear" run: | vcpkg install "harfbuzz[graphite2]:x64-windows-static-release" \ fontconfig:x64-windows-static-release \ freetype:x64-windows-static-release \ icu:x64-windows-static-release - name: Save vcpkg cache if: steps.vcpkg-cache.outputs.cache-hit != 'true' uses: actions/cache/save@v4 with: path: ${{ env.VCPKG_INSTALLED }} key: vcpkg-win-v2 - name: Set Windows build environment shell: bash run: | echo "TECTONIC_DEP_BACKEND=vcpkg" >> $GITHUB_ENV echo "VCPKG_ROOT=$VCPKG_INSTALLATION_ROOT" >> $GITHUB_ENV echo "VCPKGRS_TRIPLET=x64-windows-static-release" >> $GITHUB_ENV echo "VCPKG_DEFAULT_HOST_TRIPLET=x64-windows-static-release" >> $GITHUB_ENV echo "RUSTFLAGS=-Ctarget-feature=+crt-static" >> $GITHUB_ENV echo "CXXFLAGS=/std:c++17" >> $GITHUB_ENV echo "CFLAGS=" >> $GITHUB_ENV - uses: dtolnay/rust-toolchain@stable with: targets: x86_64-pc-windows-msvc - uses: swatinem/rust-cache@v2 with: workspaces: apps/desktop/src-tauri -> target - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm - run: pnpm install - name: Build Tauri app shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ZOTERO_CONSUMER_KEY: ${{ secrets.ZOTERO_CONSUMER_KEY }} ZOTERO_CONSUMER_SECRET: ${{ secrets.ZOTERO_CONSUMER_SECRET }} run: pnpm --filter @claude-prism/desktop tauri build --target x86_64-pc-windows-msvc - name: Collect updater artifacts id: collect shell: bash run: | BUNDLE="apps/desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle" NSIS_SIG=$(find "$BUNDLE/nsis" -name '*-setup.exe.sig' 2>/dev/null | head -1) NSIS_EXE=$(find "$BUNDLE/nsis" -name '*-setup.exe' ! -name '*.sig' 2>/dev/null | head -1) if [ -n "$NSIS_SIG" ] && [ -n "$NSIS_EXE" ]; then echo "sig=$(cat "$NSIS_SIG")" >> $GITHUB_OUTPUT echo "url=$(basename "$NSIS_EXE")" >> $GITHUB_OUTPUT fi - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: desktop-windows path: | apps/desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/**/*.exe apps/desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/**/*.msi apps/desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/**/*.sig if-no-files-found: warn outputs: sig: ${{ steps.collect.outputs.sig }} url: ${{ steps.collect.outputs.url }} # ────────────────────────────────────────────── # macOS (Apple Silicon) # ────────────────────────────────────────────── build-macos: runs-on: macos-14 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install macOS dependencies run: | brew install icu4c harfbuzz pkg-config graphite2 freetype fontconfig INCLUDES="-I$(brew --prefix harfbuzz)/include -I$(brew --prefix freetype)/include -I$(brew --prefix graphite2)/include -I$(brew --prefix icu4c)/include" { echo "PKG_CONFIG_PATH=$(brew --prefix icu4c)/lib/pkgconfig:$(brew --prefix harfbuzz)/lib/pkgconfig:$(brew --prefix graphite2)/lib/pkgconfig:$(brew --prefix freetype)/lib/pkgconfig:$(brew --prefix libpng)/lib/pkgconfig:$(brew --prefix fontconfig)/lib/pkgconfig" echo "CXXFLAGS=-std=c++17 $INCLUDES" echo "CFLAGS=$INCLUDES" echo "LDFLAGS=-L$(brew --prefix harfbuzz)/lib -L$(brew --prefix freetype)/lib -L$(brew --prefix graphite2)/lib -L$(brew --prefix icu4c)/lib" } >> $GITHUB_ENV - name: Import Apple certificate env: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} run: | KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db KEYCHAIN_PASSWORD=$(openssl rand -base64 32) # Create temporary keychain security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" # Import certificate echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12 security import $RUNNER_TEMP/certificate.p12 \ -k "$KEYCHAIN_PATH" \ -P "$APPLE_CERTIFICATE_PASSWORD" \ -T /usr/bin/codesign \ -T /usr/bin/security security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain-db - uses: dtolnay/rust-toolchain@stable with: targets: aarch64-apple-darwin - uses: swatinem/rust-cache@v2 with: workspaces: apps/desktop/src-tauri -> target - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm - run: pnpm install - name: Build Tauri app env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ZOTERO_CONSUMER_KEY: ${{ secrets.ZOTERO_CONSUMER_KEY }} ZOTERO_CONSUMER_SECRET: ${{ secrets.ZOTERO_CONSUMER_SECRET }} APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} TECTONIC_DEP_BACKEND: pkg-config run: pnpm --filter @claude-prism/desktop tauri build --target aarch64-apple-darwin - name: Notarize DMG env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} run: | TARGET="aarch64-apple-darwin" BUNDLE="apps/desktop/src-tauri/target/$TARGET/release/bundle" DMG_PATH=$(find "$BUNDLE/dmg" -name '*.dmg' | head -1) APP_PATH="$BUNDLE/macos/ClaudePrism.app" if [ -z "$DMG_PATH" ]; then echo "Error: DMG not found" exit 1 fi echo "==> Notarizing $DMG_PATH ..." xcrun notarytool submit "$DMG_PATH" \ --apple-id "$APPLE_ID" \ --team-id "$APPLE_TEAM_ID" \ --password "$APPLE_PASSWORD" \ --wait --timeout 30m echo "==> Stapling..." xcrun stapler staple "$DMG_PATH" xcrun stapler staple "$APP_PATH" - name: Collect updater artifacts id: collect run: | BUNDLE="apps/desktop/src-tauri/target/aarch64-apple-darwin/release/bundle" TAR_SIG=$(find "$BUNDLE/macos" -name '*.app.tar.gz.sig' 2>/dev/null | head -1) TAR_GZ=$(find "$BUNDLE/macos" -name '*.app.tar.gz' ! -name '*.sig' 2>/dev/null | head -1) if [ -n "$TAR_SIG" ] && [ -n "$TAR_GZ" ]; then echo "sig=$(cat "$TAR_SIG")" >> $GITHUB_OUTPUT echo "url=$(basename "$TAR_GZ")" >> $GITHUB_OUTPUT fi - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: desktop-macos path: | apps/desktop/src-tauri/target/aarch64-apple-darwin/release/bundle/**/*.dmg apps/desktop/src-tauri/target/aarch64-apple-darwin/release/bundle/**/*.app.tar.gz apps/desktop/src-tauri/target/aarch64-apple-darwin/release/bundle/**/*.sig if-no-files-found: warn outputs: sig: ${{ steps.collect.outputs.sig }} url: ${{ steps.collect.outputs.url }} # ────────────────────────────────────────────── # macOS (Intel x86_64) # Ref: https://github.com/actions/runner-images/issues/13045 # macos-13 retired 2025-12-04, macos-15-intel is the replacement # Intel runners available until Fall 2027 # Ref: https://v2.tauri.app/distribute/pipelines/github/ # Ref: https://v2.tauri.app/plugin/updater/ (platform key: darwin-x86_64) # ────────────────────────────────────────────── build-macos-intel: runs-on: macos-15-intel steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install macOS dependencies run: | brew install icu4c harfbuzz pkg-config graphite2 freetype fontconfig INCLUDES="-I$(brew --prefix harfbuzz)/include -I$(brew --prefix freetype)/include -I$(brew --prefix graphite2)/include -I$(brew --prefix icu4c)/include" { echo "PKG_CONFIG_PATH=$(brew --prefix icu4c)/lib/pkgconfig:$(brew --prefix harfbuzz)/lib/pkgconfig:$(brew --prefix graphite2)/lib/pkgconfig:$(brew --prefix freetype)/lib/pkgconfig:$(brew --prefix libpng)/lib/pkgconfig:$(brew --prefix fontconfig)/lib/pkgconfig" echo "CXXFLAGS=-std=c++17 $INCLUDES" echo "CFLAGS=$INCLUDES" echo "LDFLAGS=-L$(brew --prefix harfbuzz)/lib -L$(brew --prefix freetype)/lib -L$(brew --prefix graphite2)/lib -L$(brew --prefix icu4c)/lib" } >> $GITHUB_ENV - name: Import Apple certificate env: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} run: | KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db KEYCHAIN_PASSWORD=$(openssl rand -base64 32) # Create temporary keychain security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" # Import certificate echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12 security import $RUNNER_TEMP/certificate.p12 \ -k "$KEYCHAIN_PATH" \ -P "$APPLE_CERTIFICATE_PASSWORD" \ -T /usr/bin/codesign \ -T /usr/bin/security security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain-db - uses: dtolnay/rust-toolchain@stable with: targets: x86_64-apple-darwin - uses: swatinem/rust-cache@v2 with: workspaces: apps/desktop/src-tauri -> target - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm - run: pnpm install - name: Build Tauri app env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ZOTERO_CONSUMER_KEY: ${{ secrets.ZOTERO_CONSUMER_KEY }} ZOTERO_CONSUMER_SECRET: ${{ secrets.ZOTERO_CONSUMER_SECRET }} APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} TECTONIC_DEP_BACKEND: pkg-config run: pnpm --filter @claude-prism/desktop tauri build --target x86_64-apple-darwin - name: Notarize DMG env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} run: | TARGET="x86_64-apple-darwin" BUNDLE="apps/desktop/src-tauri/target/$TARGET/release/bundle" DMG_PATH=$(find "$BUNDLE/dmg" -name '*.dmg' | head -1) APP_PATH="$BUNDLE/macos/ClaudePrism.app" if [ -z "$DMG_PATH" ]; then echo "Error: DMG not found" exit 1 fi echo "==> Notarizing $DMG_PATH ..." xcrun notarytool submit "$DMG_PATH" \ --apple-id "$APPLE_ID" \ --team-id "$APPLE_TEAM_ID" \ --password "$APPLE_PASSWORD" \ --wait --timeout 30m echo "==> Stapling..." xcrun stapler staple "$DMG_PATH" xcrun stapler staple "$APP_PATH" - name: Collect updater artifacts id: collect run: | BUNDLE="apps/desktop/src-tauri/target/x86_64-apple-darwin/release/bundle" TAR_SIG=$(find "$BUNDLE/macos" -name '*.app.tar.gz.sig' 2>/dev/null | head -1) TAR_GZ=$(find "$BUNDLE/macos" -name '*.app.tar.gz' ! -name '*.sig' 2>/dev/null | head -1) if [ -n "$TAR_SIG" ] && [ -n "$TAR_GZ" ]; then echo "sig=$(cat "$TAR_SIG")" >> $GITHUB_OUTPUT echo "url=$(basename "$TAR_GZ")" >> $GITHUB_OUTPUT fi - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: desktop-macos-intel path: | apps/desktop/src-tauri/target/x86_64-apple-darwin/release/bundle/**/*.dmg apps/desktop/src-tauri/target/x86_64-apple-darwin/release/bundle/**/*.app.tar.gz apps/desktop/src-tauri/target/x86_64-apple-darwin/release/bundle/**/*.sig if-no-files-found: warn outputs: sig: ${{ steps.collect.outputs.sig }} url: ${{ steps.collect.outputs.url }} # ────────────────────────────────────────────── # Linux # ────────────────────────────────────────────── build-linux: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install Linux dependencies run: | sudo apt-get update sudo apt-get install -y \ libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf \ libicu-dev libgraphite2-dev libharfbuzz-dev libfreetype-dev libfontconfig-dev - uses: dtolnay/rust-toolchain@stable with: targets: x86_64-unknown-linux-gnu - uses: swatinem/rust-cache@v2 with: workspaces: apps/desktop/src-tauri -> target - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm - run: pnpm install - name: Build Tauri app env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ZOTERO_CONSUMER_KEY: ${{ secrets.ZOTERO_CONSUMER_KEY }} ZOTERO_CONSUMER_SECRET: ${{ secrets.ZOTERO_CONSUMER_SECRET }} TECTONIC_DEP_BACKEND: pkg-config CXXFLAGS: "-std=c++17" CFLAGS: "" run: pnpm --filter @claude-prism/desktop tauri build --target x86_64-unknown-linux-gnu - name: Collect updater artifacts id: collect run: | BUNDLE="apps/desktop/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle" APPIMAGE_SIG=$(find "$BUNDLE/appimage" -name '*.AppImage.sig' 2>/dev/null | head -1) APPIMAGE=$(find "$BUNDLE/appimage" -name '*.AppImage' ! -name '*.sig' 2>/dev/null | head -1) if [ -n "$APPIMAGE_SIG" ] && [ -n "$APPIMAGE" ]; then echo "sig=$(cat "$APPIMAGE_SIG")" >> $GITHUB_OUTPUT echo "url=$(basename "$APPIMAGE")" >> $GITHUB_OUTPUT fi - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: desktop-linux path: | apps/desktop/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/**/*.deb apps/desktop/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/**/*.rpm apps/desktop/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/**/*.AppImage apps/desktop/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/**/*.sig if-no-files-found: warn outputs: sig: ${{ steps.collect.outputs.sig }} url: ${{ steps.collect.outputs.url }} # ────────────────────────────────────────────── # Publish: generate latest.json & upload to release # ────────────────────────────────────────────── publish: needs: [build-windows, build-macos, build-macos-intel, build-linux] runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/v') || inputs.release_tag != '' steps: - uses: actions/checkout@v4 - name: Determine tag id: tag run: | if [[ "$GITHUB_REF" == refs/tags/v* ]]; then echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT else echo "tag=${{ inputs.release_tag }}" >> $GITHUB_OUTPUT fi - name: Download all artifacts uses: actions/download-artifact@v4 with: path: artifacts - name: Generate latest.json env: TAG: ${{ steps.tag.outputs.tag }} WIN_SIG: ${{ needs.build-windows.outputs.sig }} WIN_URL: ${{ needs.build-windows.outputs.url }} MAC_SIG: ${{ needs.build-macos.outputs.sig }} MAC_URL: ${{ needs.build-macos.outputs.url }} MAC_INTEL_SIG: ${{ needs.build-macos-intel.outputs.sig }} MAC_INTEL_URL: ${{ needs.build-macos-intel.outputs.url }} LIN_SIG: ${{ needs.build-linux.outputs.sig }} LIN_URL: ${{ needs.build-linux.outputs.url }} run: | VERSION="${TAG#v}" PUB_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") export BASE_URL="https://github.com/delibae/claude-prism/releases/download/$TAG" node -e " const BASE = process.env.BASE_URL; const platforms = {}; if (process.env.MAC_SIG) platforms['darwin-aarch64'] = { signature: process.env.MAC_SIG, url: BASE + '/ClaudePrism-macOS.app.tar.gz' }; if (process.env.MAC_INTEL_SIG) platforms['darwin-x86_64'] = { signature: process.env.MAC_INTEL_SIG, url: BASE + '/ClaudePrism-macOS-Intel.app.tar.gz' }; if (process.env.LIN_SIG) platforms['linux-x86_64'] = { signature: process.env.LIN_SIG, url: BASE + '/ClaudePrism-Linux.AppImage' }; if (process.env.WIN_SIG) platforms['windows-x86_64'] = { signature: process.env.WIN_SIG, url: BASE + '/ClaudePrism-Windows-setup.exe' }; const data = { version: '$VERSION', notes: 'ClaudePrism $TAG', pub_date: '$PUB_DATE', platforms }; require('fs').writeFileSync('latest.json', JSON.stringify(data, null, 2)); console.log(JSON.stringify(data, null, 2)); " - name: Create draft release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | TAG="${{ steps.tag.outputs.tag }}" gh release view "$TAG" > /dev/null 2>&1 || \ gh release create "$TAG" --title "ClaudePrism $TAG" --generate-notes --draft - name: Rename and upload assets env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | TAG="${{ steps.tag.outputs.tag }}" mkdir -p upload # Rename platform artifacts to version-free names # macOS Apple Silicon (from desktop-macos artifact) find artifacts/desktop-macos -name "*.dmg" 2>/dev/null | head -1 | xargs -I{} cp {} upload/ClaudePrism-macOS.dmg find artifacts/desktop-macos -name "*.app.tar.gz" 2>/dev/null | head -1 | xargs -I{} cp {} upload/ClaudePrism-macOS.app.tar.gz # macOS Intel (from desktop-macos-intel artifact) find artifacts/desktop-macos-intel -name "*.dmg" 2>/dev/null | head -1 | xargs -I{} cp {} upload/ClaudePrism-macOS-Intel.dmg find artifacts/desktop-macos-intel -name "*.app.tar.gz" 2>/dev/null | head -1 | xargs -I{} cp {} upload/ClaudePrism-macOS-Intel.app.tar.gz # Windows find artifacts/desktop-windows -name "*-setup.exe" 2>/dev/null | head -1 | xargs -I{} cp {} upload/ClaudePrism-Windows-setup.exe find artifacts/desktop-windows -name "*.msi" 2>/dev/null | head -1 | xargs -I{} cp {} upload/ClaudePrism-Windows.msi # Linux find artifacts/desktop-linux -name "*.deb" 2>/dev/null | head -1 | xargs -I{} cp {} upload/ClaudePrism-Linux.deb find artifacts/desktop-linux -name "*.rpm" 2>/dev/null | head -1 | xargs -I{} cp {} upload/ClaudePrism-Linux.rpm find artifacts/desktop-linux -name "*.AppImage" 2>/dev/null | head -1 | xargs -I{} cp {} upload/ClaudePrism-Linux.AppImage cp latest.json upload/ echo "==> Uploading:" ls -la upload/ for file in upload/*; do gh release upload "$TAG" "$file" --clobber done # Release stays as draft — publish manually from GitHub Releases UI