Build Single Popular App #6
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: Build Single Popular App | |
| env: | |
| NODE_VERSION: "22" | |
| PNPM_VERSION: "10.26.2" | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| name: | |
| description: "App Name" | |
| required: true | |
| default: "twitter" | |
| title: | |
| description: "App Title" | |
| required: true | |
| default: "Twitter" | |
| name_zh: | |
| description: "App Name in Chinese" | |
| required: true | |
| default: "鎺ㄧ壒" | |
| url: | |
| description: "App URL" | |
| required: true | |
| default: "https://twitter.com/" | |
| icon: | |
| description: "App Icon" | |
| required: false | |
| workflow_call: | |
| inputs: | |
| name: | |
| description: "App Name" | |
| type: string | |
| required: true | |
| default: "twitter" | |
| title: | |
| description: "App Title" | |
| required: true | |
| type: string | |
| default: "Twitter" | |
| name_zh: | |
| description: "App Name in Chinese" | |
| required: true | |
| type: string | |
| default: "鎺ㄧ壒" | |
| url: | |
| description: "App URL" | |
| required: true | |
| type: string | |
| default: "https://twitter.com/" | |
| icon: | |
| description: "App Icon" | |
| type: string | |
| required: false | |
| jobs: | |
| build: | |
| name: ${{ inputs.title }} (${{ matrix.build }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| build: [linux, macos, windows] | |
| include: | |
| - build: linux | |
| os: ubuntu-latest | |
| rust: stable | |
| - build: windows | |
| os: windows-latest | |
| rust: stable-x86_64-msvc | |
| - build: macos | |
| os: macos-latest | |
| rust: stable | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| toolchain: ${{ matrix.rust }} | |
| - name: Setup Node.js Environment | |
| uses: ./.github/actions/setup-env | |
| with: | |
| mode: build | |
| - name: Download CLI Artifact | |
| if: github.event_name != 'workflow_dispatch' | |
| uses: actions/download-artifact@v7 | |
| with: | |
| name: pake-cli-dist | |
| path: dist | |
| - name: Build CLI (workflow_dispatch) | |
| if: github.event_name == 'workflow_dispatch' | |
| run: pnpm run cli:build | |
| - name: Setup mold linker | |
| if: matrix.os == 'ubuntu-latest' | |
| uses: rui314/setup-mold@v1 | |
| - name: Prepare macOS signing | |
| if: matrix.os == 'macos-latest' | |
| env: | |
| PAKE_SIGNING_IDENTITY: ${{ secrets.PAKE_SIGNING_IDENTITY }} | |
| PAKE_CERTIFICATE_P12: ${{ secrets.PAKE_CERTIFICATE_P12 }} | |
| PAKE_CERTIFICATE_PASSWORD: ${{ secrets.PAKE_CERTIFICATE_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| SIGNING_IDENTITY="${PAKE_SIGNING_IDENTITY:-}" | |
| if [ -n "${PAKE_CERTIFICATE_P12:-}" ] && [ -n "${PAKE_CERTIFICATE_PASSWORD:-}" ]; then | |
| echo "Importing macOS signing certificate..." | |
| KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db" | |
| KEYCHAIN_PASSWORD="$(openssl rand -base64 16)" | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| echo "$PAKE_CERTIFICATE_P12" | base64 --decode > "$RUNNER_TEMP/certificate.p12" | |
| security import "$RUNNER_TEMP/certificate.p12" -P "$PAKE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" | |
| security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security list-keychain -d user -s "$KEYCHAIN_PATH" | |
| if [ -z "$SIGNING_IDENTITY" ]; then | |
| SIGNING_IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | sed -n 's/.*"\(Developer ID Application:.*\)".*/\1/p' | head -n 1)" | |
| fi | |
| else | |
| echo "No certificate secret configured, fallback to ad-hoc signing." | |
| fi | |
| if [ -z "$SIGNING_IDENTITY" ]; then | |
| SIGNING_IDENTITY="-" | |
| fi | |
| SIGNING_IDENTITY="$SIGNING_IDENTITY" node <<'NODE' | |
| const fs = require('fs'); | |
| const file = 'src-tauri/tauri.macos.conf.json'; | |
| const config = JSON.parse(fs.readFileSync(file, 'utf8')); | |
| config.bundle ??= {}; | |
| config.bundle.macOS ??= {}; | |
| config.bundle.macOS.signingIdentity = process.env.SIGNING_IDENTITY || '-'; | |
| fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`); | |
| NODE | |
| echo "Using signing identity: $SIGNING_IDENTITY" | |
| - name: Build for Linux | |
| if: matrix.os == 'ubuntu-latest' | |
| timeout-minutes: 20 | |
| run: | | |
| ARGS=("${{ inputs.url }}" "--name" "${{ inputs.name }}") | |
| # Auto-detect local icon or use provided icon | |
| if [ -n "${{ inputs.icon }}" ]; then | |
| ARGS+=("--icon" "${{ inputs.icon }}") | |
| elif [ -f "src-tauri/png/${{ inputs.name }}_512.png" ]; then | |
| ARGS+=("--icon" "src-tauri/png/${{ inputs.name }}_512.png") | |
| fi | |
| # Build once with multiple targets (faster than separate builds) | |
| node dist/cli.js "${ARGS[@]}" --targets deb,appimage | |
| mkdir -p output/linux | |
| # The CLI copies files to project root and removes them from bundle directory | |
| # Check project root first, then fallback to bundle directory | |
| if [ -f "${{ inputs.name }}.deb" ]; then | |
| mv "${{ inputs.name }}.deb" output/linux/${{inputs.title}}_`arch`.deb | |
| elif [ -n "$(ls src-tauri/target/release/bundle/deb/*.deb 2>/dev/null)" ]; then | |
| mv src-tauri/target/release/bundle/deb/*.deb output/linux/${{inputs.title}}_`arch`.deb | |
| else | |
| echo "Error: No DEB file found" | |
| find . -name "*.deb" -type f | |
| exit 1 | |
| fi | |
| if [ -f "${{ inputs.name }}.AppImage" ]; then | |
| mv "${{ inputs.name }}.AppImage" output/linux/"${{inputs.title}}"_`arch`.AppImage | |
| elif [ -n "$(ls src-tauri/target/release/bundle/appimage/*.AppImage 2>/dev/null)" ]; then | |
| mv src-tauri/target/release/bundle/appimage/*.AppImage output/linux/"${{inputs.title}}"_`arch`.AppImage | |
| else | |
| echo "Error: No AppImage file found" | |
| find . -name "*.AppImage" -type f | |
| exit 1 | |
| fi | |
| - name: Build for macOS | |
| if: matrix.os == 'macos-latest' | |
| timeout-minutes: 25 | |
| run: | | |
| ARGS=("${{ inputs.url }}" "--name" "${{ inputs.name }}" "--hide-title-bar") | |
| # Auto-detect local icon or use provided icon | |
| if [ -n "${{ inputs.icon }}" ]; then | |
| ARGS+=("--icon" "${{ inputs.icon }}") | |
| elif [ -f "src-tauri/icons/${{ inputs.name }}.icns" ]; then | |
| ARGS+=("--icon" "src-tauri/icons/${{ inputs.name }}.icns") | |
| fi | |
| node dist/cli.js "${ARGS[@]}" --targets universal --multi-arch | |
| mkdir -p output/macos | |
| # The CLI copies the DMG to project root and removes it from bundle directory | |
| # Check project root first, then fallback to bundle directory | |
| if [ -f "${{ inputs.name }}.dmg" ]; then | |
| mv "${{ inputs.name }}.dmg" output/macos/"${{inputs.title}}".dmg | |
| elif [ -n "$(ls src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg 2>/dev/null)" ]; then | |
| mv src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg output/macos/"${{inputs.title}}".dmg | |
| else | |
| echo "Error: No DMG file found" | |
| echo "Searched locations:" | |
| echo " - ${{ inputs.name }}.dmg (project root)" | |
| echo " - src-tauri/target/universal-apple-darwin/release/bundle/dmg/" | |
| find . -name "*.dmg" -type f | |
| exit 1 | |
| fi | |
| - name: Notarize macOS DMG | |
| if: matrix.os == 'macos-latest' | |
| env: | |
| PAKE_NOTARIZE_APPLE_ID: ${{ secrets.PAKE_NOTARIZE_APPLE_ID }} | |
| PAKE_NOTARIZE_TEAM_ID: ${{ secrets.PAKE_NOTARIZE_TEAM_ID }} | |
| PAKE_NOTARIZE_PASSWORD: ${{ secrets.PAKE_NOTARIZE_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${PAKE_NOTARIZE_APPLE_ID:-}" ] || [ -z "${PAKE_NOTARIZE_TEAM_ID:-}" ] || [ -z "${PAKE_NOTARIZE_PASSWORD:-}" ]; then | |
| echo "Notarization secrets not configured, skipping notarization." | |
| exit 0 | |
| fi | |
| DMG_PATH="output/macos/${{inputs.title}}.dmg" | |
| if [ ! -f "$DMG_PATH" ]; then | |
| echo "Error: DMG file not found at $DMG_PATH" | |
| exit 1 | |
| fi | |
| echo "Submitting DMG to Apple notarization service..." | |
| xcrun notarytool submit "$DMG_PATH" \ | |
| --apple-id "$PAKE_NOTARIZE_APPLE_ID" \ | |
| --team-id "$PAKE_NOTARIZE_TEAM_ID" \ | |
| --password "$PAKE_NOTARIZE_PASSWORD" \ | |
| --wait | |
| echo "Stapling notarization ticket..." | |
| xcrun stapler staple "$DMG_PATH" | |
| - name: Build for Windows | |
| if: matrix.os == 'windows-latest' | |
| timeout-minutes: 20 | |
| run: | | |
| $args = "${{ inputs.url }}", "--name", "${{ inputs.name }}" | |
| # Auto-detect local icon or use provided icon | |
| if ("${{ inputs.icon }}" -ne "") { | |
| $args += "--icon", "${{ inputs.icon }}" | |
| } elseif (Test-Path "src-tauri\png\${{ inputs.name }}_256.ico") { | |
| $args += "--icon", "src-tauri\png\${{ inputs.name }}_256.ico" | |
| } | |
| $args += "--targets", "x64" | |
| node dist/cli.js $args | |
| New-Item -Path "output\windows" -ItemType Directory -Force | |
| # The CLI copies the MSI to project root and removes it from bundle directory | |
| # Check project root first, then fallback to bundle directories | |
| $projectRootMsi = "${{ inputs.name }}.msi" | |
| if (Test-Path $projectRootMsi) { | |
| Move-Item -Path $projectRootMsi -Destination "output\windows\${{inputs.title}}_x64.msi" | |
| } else { | |
| # Check architecture-specific path (x64 builds) | |
| $msiFiles = Get-ChildItem -Path "src-tauri\target\x86_64-pc-windows-msvc\release\bundle\msi\*.msi" -ErrorAction SilentlyContinue | |
| # Fallback to generic path | |
| if (-not $msiFiles) { | |
| $msiFiles = Get-ChildItem -Path "src-tauri\target\release\bundle\msi\*.msi" -ErrorAction SilentlyContinue | |
| } | |
| if ($msiFiles) { | |
| Move-Item -Path $msiFiles[0].FullName -Destination "output\windows\${{inputs.title}}_x64.msi" | |
| } else { | |
| Write-Error "No MSI files found in expected locations" | |
| Write-Host "Searched paths:" | |
| Write-Host " - $projectRootMsi (project root)" | |
| Write-Host " - src-tauri\target\x86_64-pc-windows-msvc\release\bundle\msi\" | |
| Write-Host " - src-tauri\target\release\bundle\msi\" | |
| Write-Host "`nAll MSI files in target directory:" | |
| Get-ChildItem -Path "src-tauri\target\" -Recurse -Name "*.msi" | Write-Host | |
| exit 1 | |
| } | |
| } | |
| git checkout -- src-tauri/Cargo.lock | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: ${{ inputs.title }}-${{ matrix.build }} | |
| path: output/*/*.* | |
| retention-days: 3 | |
| - name: Upload to release (Linux) | |
| uses: ncipollo/release-action@v1 # cspell:disable-line | |
| if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/') | |
| with: | |
| allowUpdates: true | |
| artifacts: "output/linux/*.deb,output/linux/*.AppImage" | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Upload to release (macOS) | |
| uses: ncipollo/release-action@v1 # cspell:disable-line | |
| if: matrix.os == 'macos-latest' && startsWith(github.ref, 'refs/tags/') | |
| with: | |
| allowUpdates: true | |
| artifacts: "output/macos/*.dmg" | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Upload to release (Windows) | |
| uses: ncipollo/release-action@v1 # cspell:disable-line | |
| if: matrix.os == 'windows-latest' && startsWith(github.ref, 'refs/tags/') | |
| with: | |
| allowUpdates: true | |
| artifacts: "output/windows/*.msi" | |
| token: ${{ secrets.GITHUB_TOKEN }} |